행동 관련 : 커맨드 패턴

    들어가기 전

    이 글은 인프런 백기선님의 GOF 강의를 복습하며 작성한 글입니다. 

     


    커맨드 패턴

    • GOF : 요청을 캡슐화 하여 호출자(Invoker)와 수신자(Receiver)를 분리하는 패턴
      • 요청을 처리하는 방법이 바뀌거나 추가되더라도 호출자의 코드는 변경되지 않는다. 
      • 자주 변경되지 않는 부분 : 호출자
      • 자주 변경되는 부분 : 요청 처리 부분
    • Invoker(호출자)와 Receiver(수신자)를 디커플링 시키는 패턴이다. 디커플링의 수단으로 Command 객체를 활용한다.
      • Invoker는 Command Interface에 의존. 
      • Command Interface는 Receiver를 호출. 
      • 새로운 Receiver나 기능이 추가될 때 마다 Concrete Command가 추가됨. 
    • Component
      • Invoker : 요청자. Command 인터페이스를 받고 execute()만 실행함.
      • Comamnd : Invoker가 사용할 수 있도록 인터페이스를 제공함.
      • ConcreteCommand : 구체적인 Receiver의 메서드를 호출함. Receiver가 누구인지, Operation이 무엇인지 등을 결정함. 
      • Receiver : 실제로 요청을 처리하는 클래스 

     

     


    패턴이 필요한 코드

    public class Button {
        private Light light;
        public Button(Light light) { this.light = light; }
    
        public void press() {
            light.off();
            // light.on()
        }
    
        public static void main(String[] args) {
            Button button = new Button(new Light());
            button.press();
            button.press();
            button.press();
            button.press();
        }
    }

    버튼 클래스를 이용해 Light 클래스의 불을 끄고 키는 기능이 있다고 가정해보자. 이 때 이런 문제점이 존재한다.

    • 현재 press() 메서드만 존재하는데, Light에서 불을 키고 끄기 위해 press() 메서드를 수정해야 함. 
    • 혹은 Light의 On, Off를 위해 Button 클래스에 on(), off() 메서드를 추가해야함. 

    둘다 문제가 있다. 첫번째는 쓸 데 없이 계속 코드를 수정해야하는 점이다. 두번째는 Light - Button이 강하게 결합되어 있어서 Light에서 제공하는 인터페이스에 대응되는 메서드를 Button 클래스에서 생성해야 한다는 점이다. 

    만약 두번째처럼 구현하기로 결정했을 때, Light에 On / Off 기능보다 더 세분화 되어 1~5 단계로 밝기를 조절하는 기능이 추가되는 것을 상상해보자. 그렇다면 그에 대응되는 메서드가 Button에 추가되어야 할 것이다. Light에 새로운 기능이 추가될 때 마다 Button 클래스도 영향을 받기 때문에 코드 확장에 문제가 발생할 것이다. 

     


    디자인 패턴 적용하기

    Button은 Invoker(호출자), Light는 Receiver(수신 및 처리자)로 볼 수 있다. 이 때, Command 클래스를 중간에 추가해서 Button - Receiver의 결합을 줄일 수 있다. 

    1. Command 인터페이스를 제공한다.
    2. Command 인터페이스를 구현한 LightOffCommand, LightOnCommand를 구현한다. 
    3. Button은 Command의 execute()만 실행하도록 한다. 
    4. Command 구현체는 Receiver에 대한 정보를 모두 가진다. (어떤 Receiver 호출해야할지, 무슨 메서드 호출해야 할지) 
    public interface Command {
        void execute();
    
        void undo();
    }
    

    Command 인터페이스를 제공한다. 

    public class LightOffCommand implements Command {
    
        private final Light light;
    
        public LightOffCommand(Light light) { this.light = light; }
    
        @Override
        public void execute() { this.light.off(); }
    
        @Override
        public void undo() { new LightOnCommand(this.light).execute(); }
    }

    ConcreteCommand 클래스는 다음 기능을 추가한다.

    1. 어떤 Receiver를 호출해야할지 
    2. Receiver의 어떤 인터페이스를 호출해야할지.
    3. Receiver를 호출할 때 어떤 파라메터를 전달해야할지 

    등을 전달해주면 된다. 

    public class Button {
    
        private Stack<Command> commands = new Stack<>();
    
        public void press(Command command) {
            this.commands.add(command);
            command.execute();
        }
    
        public void undo() {
            Command command = this.commands.pop();
            command.undo();
        }
    
    
        public static void main(String[] args) {
            Button button = new Button();
            button.press(new LightOnCommand(new Light()));
            button.undo();
    
            button.press(new GameStartCommand(new Game()));
            button.undo();
        }
    }

    Button 클래스는 다음과 같이 작성할 수 있다. Invoker는 단순히 Command를 Execute 하기만 하면 된다.

    1. Command 인터페이스에만 의존한다.
    2. Command 인터페이스의 execute() 메서드만 실행한다. 

    이 두 가지 덕분에 Button(Invoker) - Light(Receiver)는 디커플링된다. 

     

    조삼모사가 아닌가? 라고 생각할 수 있다. 왜냐하면 이런 관점이 있기 때문이다.

    • Button 코드가 바뀌면 Command쪽 코드가 바뀌는 것 아닌가? 

    그것은 맞는 말인데, 기본적으로 Command 패턴에서 Invoker는 최대한 변화가 없는 역할로 본다. 주로 Receiver에 새로운 기능이 많이 추가되는 것을 가정했을 때 사용하는 디자인 패턴이다. 이런 경우라면 Receiver에 새로운 기능이 추가되더라도 ConcreteCommand만 추가되고 Invoker는 영향을 받지 않는다는 것에 주목해야 한다.

     


    Command 패턴의 장점 / 단점

    • 장점
      • 기존 코드를 변경하지 않고 새로운 커맨드를 만들 수 있음.  (OCP)
      • Receiver(수신자)의 코드가 변경되어도 Invoker(호출자)의 코드는 변경되지 않음.
      • Command 객체를 로깅, DB에 저장, 네트워크로 전송하는 등 다양한 방법으로 활용할 수도 있음.
    • 단점
      • 코드가 복잡하고 클래스가 많아진다. 

    예를 들어 커맨드에 undo() 기능을 추가해 볼 수도 있다. 예를 들어 GameEndCommand의 Undo는 Game을 시작하는 거니 new GameStart(this).execute() 같은 식으로도 처리해볼 수 있는데, 이를 통해 Command 객체를 다양한 형태로 확장해 볼 수 있다는 것이다.

    댓글

    Designed by JB FACTORY