지난 포스팅에선 어떤 클래스의 인스턴스 개수를 프로그램에서 하나로 제한하고 전역 접근을 제공하는 싱글턴 패턴에 대해 알아보았다. 이번엔 메소드 호출을 캡슐화 하는 커맨드 패턴에 대해 복습한다.

정의

커맨드 패턴은 요구 사항을 객체로 캡슐화하며 매개변수를 이용하여 여러 가지 다른 요구 사항을 추가한다. 커맨드 패턴을 이용하면 캡슐화된 메소드 호출을 큐에 저장하거나 로그로 기록하거나 작업취소 등의 기능도 작성할 수 있다.

다이어그램

Invoker는 클라이언트가 필요한 작업을 요청할 때 사용한다. Command 객체를 구성요소로 갖고 있으며 set 메소드를 이용하여 커맨드 객체를 설정해야한다.

Receiver는 실제 호출될 메소드를 갖고 있는 클래스이다. Invoker를 거쳐 Command 객체로부터 특정 행동을 요청받는다.

Command는 리시버에 전달할 일련의 행동들로 구성된 클래스로 행동과 리시버에 대한 정보가 담겨있다. Command의 execute() 메소드로 Invoker의 요청을 받아 Receiver에 있는 특정 메소드를 호출한다.

클라이언트는 Invoker, Receiver, Command 클래스의 인스턴스를 생성해야한다. 제일 먼저 커맨드를 사용하기 위해 Invoker를 생성한 뒤, 실제 호출될 메소드를 갖고 있는 Receiver를 생성한다. 마지막으로 메소드 호출이 캡슐화된 Comamnd를 생성하면서 리시버를 넘겨준 뒤 인보커의 메소드를 호출한다.

예시

리모컨으로 전등을 제어하는 예시를 살펴보자. Receiver인 전등은 이미 만들어져 있다고 가정한다.

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()메소드를 Command 클래스 내부에 캡슐화한다. 마지막으로 전등을 리모컨으로 제어하기 위한 인보커를 만든다.

public class SimpleRemoteControl() {
    Command slot;
    public void setCommand(Command command) {
   	 slot = command;
    }

    public void buttonWasPressed() {
    	slot.execute();
    }
}

리모컨 Invoker의 슬롯에 커맨드를 설정하여 버튼이 눌렸을 때 커맨드의 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())을 캡슐화 시키는 것이다. 리모컨의 버튼마다 특정 커맨드 객체를 저장해 두면 사용자가 버튼을 눌렀을 때 커맨드 객체를 통해서 작업을 처리하게 된다. 사용자는 리모컨 사용법만 알고 있고 리모컨은 어떤 객체에 어떤 일을 시켜야 할지만 알고 있으면 된다. 결국 리모컨과 전등 객체를 완전히 분리시킬 수 있게 된다.

다른 예시를 살펴보자. 이 예시에서는 고객이 주방장 리시버를 생성하거나 하지 않는다. 리시버에 대한 정보는 커맨드의 역할을 하는 주문서에 들어있다.

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

다른 객체와 마찬가지로 주문서는 여기저기로 전달될 수 있으며 이 객체의 인터페이스에는 orderUp()이라는 메소드가 있다. orderUp() 메소드는 식사를 준비하기 위한 행동을 하는 메소드가 캡슐화되어 있다.

요리를 만드는 하는 객체(주방장)에 대한 래퍼런스는 주문서에 들어있다.

이런 내용은 캡슐화되어 있으므로 이를 전달하는 웨이트리스 인보커는 어떤 내용이 주문되었는지, 누가 식사를 준비할지 등을 전혀 몰라도 된다. 주문서를 주방이나 적당한 곳에 갖다 주고 주문이 들어왔다고 알리기만 하면 된다.

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

웨이트리스가 손님한테 주문을 받고 손님을 도와주다가 계산대로 가서 orderUp() 메소드를 호출하여 주방장이 식사를 준비할 수 있도록 하면 된다.

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

여러 고객이 여러 주문서를 매개변수로 전달할 수 있다. 주문이 많이 들어오더라도 모든 주문서에는 orderUp()이라는 메소드가 있으며 그 메소드를 호출하기만 하면 식사가 준비될 것이다.

주방장은 요리를 준비하는 데 필요한 정보를 가지고 있다.

요리를 준비하는 방법은 리시버인 주방장만 알고 있다. 웨이트리스가 orderUp() 메소드를 호출하면 주방장이 그 주문을 받아서 음식을 만들기 위한 메소드를 전부 처리한다. 주방장과 웨이트리스는 완전히 분리되었다는 점을 기억해야한다. 고객의 주문을 받아 음식 준비를 요청하는 웨이트리스와 요리 준비를 요청받아 처리하는 주방장은 서로가 누군지 몰라도 식당은 제 역할을 할 수 있게 된다.

내용추가

작년에 작성한 이 포스트를 정리하면서도, 커맨드 패턴이 요구사항을 캡슐화한다는 의미가 잘 와닿지 않았다. 이는 단순히 메소드 호출을 캡슐화한다는 의미로 해석할 수 있다. 특정 메소드에 대한 요청을 객체의 형태로 캡슐화하여 보낸 요청을 이용할 수 있도록 메소드의 이름, 매개변수 등 요청에 필요한 정보를 저장, 로깅 또는 취소할 수 있게 된다. 커맨드라는 객체로 다른 객체의 메소드 호출을 캡슐화한다. 예를 들어 TV, 전등, 에어컨 3개의 객체가 있고 각각 on, off에 해당하는 기능이 있다고 가정하자. 이러한 전자기기의 종류는 앞으로 더 추가될 수 있다. 문제점을 분석해보자. 각 전자제품에는 on, off 기능뿐만 아니라 각각의 다른 기능이 존재한다. TV는 채널을 변경해야하며 에어컨은 온도를 조절하는 기능을 수행해야한다. 다른 제품이 추가되는 경우에도 각 제품은 자신만의 기능이 존재할 것이다. 이러한 객체는 리모컨을 이용해서 제어할 수 있다. 리모컨을 하나의 객체로 본다면, 리모컨 자체는 자신이 제어하는 가전제품이 어떻게 동작하는지 알 필요가 없다. 그저 리모컨 버튼이 눌렸을 때, TV 객체에게 전원을 키라는 요청을 하거나 볼륨을 조절하라는 요청을 하면된다. 이를 단순한 코드로 구현하면 리모컨에는 각 기능에 해당하는 메소드를 만들어야한다. 이는 리모컨 제조회사에서 제품을 만들고 리모컨을 만들 때 문제가 된다. 제품에 해당하는 리모컨을 만들고, 리모컨은 다시 제품의 기능에 해당하는 메소드를 각각 만들어야한다. 이는 리모컨과 제품이 서로 심하게 의존적인 상황을 의미한다. 반면, 커맨드 패턴을 이용하면 이야기가 달라진다. Command 라는 공통 인터페이스를 정의한다. 제품이 추가되면 해당 제품의 메소드 호출을 캡슐화하기 위해 Command 인터페이스를 구현한 구상 Command를 제품의 기능만큼 만든다. 리모컨은 새로운 클래스를 추가할 필요가 없다. 비어있는 리모컨을 생성한 뒤, 각 버튼에 새로 만든 구상 Command를 설정한다. 각 버튼이 눌리면 설정된 Command의 기능이 호출되어 결국 TV의 on, off 등의 기능이 호출되게 된다. 커맨드 패턴은 작업을 호출하는 쪽과 작업을 처리하는 쪽을 분리하기 위한 패턴이다. 리모컨에서 작업을 요청하고 업체에서 제작한 전자제품이 그 작업을 처리한다. 리모컨은 TV가 무엇을 하는지 알아낼 방법이 없으며, TV 또한 누가 자신에게 작업을 요청했는지 알아낼 방법이 없다. 커맨드 패턴을 이용하면 요청 히스토리, 매크로, 작업 취소 등의 기능을 제공할 수 있다.

커맨드 패턴 예제