정보보안-이론/XX에 대하여

[디자인 패턴] (GoF 행위패턴) Command Pattern에 대하여

리덕토 2025. 7. 9. 14:43

안녕하세요! GoF 디자인 패턴 연재, 이번 시간부터는 객체 간의 상호작용 및 책임 분배에 관한 **행동 패턴(Behavioral Patterns)**에 대해 알아봅니다. 그 첫 번째 주자는 커맨드(Command) 패턴입니다!


정의

커맨드 패턴요청(Request) 그 자체를 하나의 객체로 캡슐화하는 패턴입니다. 이렇게 캡슐화된 요청(커맨드 객체)은 매개변수로 전달되거나, 큐에 저장되거나, 로그로 기록되는 등 다양한 방식으로 활용될 수 있으며, 요청을 보내는 객체(Invoker)와 요청을 실제로 처리하는 객체(Receiver)를 분리할 수 있습니다.

 

레스토랑 주문 과정을 생각하면 쉽습니다.

  • 손님(Client)은 메뉴판을 보고 '스테이크'를 주문합니다.
  • 웨이터(Invoker)는 주문 내용('스테이크 주문서'라는 Command 객체)을 받습니다. 웨이터는 스테이크를 어떻게 요리하는지 모릅니다. 단지 주문서를 주방에 전달할 뿐입니다.
  • 주방장(Receiver)은 주문서를 보고 실제로 스테이크를 요리합니다.

이 과정에서 '주문서(Command)'라는 객체 덕분에 손님과 주방장은 완전히 분리됩니다. 웨이터는 주문서를 큐에 쌓아두었다가 순서대로 처리하거나, 특정 주문을 취소하는 등의 작업을 할 수 있습니다. 이처럼 요청을 객체로 다루는 것이 커맨드 패턴의 핵심입니다.

 

커맨드 패턴은 다음과 같은 구조

  • Command (커맨드): 모든 커맨드 객체들이 구현해야 할 공통 인터페이스입니다. 보통 execute()라는 단일 메서드를 가집니다.
  • ConcreteCommand (구체적인 커맨드): Command 인터페이스를 구현하며, Receiver 객체에 대한 참조를 가집니다. execute()가 호출되면, Receiver의 특정 메서드를 호출하여 작업을 수행합니다.
  • Receiver (수신자): 요청을 받아서 실제로 작업을 수행하는 객체입니다. (예: Light, Stereo)
  • Invoker (호출자): Command 객체를 가지고 있으며, 특정 시점에 Commandexecute() 메서드를 호출하여 요청을 실행합니다. (예: RemoteControlButton)
  • Client (클라이언트): ReceiverConcreteCommand를 생성하고, 커맨드 객체를 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 패턴을 사용합니다.

 

  • 요청을 하는 객체와 요청을 처리하는 객체를 분리하고 싶을 때: InvokerCommandexecute()만 호출하면 되므로, Receiver가 누구인지, 무슨 일을 하는지 전혀 알 필요가 없습니다.
  • 요청을 큐에 저장하거나, 로깅하거나, 되돌리는(Undo/Redo) 기능을 구현하고 싶을 때: 요청이 객체로 캡슐화되어 있으므로, 이 객체들을 스택이나 리스트에 저장하여 관리하기 용이합니다.
  • 다양한 요청을 동적으로 설정하고 실행하고 싶을 때: Invoker에 설정되는 Command 객체를 런타임에 교체할 수 있습니다.

다만 간단한 요청 하나를 처리하는 데에도 Command 인터페이스, ConcreteCommand 클래스 등 여러 클래스를 만들어야 하므로 코드 구조가 복잡해질 수 있습니다. 커맨드 패턴은 요청을 객체 지향적으로 다루는 매우 세련된 방법을 제공합니다. 특히 GUI 버튼, 메뉴 항목, 매크로 기록, 트랜잭션 관리 등 다양한 애플리케이션에서 유용하게 사용됩니다. 다음 시간에는 Chain of Responsibility 패턴에 대해서 알아보겠습니다.