[design pattern] 커맨드 패턴이란 References

Goal

  • 커맨드 패턴의 개념을 이해한다.
  • 예시를 통해 커맨드 패턴을 이해한다.

커맨드 패턴이란

  • 실행될 기능을 캡슐화함으로써 주어진 여러 기능을 실행할 수 있는 재사용성이 높은 클래스를 설계하는 패턴
    • 즉, 이벤트가 발생했을 때 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할 때 유용하다.
    • ‘행위(Behavioral) 패턴’의 하나 (아래 참고)
  • 실행될 기능을 캡슐화함으로써 기능의 실행을 요구하는 호출자(Invoker) 클래스와 실제 기능을 실행하는 수신자(Receiver) 클래스 사이의 의존성을 제거한다.
  • 따라서 실행될 기능의 변경에도 호출자 클래스를 수정 없이 그대로 사용 할 수 있도록 해준다.
  • 역할이 수행하는 작업
    • Command
      • 실행될 기능에 대한 인터페이스
      • 실행될 기능을 execute 메서드로 선언함
    • ConcreteCommand
      • 실제로 실행되는 기능을 구현
      • 즉, Command라는 인터페이스를 구현함
    • Invoker
      • 기능의 실행을 요청하는 호출자 클래스
    • Receiver
      • ConcreteCommand에서 execute 메서드를 구현할 때 필요한 클래스
      • 즉, ConcreteCommand의 기능을 실행하기 위해 사용하는 수신자 클래스

참고

  • 행위(Behavioral) 패턴
    • 객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴
    • 한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 또 그렇게 하면서도 객체 사이의 결합도를 최소화하는 것에 중점을 둔다.

예시

만능 버튼 만들기

  • 버튼이 눌리면 램프의 불이 켜지는 프로그램
    public class Lamp {
    public void turnOn(){ System.out.println("Lamp On"); }
    }
    public class Button {
    private Lamp theLamp;
    public Button(Lamp theLamp) { this.theLamp = theLamp; }
    public void pressed() { theLamp.turnOn(); }
    }
    
    public class Client {
    public static void main(String[] args) {
      Lamp lamp = new Lamp();
      Button lampButton = new Button(lamp);
      lampButton.pressed();
    }
    }
    
  • Button 클래스의 생성자를 이용해 불을 켤 Lamp 객체를 전달한다.
  • Buttom 클래스의 pressed()가 호출되면 생성자를 통해 전달받은 Lamp 객체의 turnOn()를 호출해 불을 켠다.

문제점

  1. 버튼을 눌렀을 때 다른 기능을 실행하는 경우
    • 버튼을 눌렀을 때 알람이 시작되게 하려면?
      public class Alarm {
        public void start(){ System.out.println("Alarming"); }
      }
      public class Button {
        private Alarm theAlarm;
        public Button(Alarm theAlarm) { this.theAlarm = theAlarm; }
        public void pressed() { theAlarm.start(); }
      }
      
      public class Client {
        public static void main(String[] args) {
       Alarm alarm = new Alarm();
       Button alarmButton = new Button(alarm);
       alarmButton.pressed();
        }
      }
      
    • 새로운 기능으로 변경하려고 기존 코드(Button 클래스)의 내용을 수정해야 하므로 OCP에 위배된다.
    • Button 클래스의 pressed() 전체를 변경해야 한다.
  2. 버튼을 누르는 동작에 따라 다른 기능을 실행하는 경우
    • 버튼을 처음 눌렀을 때는 램프를 켜고, 두 번째 눌렀을 때는 알람을 동작하게 하려면?
      enum Mode { LAMP, ALARM };
      // Button 클래스의 코드를 수정
      public class Button {
        private Lamp theLamp;
        private Alarm theAlarm;
        private Mode theMode;
        // 생성자에서 버튼을 눌렀을 때 필요한 기능을 인지로 받는다.
        public Button(Lamp theLamp, Alarm theAlarm) {
         this.theLamp = theLamp;
         this.theAlarm = theAlarm;
        }
        // 램프 모드 또는 알람 모드를 설정
        public void setMode(Mode mode) { this.theMode = mode; }
        // 설정된 모드에 따라 램프를 켜거나 알람을 울림
        public void pressed() {
         switch(theMode) {
         case LAMP: theLamp.turnOn(); break;
         case ALARM: theAlarm.start(); break;
         }
       }
      }
      
    • 필요한 기능을 새로 추가할 때마다 Button 클래스의 코드를 수정해야 하므로 재사용하기 어렵다.

해결책

문제를 해결하기 위해서는 구체적인 기능을 직접 구현하는 대신 실행될 기능을 캡슐화해야 한다.

  • 즉, Button 클래스의 pressed 메서드에서 구체적인 기능(램프 켜기, 알람 동작 등)을 직접 구현하는 대신 버튼을 눌렀을 때 실행될 기능을 Button 클래스 외부에서 제공받아 캡슐화해 pressed 메서드에서 호출한다.
  • 이를 통해 Button 클래스 코드를 수정하지 않고 그대로 사용할 수 있다.
    • Button 클래스는 미리 약속된 Command 인터페이스의 execute 메서드를 호출한다.
      • 램프를 켜는 경우에는 theLamp.turnOn 메서드를 호출하고
      • 알람이 동작하는 경우에는 theAlarm.start 메서드를 호출하도록 pressed 메서드를 수정한다.
    • LampOnCommand 클래스에서는 Command 인터페이스의 execute 메서드를 구현해 Lamp 클래스의 turnOn 메서드(램프 켜는 기능)를 호출한다.
    • 마찬가지로 AlarmStartCommand 클래스는 Command 인터페이스의 execute 메서드를 구현해 Alarm 클래스의 start 메서드(알람이 울리는 기능)를 호출한다.
  • Command 클래스
    public interface Command { public abstract void execute(); }
    
  • Button 클래스
    public class Button {
    private Command theCommand;
    // 생성자에서 버튼을 눌렀을 때 필요한 기능을 인지로 받는다.
    public Button(Command theCommand) { setCommand(theCommand); }
    public void setCommand(Command newCommand) { this.theCommand = newCommand; }
    // 버튼이 눌리면 주어진 Command의 execute 메서드를 호출한다.
    public void pressed() { theCommand.execute(); }
    
  • Lamp, LampOnCommand 클래스
    public class Lamp {
    public void turnOn(){ System.out.println("Lamp On"); }
    }
    /* 램프를 켜는 LampOnCommand 클래스 */
    public class LampOnCommand implements Command {
    private Lamp theLamp;
    public LampOnCommand(Lamp theLamp) { this.theLamp = theLamp; }
    // Command 인터페이스의 execute 메서드
    public void execute() { theLamp.turnOn(); }
    }
    
  • Alarm, AlarmStartCommand 클래스
    public class Alarm {
    public void start(){ System.out.println("Alarming"); }
    }
    /* 알람을 울리는 AlarmStartCommand 클래스 */
    public class AlarmStartCommand implements Command {
    private Alarm theAlarm;
    public AlarmStartCommand(Alarm theAlarm) { this.theAlarm = theAlarm; }
    // Command 인터페이스의 execute 메서드
    public void execute() { theAlarm.start(); }
    }
    
  • 클라이언트에서의 사용
    public class Client {
    public static void main(String[] args) {
      Lamp lamp = new Lamp();
      Command lampOnCommand = new LampOnCommand(lamp);
      Alarm alarm = new Alarm();
      Command alarmStartCommand = new AlarmStartCommand(alarm);
    
      Button button1 = new Button(lampOnCommand); // 램프 켜는 Command 설정
      button1.pressed(); // 램프 켜는 기능 수행
    
      Button button2 = new Button(alarmStartCommand); // 알람 울리는 Command 설정
      button2.pressed(); // 알람 울리는 기능 수행
      button2.setCommand(lampOnCommand); // 다시 램프 켜는 Command로 설정
      button2.pressed(); // 램프 켜는 기능 수행
    }
    }
    
  • Command 인터페이스를 구현하는 LampOnCommand와 AlarmStartCommand 객체를 Button 객체에 설정한다.
  • Button 클래스의 pressed 메서드에서 Command 인터페이스의 execute 메서드를 호출한다.
  • 즉, 버튼을 눌렀을 때 필요한 임의의 기능은 Command 인터페이스를 구현한 클래스의 객체를 Button 객체에 설정해서 실행할 수 있다.
  • 이렇게 Command 패턴을 이용하면 Button 클래스의 코드를 변경하지 않으면서 다양한 동작을 구현할 수 있게 된다.

References