[디자인 패턴] (GoF 행위패턴) Command Pattern에 대하여
안녕하세요! GoF 디자인 패턴 연재, 이번 시간부터는 객체 간의 상호작용 및 책임 분배에 관한 **행동 패턴(Behavioral Patterns)**에 대해 알아봅니다. 그 첫 번째 주자는 커맨드(Command) 패턴입니다!
정의
커맨드 패턴은 요청(Request) 그 자체를 하나의 객체로 캡슐화하는 패턴입니다. 이렇게 캡슐화된 요청(커맨드 객체)은 매개변수로 전달되거나, 큐에 저장되거나, 로그로 기록되는 등 다양한 방식으로 활용될 수 있으며, 요청을 보내는 객체(Invoker)와 요청을 실제로 처리하는 객체(Receiver)를 분리할 수 있습니다.
레스토랑 주문 과정을 생각하면 쉽습니다.
- 손님(Client)은 메뉴판을 보고 '스테이크'를 주문합니다.
- 웨이터(Invoker)는 주문 내용('스테이크 주문서'라는 Command 객체)을 받습니다. 웨이터는 스테이크를 어떻게 요리하는지 모릅니다. 단지 주문서를 주방에 전달할 뿐입니다.
- 주방장(Receiver)은 주문서를 보고 실제로 스테이크를 요리합니다.
이 과정에서 '주문서(Command)'라는 객체 덕분에 손님과 주방장은 완전히 분리됩니다. 웨이터는 주문서를 큐에 쌓아두었다가 순서대로 처리하거나, 특정 주문을 취소하는 등의 작업을 할 수 있습니다. 이처럼 요청을 객체로 다루는 것이 커맨드 패턴의 핵심입니다.
커맨드 패턴은 다음과 같은 구조
- Command (커맨드): 모든 커맨드 객체들이 구현해야 할 공통 인터페이스입니다. 보통 execute()라는 단일 메서드를 가집니다.
- ConcreteCommand (구체적인 커맨드): Command 인터페이스를 구현하며, Receiver 객체에 대한 참조를 가집니다. execute()가 호출되면, Receiver의 특정 메서드를 호출하여 작업을 수행합니다.
- Receiver (수신자): 요청을 받아서 실제로 작업을 수행하는 객체입니다. (예: Light, Stereo)
- Invoker (호출자): Command 객체를 가지고 있으며, 특정 시점에 Command의 execute() 메서드를 호출하여 요청을 실행합니다. (예: RemoteControlButton)
- Client (클라이언트): Receiver와 ConcreteCommand를 생성하고, 커맨드 객체를 Invoker에 설정하여 둘을 연결합니다.
사용예시
가전제품을 제어하는 간단한 리모컨 예제를 통해 커맨드 패턴을 구현해 보겠습니다.
1. 수신자 (Receiver) 클래스 정의
실제 작업을 수행할 Light 클래스를 정의합니다.
// Receiver 클래스
public class Light {
public void on() { System.out.println("조명이 켜졌습니다."); }
public void off() { System.out.println("조명이 꺼졌습니다."); }
}
2. 커맨드 (Command) 인터페이스 및 구체적인 커맨드 (ConcreteCommand) 구현
execute() 메서드를 가진 Command 인터페이스와, 이를 구현하여 Light를 제어하는 커맨드들을 만듭니다.
// Command 인터페이스
public interface Command {
void execute();
}
// ConcreteCommand A: 조명 켜기
class LightOnCommand implements Command {
private Light light; // Receiver에 대한 참조
public LightOnCommand(Light light) { this.light = light; }
@Override
public void execute() { light.on(); }
}
// ConcreteCommand B: 조명 끄기
class LightOffCommand implements Command {
private Light light; // Receiver에 대한 참조
public LightOffCommand(Light light) { this.light = light; }
@Override
public void execute() { light.off(); }
}
3. 호출자 (Invoker) 클래스 구현
커맨드를 받아 실행하는 리모컨 버튼 역할을 하는 클래스를 정의합니다.
// Invoker 클래스
public class SimpleRemoteControl {
private Command slot; // 하나의 커맨드를 저장할 슬롯
public void setCommand(Command command) { this.slot = command; }
public void buttonWasPressed() { slot.execute(); }
}
4. 클라이언트 (Client) 코드
클라이언트는 모든 객체들을 생성하고 설정하여 연결합니다.
public class RemoteControlTest {
public static void main(String[] args) {
// 1. Invoker 생성
SimpleRemoteControl remote = new SimpleRemoteControl();
// 2. Receiver 생성
Light light = new Light();
// 3. Command 객체들 생성 (Receiver와 연결)
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);
// 4. Invoker에 Command 설정 및 실행
remote.setCommand(lightOn);
remote.buttonWasPressed(); // 조명이 켜졌습니다.
remote.setCommand(lightOff);
remote.buttonWasPressed(); // 조명이 꺼졌습니다.
}
}
결론
이런 경우에 Command 패턴을 사용합니다.
- 요청을 하는 객체와 요청을 처리하는 객체를 분리하고 싶을 때: Invoker는 Command의 execute()만 호출하면 되므로, Receiver가 누구인지, 무슨 일을 하는지 전혀 알 필요가 없습니다.
- 요청을 큐에 저장하거나, 로깅하거나, 되돌리는(Undo/Redo) 기능을 구현하고 싶을 때: 요청이 객체로 캡슐화되어 있으므로, 이 객체들을 스택이나 리스트에 저장하여 관리하기 용이합니다.
- 다양한 요청을 동적으로 설정하고 실행하고 싶을 때: Invoker에 설정되는 Command 객체를 런타임에 교체할 수 있습니다.
다만 간단한 요청 하나를 처리하는 데에도 Command 인터페이스, ConcreteCommand 클래스 등 여러 클래스를 만들어야 하므로 코드 구조가 복잡해질 수 있습니다. 커맨드 패턴은 요청을 객체 지향적으로 다루는 매우 세련된 방법을 제공합니다. 특히 GUI 버튼, 메뉴 항목, 매크로 기록, 트랜잭션 관리 등 다양한 애플리케이션에서 유용하게 사용됩니다. 다음 시간에는 Chain of Responsibility 패턴에 대해서 알아보겠습니다.