본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 6. 커맨드 패턴



지난 포스팅에선 어떤 클래스의 인스턴스 개수를 프로그램에서 하나로 제한하고 전역 접근을 제공하는 싱글턴 패턴에 대해 알아보았다.


이번엔 메소드 호출을 캡슐화 하는 커맨드 패턴에 대해 복습한다.


커맨드 패턴

요구 사항을 객체로 캡슐화할 수 있으며 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있다.

커맨드 패턴을 이용하면 캡슐화된 메소드 호출을 큐에 저장하거나 로그로 기록할 수 있으며, 작업취소 기능도 지원 가능하다.




다른 패턴에 비해 다이어그램이 조금 복잡하다.


(1) 인보커: 클라이언트로가 필요한 작업을 요청할 때 사용한다.

   커맨드 객체를 구성요소로 갖고 있으며 세터를 지원해서 반드시 커맨드 객체를 설정해야한다.


(2) 리시버: 실제 호출될 메소드를 갖고 있는 객체, 인보커를 거쳐 커맨드 객체로부터 특정 행동을 요청받는다.


(3) 커맨드: 리시버에 전달할 일련의 행동들로 구성된 클래스로 행동과 리시버에 대한 정보가 들어있다.

               커맨드 객체에서 제공하는 메소드는 execute()로

    인보커의 요청을 받아 리시버에 있는 특정 메소드를 호출한다.


클라이언트는 위 세 개의 클래스의 인스턴스를 생성해야한다.

제일 먼저 커맨드를 사용하기 위해 인보커를 생성한다.


다음으로 실제 호출될 메소드를 갖고 있는 리시버를 생성한다.

이제 메소드 호출이 캡슐화된 커맨트 객체를 생성하면서 리시버를 넘겨준다.


마지막으로 인보커에 setCommand()로 생성한 커맨드 객체를 넘겨주고 인보커의 메소드를 호출하면 된다.


이 과정을 말로 설명하려니까 복잡한데 예제 코드를 보면 이해가 잘된다.

리시버(여기서는 전등)는 이미 만들어져 있다고 가정한다.


가장 먼저 커맨드 인터페이스를 만든다.

public interface Command {

public void execute();

}


다음으로 전등을 켜기 위한 메소드 호출을 캡슐화하기 위해 커맨트 클래스를 구현한다.

public class LightOnCommand implements Command {

Light light;


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

}


public void execute() {

light.on();

}


}


전등을 키는 on()메소드를 캡슐화했다.


다음으로 전등을 리모컨으로 제어하기 위한 인보커를 만든다.

public class SimpleRemoteControl() {

Command slot;


public void setCommand(Command command) {

slot = command;

}


public void buttonWasPressed() {

slot.execute();

}

}


리모컨 인보커의 슬롯에 커맨드를 설정하여 버튼이 눌렸을 때 커맨드의 execute()가 호출되게 했다.


이제 실제로 사용해보면 된다.

public class RemoteControlTest {

public static void main(String[] args) {

SimpleRemoteControl remote = new SimpleRemoteControl(); // 인보커

Light light = new Light(); // 리시버

LightOnCommand lightOn = new LightOnCommand(light); // 커맨드, 리시버를 전달


remote.setCommand(lightOn); // 커맨드 설정

remote.buttonWasPressed();

}

}


결국 리시버인 Light의 on() 메소드가 호출 될 것이다.

이렇게 커맨드 패턴을 사용하면 어떤 작업을 요청한 쪽하고 그 작업을 처리하는 쪽을 분리시킬 수 있다.

특정 객체(Light)에 대한 특정 요청(Light.on())을 캡슐화 시키는 것이다.

리모컨의 버튼마다 특정 커맨드 객체를 저장해 두면 사용자가 버튼을 눌렀을 때 커맨드 객체를 통해서 작업을 처리하게 된다.

사용자는 리모컨 사용법만 알고 있고 리모컨에서는 어떤 객체에 어떤 일을 시켜야 할지만 알고 있으면 된다.

결국 리모컨과 전등 객체를 완전히 분리시킬 수 있게 된다.


책에서 나온 다른 예제를 살펴보자.



우선 여기선 고객이 앞에 예제처럼 리시버인 주방장을 생성하거나 하지 않는다.

리시버에 대한 정보는 커맨드인 주문서에 들어있다.


(1) 주문서는 주문한 메뉴를 요구하는 역할을 하는 객체로 생각할 수 있다.

- 다른 객체와 마찬가지로 주문서는 여기저기로 전달될 수 있으며 이 객체의 인터페이스에는 orderUp()이라는 메소드가 있다.

  orderUp() 메소드는 식사를 준비하기 위한 행동을 하는 메소드가 캡슐화되어 있다.


(2) 식사를 주문해야 하는 객체(주방장)에 대한 래퍼런스는 주문서에 들어있다.

- 이런 내용은 캡슐화되어 있으므로 이를 전달하는 웨이트리스는 어떤 내용이 주문되었는지,

  누가 식사를 준비할지 등을 전혀 몰라도 된다.

  그저 주문서를 주방이나 적당한 곳에 갖다 주고 주문이 들어왔다고 알리기만 하면 된다.


(3) 웨이트리스는 주문서를 받아 주문서의 orderUp() 메소드를 호출한다.

- 손님한테 주문을 받고, 손님을 도와주다가 계산대로 가서 orderUp()메소드를 호출하여

  주방장이 식사를 준비할 수 있도록 하면 된다.


(4) 웨이트리스에는 takeOrder()라는 주문서를 받기 위한 메소드가 있다.

- 여러 고객이 여러 주문서를 매개변수로 전달할 수 있다.

  주문이 많이 들어오더라도 모든 주문서에는 orderUp()이라는 메소드가 있으며

  그 메소드를 호출하기만 하면 식사가 준비될 것이다.


(5) 주방장은 식사를 준비하는 데 필요한 정보를 가지고 있다.

- 실제로 식사를 준비하는 방법은 리시버인 주방장만 알고 있다.

  웨이트리스가 orderUp()메소드를 호출하면 주방장이 그 주문을 받아서

  음식을 만들기 위한 메소드를 전부 처맇나다.



* 여기서 주방장과 웨이트리스는 완전히 분리되었다는 점을 기억해야한다.

앞서 커맨드 패턴을 사용하면 작업을 요청하는 쪽과 작업을 처리하는 쪽을 분리시킬 수 있다고 했다.

고객의 주문을 받아 음식 준비를 요청하는 웨이트리스.

음식 준비를 요청받아 음식 준비를 처리하는 주방장은 서로 누군지 몰라도 식당은 제 역할을 할 수 있게 된다.

이러한 분리가 이루어지는 것은 주문서라는 커맨드, 이를 전달하는 웨이트리스라는 인보커,

마지막으로 작업을 처리하는 주방장이라는 리시버로 구성했기 때문에 가능하게된다.



책에서 딱히 위의 다이어그램에서 주방장이 리시버, 웨이트리스가 인보커, 주문서가 커맨드라고 정의해주지 않았지만

예제의 흐름을 보면 이렇게 분류되는 것 같다.

확실하지는 않으므로 이를 읽는 사람이 있다면 이를 정답이라고 받아들이지 않길 바란다.


UndoCommand와 undo()를 이용해 이전작업 취소 기능을 제공할 수도 있고,

Command[] 배열을 이용해 일련의 작업을 한꺼번에 처리하는 매크로 커맨드를 제공할 수도 있고,

요청을 큐나 로그에 기록해서 히스토리와 같은 기능을 제공할 수도 있다.


메소드 호출을 캡슐화 하는 커맨드 패턴에 대해 복습을 마치겠다.



1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴

4-2. 추상 팩토리 패턴

5. 싱글턴 패턴

6. 커맨드 패턴

7. 어댑터 패턴