커맨드 패턴(Command Pattern) ; 캡슐화

요구사항을 객체로 캡슐화할 수 있으며, 매개변수를 써서 여러 가지 다른 요구사항을 집어 넣을 수 있다.
이를 통해 요청 내역을 큐에 저장하거나, 로그를 기록할 수 있으며, 작업취소 기능도 지원 가능하다.

또한 각 커맨드는 캡슐화 되므로, 안에서 어떤 방식으로 구현되던 상관없이 리시버 객체는 커맨드를 호출하기만 하면,
각 커맨드는 각자의 지정되어 있는 동작을 수행한다.

책에서 나오는 예시 - 리모컨 API 개발

리모컨 API를 개발해야 하는데 요구사항은 다음과 같다.

  • 리모컨에는 On / off 버튼이 있다. 여기에는 어떠한 전자기기에 대한 제어가 주어질 지 모른다.
    • ex) 어떤 TV라도 이 리모컨으로 on/off 를 제어할 수 있어야 한다.
  • on/off 할 수 있는 영역은 총 7개가 주어진다.

즉 만능 리모컨을 만들어야 하는 문제가 주어지는데, 가장 문제는 각 기기마다 on/off 에 대한 인터페이스가 제각각 이라는 것이다.

따라서 리모컨 API 에서는 제각각인 전자기기의 인터페이스를 하나로 통일할 필요가 있다.

커맨드 패턴을 활용한 해결

RemoteController 클래스

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
// 인보커 객체
public class RemoteControllerWithUndo {
private static final int COMMANDS_SIZE = 7;
private List<Command> onCommands;
private List<Command> offCommands;
private Command undoCommand;

public RemoteControllerWithUndo() {
onCommands = new ArrayList<>(COMMANDS_SIZE);
offCommands = new ArrayList<>(COMMANDS_SIZE);

Command noCommand = new NoCommand();
for (int i = 0; i < COMMANDS_SIZE; i++) {
onCommands.add(noCommand);
offCommands.add(noCommand);
}

undoCommand = noCommand;
}

public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands.set(slot, onCommand);
offCommands.set(slot, offCommand);
}

public void onButtonWasPushed(int slot) {
Command onCommand = onCommands.get(slot);
onCommand.execute();
undoCommand = onCommand;
}

public void offButtonWasPushed(int slot) {
Command offCommand = offCommands.get(slot);
offCommand.execute();
undoCommand = offCommand;
}

public void undoButtonWasPushed() {
undoCommand.undo();
}
}

Command 인터페이스

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

void undo();
}

LightOffCommand

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
public class LightOnCommand implements Command {
private Light light;

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

@Override
public void execute() {
light.on();
}

@Override
public void undo() {
light.off();
}
}

public class LightOffCommand implements Command {
private Light light;

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

@Override
public void execute() {
light.off();
}

@Override
public void undo() {
light.on();
}
}

NoCommand ; null 객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Null 객체는 리턴할 객체는 없지만, 클라이언트에서 null 처리를 하지 않아도 되도록 하고 싶은 경우 사용
*/
public class NoCommand implements Command {

@Override
public void execute() {

}

@Override
public void undo() {

}
}

결론

Command 인터페이스 구현체는 자신이 하려고 하는 Action 을 캡슐화 하고,
클라이언트 측에선 인터페이스로 추상화 하여 한층 유연하게 Command를 사용할 수 있다.

생각해보자

Spring에서 Command 의 예시로는 PoolBase의 내부 클래스인 SynchronousExecutor.execute(Runnable command)
정도로 볼 수 있을 것 같다.

Spring 같은 고도화된 프레임워크 내에선 특정 디자인 패턴이 단독으로 쓰이는 경우는 거의 없고,
여러 디자인 패턴이 복합적으로 구현되어 있다.

또한, 최근에는 디자인 패턴을 더욱 더 추상화한 라이브러리등의 지원으로 고전적인 디자인 패턴이 적용된 구현체를 찾기가
매우매우 어렵다ㅠㅠ..

invoke와 command pattern

invoke ; 사전적 의미는 ‘들먹이다, 부르다, 불러오다’ 이다.

Method 클래스 내에 정의되어 있는 invoke(Object obj, Object... args) 를 호출하면,
특정 메소드명으로, 매개변수를 직접 주입하여 실행이 가능하다.
그렇다면, invoke() 메서드 같은 것도 Command 패턴의 일종이라 볼 수 있을까?

Thread 와 Runnable 인터페이스

일반적으로 Thread 를 사용하려면, Runnable 구현체를 직접 정의하고, 이를 Thread 객체 생성 시 넘겨주고,
thread.start(); 식으로 사용하곤 한다.
그렇다면, 일종의 Command Pattern 으로 볼 수도 있는 거 아닌가? 라는 생각도 들었다.

결론

사실 어떤 클래스와 그 설계가 커맨드 패턴이다 아니다는 중요한게 아니라고 생각한다.
다만, 커맨드 패턴이 변화하는 부분에 대해 캡슐화를 하고, 이를 사용하는 측면에서는 유연하게 사용할 수 있게 도와준다는 점에서
혹시 이런 측면에서 구현된 좋은 설계 구현체가 있나 살펴보는 것일 뿐..

참고자료