[Design Pattern] Command Pattern (명령 패턴)

Command Pattern

Command Pattern은 객체 지향 디자인 패턴 중 하나로, 실행될 기능을 객체화하여 클라이언트와 리시버(수신자) 사이 의존성을 줄이는 패턴이다. 클라이언트는 리시버 객체의 구체적인 구현과 결합하지 않고, 명령 객체를 통해 실행될 기능을 저장할 수 있다.

Command Pattern은 아래와 같은 요소들로 구성되어 있다. 설명만으로는 어려울 수 있으나 예시 코드와 함께 읽어보면 쉽게 이해할 수 있을 것이다.

먼저 명령 인터페이스를 선언한다.

// Command 인터페이스
public interface Command {
    void execute();
}

위에서 선언한 명령 인터페이스는 구체화 될 수 있다. 아래는 ConcreteCommand1, ConcreteCommand2로 인터페이스를 구현한 코드다.

// ConcreteCommand1 클래스
public class ConcreteCommand1 implements Command {
    private Receiver receiver;
    
    public ConcreteCommand1(Receiver receiver) {
        this.receiver = receiver;
    }
    
    public void execute() {
        receiver.action1();
    }
}

// ConcreteCommand2 클래스
public class ConcreteCommand2 implements Command {
    private Receiver receiver;
    
    public ConcreteCommand2(Receiver receiver) {
        this.receiver = receiver;
    }
    
    public void execute() {
        receiver.action2();
    }
}

각 클래스는 Receiver를 인자로 가지고 있다. 즉, 명령 객체는 해당 명령을 수행하는 수행 주체의 정보를 가지고 있다고 생각할 수 있다. 수행 주체는 다양한 명령을 가지고 있지만, 나에게 해당하는 명령만을 수행하도록 작성되어 있다.

명령을 호출하는 객체는 특정한 명령 객체를 설정한 뒤 그 객체에 executeCommand()를 통해 실행 명령을 내린다. 명령 호출 객체는 명령 종류가 얼마나 많든 명령 설정 → 실행 과정만 거치면 되기 때문에 실제 실행 주체로부터 연관성을 떨어트릴 수 있다.

// Invoker 클래스
public class Invoker {
    private Command command;
    
    public void setCommand(Command command) {
        this.command = command;
    }
    
    public void executeCommand() {
        command.execute();
    }
}

명령을 수신하는 객체, 이를테면 리모컨과 같은 객체는 내부에 각 명령들에 대해 어떤 동작을 할 지 미리 정해져있다.

// Receiver 클래스
public class Receiver {
    public void action1() {
        System.out.println("action1 실행");
    }
    
    public void action2() {
        System.out.println("action2 실행");
    }
}

아래는 앞서 살펴본 코드를 실행해보는 Client 코드다.

// Client 클래스
public class Client {
    public static void main(String[] args) {
        // 리시버 객체 생성
        Receiver receiver = new Receiver();
        
        // 명령 객체 생성
        Command command1 = new ConcreteCommand1(receiver);
        Command command2 = new ConcreteCommand2(receiver);
        
        // Invoker 객체 생성
        Invoker invoker = new Invoker();
        
        // 명령 객체 설정
        invoker.setCommand(command1);
        invoker.executeCommand(); // action1 실행
        
        invoker.setCommand(command2);
        invoker.executeCommand(); // action2 실행
    }
}

Undo / Redo

명령을 객체로 나타낼 수 있다는 개념을 조금만 생각해보면, 명령을 저장했다가 다시 복구하는 것도 실행할 수 있다는 개념도 가능하다. 이는 특정 작업에 대한 반대 작업을 기록해 놓거나, 특정 작업 후 상태를 저장하여 기록할 수 있다.

가령 아래와 같이 덧셈을 하는 명령에 대해, 반대 급부로 뺄셈을 하는 명령을 나타냈다고 가정하자.

// AddCommand 클래스
public class AddCommand implements Command {
    private Receiver receiver;
    private int value;
    
    public AddCommand(Receiver receiver, int value) {
        this.receiver = receiver;
        this.value = value;
    }
    
    public void execute() {
        receiver.addValue(value);
    }
    
    public void undo() {
        receiver.subValue(value);
    }
}

아래는 실질적으로 명령을 수행하는 Invoker 클래스에서 Stack 자료구조를 이용해 이전에 사용했던 명령들 목록을 저장하는 방법을 보여준다.

// Invoker 클래스
public class Invoker {
    private Stack<Command> undoStack = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();
    
    public void executeCommand(Command command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();
    }
    
    public void undoCommand() {
        if (!undoStack.empty()) {
            Command command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        }
    }
    
    public void redoCommand() {
        if (!redoStack.empty()) {
            Command command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        }
    }
}

이렇게 템플릿 코드가 작성되었으면, 명령을 수행하는 수신자에 구체적인 동작 방법을 구현해준다. 아래는 덧셈과 뺄셈에 대한 구현이다.

// Receiver 클래스
public class Receiver {
    private int value = 0;
    
    public void addValue(int value) {
        this.value += value;
        System.out.println("현재 값: " + this.value);
    }
    
    public void subValue(int value) {
        this.value -= value;
        System.out.println("현재 값: " + this.value);
    }
}

실행하는 Invoker는 명령이 어떻게 동작하는지와는 관계 없이, undo() 명령어를 수행하면 이전과 같은 형태로 돌아가게 된다.

// Invoker 클래스
public class Invoker {
    private Stack<Command> undoStack = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();
    
    public void executeCommand(Command command) {
        command.execute();
        undoStack.push(command);
        redoStack.clear();
    }
    
    public void undoCommand() {
        if (!undoStack.empty()) {
            Command command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        }
    }
    
    public void redoCommand() {
        if (!redoStack.empty()) {
            Command command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        }
    }
}

위 코드를 참고해 작성한 테스트 코드는 아래와 같다.

// Client 클래스
public class Client {
    public static void main(String[] args) {
        // 리시버 객체 생성
        Receiver receiver = new Receiver();
        
        // Invoker 객체 생성
        Invoker invoker = new Invoker();
        
        // AddCommand 객체 생성
        Command command1 = new AddCommand(receiver, 10);
        Command command2 = new AddCommand(receiver, 5);
        
        // 명령 객체 실행
        invoker.executeCommand(command1); // 현재 값: 10
        invoker.executeCommand(command2); // 현재 값: 15
        
        // undo 실행
        invoker.undoCommand(); // 현재 값: 10
        
        // redo 실행
        invoker.redoCommand(); // 현재 값: 15
    }
}

Updated:

Leave a comment