커맨드 패턴(Command Pattern)

Head First - Design Patterns
(에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠 저 | 서환수 역)
을 읽고 정리한 내용입니다.


커맨드 패턴(Command Pattern)이란

  • 커맨드 패턴은 호출 캡슐화에 중점을 두는 패턴이다.

예를 들어, 리모컨 API 디자인에 대해 생각해보자.
얼핏 생각하면 리모컨에는 ON/OFF 버튼만 있으면 될 것 같지만,
클래스들과 메서드들이 다양하게 들어 있다.
게다가 나중에 또 다른 제품이 추가될 경우에는 메서드가 또 추가될 것이다.
어떻게 해야 할까?

리모컨 버튼을 눌렀을 때 자동으로 해야할 일을 처리하게 만들되,
리모컨 자체에서는 욕조를 켜는 방법과 같이
어떤 일을 수행하는지 자세한 내용을 모르도록 캡슐화 한다.
복잡한 인터페이스를 간단하게 추상화함으로써 구현할 수 있다.

커맨드 객체는 특정 객체에 대한 특정 작업 요청을 캡슐화시켜준다.
버튼마다 커맨드 객체를 저장해두고, 클라이언트가 버튼을 눌렀을 때 커맨드 객체를 통해서 작업을 처리하게 만든다.
리모컨에서는 자세한 내용은 전혀 몰라도 된다.
그저 리모컨에는 어떤 객체에 어떤 일을 시켜야 할지를 잘 알고 있는 커맨드 객체만 있으면 된다.
리모컨과 전등 객체를 완전히 분리시키는 것이다.

리모컨 버튼이 눌렸을 때 호출되는 코드와 특정 업체에서 제공한,
실제로 일을 처리하는 코드를 분리시켜야 한다.
여기에서 포인트는 모든 일을 처리해주는 메서드를 만들어서 캡슐화한다는 것이다.


리모컨용 코드를 만들어보자.

1. Command 인터페이스 만들기

커맨드 객체는 모두 같은 인터페이스를 구현해야 한다.
일반적으로 excute() 메소드를 사용한다.

1
2
3
public interface Command {
public void execute();
}

2. 전등을 켜기 위한 커맨드 클래스 구현

전자제품 공급 업체에서 제공한 클래스를 보니 Light 클래스에 on()off() 두 개의 메서드가 있다.
커맨드 객체를 만들기 위해 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 커맨드 객체이므로 Command 인터페이스를 구현
public class LightonCommand implements Command {
// 인스턴스 변수에 전등 객체를 저장한다.
Light light;

// 생성자에 이 커맨드 객체로 제어(on, off)할 전등 종류에 대한 정보가 전달된다.
public LightonCommand(Light light) {
this.light = light;
}

// execute() 메서드가 호출될 때,
// Recevier 객체인 light 객체에 있는 on() 메서드가 호출된다.
public void execute() {
light.on();
}
}

커맨드 객체 사용하기

버튼이 하나만 있는 리모컨이 있다고 가정하고 커맨드 객체를 사용하는 코드를 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class simpleRemoteControl {
// 커맨드를 넣을 하나의 슬롯으로 제어한다.
Command slot;

public simpleRemoteControl() {}

// 클라이언트에서 리모컨의 명령을 바꾸고 싶다면,
// 커맨드 객체를 바꿔 끼울 수 있다.
public void setCommand(Command command) {
slot = command;
}

// 버튼이 눌려지면 이 메서드가 호출된다.
// 지금 연결되어 있는 커맨드 객체의 execute() 메서드가 호출된다.
public void buttonWasPressed() {
slot.execute();
}
}

리모컨을 사용하기 위한 간단한 테스트 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// RemoteControlTest: 커맨드 패턴에서 클라이언트에 해당하는 부분
public class RemoteControlTest {
public static void main(String[] args) {
// remote 변수가 인보커(Invoker) 역할
// 필요한 작업을 요청할 때 사용할 커맨드 객체를 인자로 받을 예정이다.
SimpleRemoteControl remote = new SimpleRemoteControl();
//요청을 받아서 처리할 리시버(Receiver)인 Light 객체를 만든다.
Light light = new Light();

LightOnCommand lightOn = new LightOnCommand(light);

// 커맨드 객체를 인보커(remote 변수)에 전달
remote.setCommand(lightOn);
// 인보커인 remote에서 buttonWasPressed 메서드가 실행되면,
// 지금 리모컨에 연결되어 있는 커맨드 객체(버튼에 연결된)인
// lightOn 객체의 excute() 메서드를 실행한다.
remote.buttonWasPressed();
}
}
// 실행 결과: Light is On

커맨드 패턴의 정의

커맨트 패턴
: 요구사항을 객체로 캡슐화할 수 있으며,
매개변수를 사용하여 여러 가지 다른 요구 사항을 집어넣을 수 있다.
요청 내역을 큐에 저장하거나 로그로 기록할 수 있고, 작업취소 기능도 지원한다.

  • 어떤 행동을 특정 리시버와 연결시킴으로써 요구 사항을 캡슐화한 것이다.
  • 이를 구현하기 위해, 행동과 리시버를 한 객체에 넣고
    execute() 메서드 하나만 외부에 공개하는 방법을 사용한다.
  • 특정 인터페이스만 구현되어 있다면 그 커맨드 객체에서 실제로 어떤 일을 하는지는
    신경 쓸 필요 없다.

클래스 다이어그램

  • Client는 ConcreteCommand를 생성하고 Reciver를 설정한다.
  • Invoker에서는 명령이 들어있고, exceute() 메서드를 호출함으로써 커맨드 객체에게 특정한 작업을 수행해 달라고 한다.
  • Command모든 커맨드 객체에서 구현해야 하는 인터페이스이다.
  • ConcreteCommand는 특정 행동과 리시버 사이를 연결해준다.
  • execute() 메서드에는 리시버에 있는 메서드를 호출하여 요청된 작업을 수행한다.
  • Reciver는 요구 사항을 수행하기 위해서 어떤 일을 처리해야 하는지 알고 있는 객체

Light 객체를 사용할 때, 거실에 있는 전등과 부엌에 있는 전등을 구분하는 방법?

  • 클라이언트가 버튼을 누를 때, 그냥 excuete() 메서드를 호출하는데,
    이를 구분하기 위해서는 서로 다른 구현체를 만든 후,
    Command 객체를 바꿔가면서 호출하면 된다.
    • Stategy pattern도 적용된 것으로 생각하면 된다.

슬롯에 명령 할당하기

  1. 리모컨의 각 슬롯에 명령을 할당한다.
    리모컨이 인보커가 되는 것이다.
  2. 사용자가 버튼을 누르면, 그 버튼에 연결된 커맨드 객체의 execute()메서드가 호출된다.
  3. 리시버(전등, 선풍기, 오디오 등)에서 특정 행동을 하는 메서드가 실행되어 작업을 처리한다.

널 객체

특정 슬롯을 사용하려고 할 때, 거기에 뭔가가 로딩되어 있는지 확인하려면
null 체크하는 코드가 반복된다.
ex) null 체크하는 코드

1
2
3
4
5
public void onButtonWasPushed(int slot) {
if (onCommands[slot] != null) {
onCommands[slot].execute();
}
}
  • 이 문제를 해결하기 위해서 아무것도 하지 않는 커맨드 클래스를 구현한다.
1
2
3
public class NoCommand implements Command {
public void execute() {}
}
  • 그 후, RemoteContorl 생성자에서 모든 슬롯에 기본 커맨드 객체로
    NoComamnd 객체를 집어넣는다.
1
2
3
4
5
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}

모든 슬롯에 커맨드 객체가 들어있을 수 밖에 없게 된다.
커맨드 객체를 대입하지 않은 슬롯에는 NoCommand가 들어가게 된다.

NoCommand 객체

NoCommand 객체는 일종의 널 객체이다.
리턴할 객체는 없지만, 클라이언트 쪽에서 null을 처리하지 않아도 되도록 하고 싶을 때 널 객체를 활용하면 좋다.


작업 취소 기능

UNDO 버튼을 지원하는 기능을 추가한다.
거실 전등이 꺼져 있고, 리모컨에 ON 버튼이 눌렸다고 생각해보자.
그럼 불이 켜진다. 이제 UNDO 버튼을 누르면 마지막으로 했던 작업이 취소되어야 한다.
즉, 이 경우에는 켜졌던 거실 전등이 꺼져야 한다.

1. Command에 undo() 메서드를 만든다.

커맨드에서 작업 취소 기능을 지원하기 위해 execute()와 비슷한 undo() 메서드를 만든다.
execute()메서드에서 했던 것과 정반대의 작업을 처리하면 된다.

1
2
3
4
public interface Command {
public void execute();
public void undo();
}

2-1) LigntOnCommand에 undo()메서드를 추가한다.

LightOnCommand에 undo()메서드가 호출되면 그 메서드에서는 light객체의 off()메서드를 호출해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LightOnCommand implements Command {
Light light;

public LightOnCommand(Light light) {
this.light = light;
}

public void execute() {
light.on();
}
public void undo() {
light.off();
}
}

2-2) LightOffCommand에 undo() 메서드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LightOffCommand implements Command {
Light light;

public LightOffCommand(Light light) {
this.light = light;
}

public void execute() {
light.off();
}
public void undo() {
// 불이 꺼져있으면, 다시 켠다.
light.on();
}
}

3. RemoteControl 클래스에 작업 취소 기능을 추가한다.

RemoteControl 클래스에 클라이언트가 마지막으로 누른 버튼을 기록하고,
UNDO 버튼이 눌렸을 때 필요한 작업을 처리하기 위한 코드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class RemoteControlWithUndo {
// 이 리모컨에서는 7개의 on/off 명령을 처리할 수 있으며
// 여러 개의 Command를 호출해야 하므로 배열로 나타낸다.
Command[] onCommands;
Command[] offCommands;
/*
undoCommand가 배열이 아닌 이유?
가장 최근 커맨드 객체 1개를 되돌리기 위해서 배열이 아닌 것이다.
참조를 지속해서 바꿔준다.
전체를 되돌리려면, Stack으로 구현한다.
(Stack에서 pop()메서드로 빼내면 된다.)
*/
Command undoCommand;

public RemoteControlWithUndo() {
onCommands = new Command[7];
offCommands = new Command[7];

Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = offCommand;
}
// 클라이언트가 다른 버튼을 누르지 않은 상태에서
// UNDO 버튼을 누르더라도 문제가 생기지 않도록 만든다.
undoCommand = noCommand;
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
undoCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}

public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
// 클라이언트가 버튼을 누르면, 슬롯에 연결된 커맨드 객체의
// execute()메서드를 호출한 후
// 그 객체의 참조를 undoCommand에 바꿔 끼워준다.
undoCommand = onCommands[slot];
}

public void offButtonWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
// 클라이언트가 UNDO 버튼을 누르면,
// undoCommand에 연결된 커맨드 객체의
// undo() 메서드를 호출한다.
public void undoButtonWasPushed() {
// undo() 메서드가 실행되면,
// 가장 최근에 했던 작업이 취소된다.
undoCommand.undo();
}

public String toString() {
// toString 코드...
}
}

정리

  • 커맨드 패턴은 요청을 하는 객체와 그 요청을 수행하는 객체를 분리시켜서 구현한다.
  • 기존의 코드를 건드리지 않으면서 캡슐화하여
    호출한 객체 입장에서는 어떤 식으로 일을 처리하는지를 전혀 신경 쓰지 않는다.
  • 정해진 한 개의 메서드인 excuete()호출만하면 된다.
    -> 모든 일을 처리해주는 메서드를 만들어서 캡슐화한다.
  • 작업 취소의 히스토리 기능은 Stack을 이용해 구현한다.