헤드퍼스트 디자인패턴 Chapter6

헤드퍼스트 디자인패턴

  • 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.

Chapter 6. 호출 캡슐화하기 - 커맨드 패턴

예시 - IoT 리모컨

리모컨 세부 사항

  • 프로그래밍이 가능한 7개의 슬롯 버튼
  • 7개의 슬롯 각각에 대한 ON/OFF 버튼
  • 누른 버튼의 명령을 취소하는 UNDO 버튼

제어해야 하는 호출

  • 리모컨은 다양한 기기를 제어한다.
  • 각 기기들에 대한 객체의 인터페이스들은 공통적인 인터페이스가 있는 것처럼 보이지 않는다.
    • 어떤 기기는 다른 기기들과 인터페이스가 많이 다른 점

커맨드 패턴 소개

음식 주문 과정

  1. 고객이 종업원에게 음식을 주문한다.
    • createOrder()
    • 주문(Order)은 주문서와 그 위에 적혀있는 주문 내용으로 구성된다.
    • 즉 Order는 주문 내용을 캡슐화한다.
  2. 종업원은 주문을 받고, 카운터에 주문을 전달한다.
    • takeOrder()
    • orderUp(): 주문 처리를 준비하는 메서드
  3. 주방에서 주문대로 음식을 준비한다.
    • makeBurger(), makeCoke()
    • Order 객체에는 음식 준비를 하기 위해 필요한 모든 지시 사항이 들어있다.
    • Order 객체가 주방장에게 makeBurger()와 같은 메서드 호출로 행동을 지시한다.

커맨드 패턴으로 살펴보기

  1. 클라이언트
    • 클라이언트는 커맨드 객체를 생성해야 한다.
    • createCommandObject()
      
  2. 인보커
    • 클라이언트는 인보커 객체의 setCommand() 메서드를 호출하면서 커맨드 객체를 넘겨준다.
    • 그 커맨드 객체는 쓰이기 전까지 인보커 객체에 보관된다.
  3. 커맨드
    • 커맨드 객체에는 행동과 리시버의 정보가 들어있다.
    • 커맨드 객체에서는 execute() 메서드 하나만 제공한다.
    • execute() 메서드는 행동을 캡슐화하여 리시버에 있는 특정 행동을 처리한다.
    • public void execute() {
          receiver.action1();
          receiver.action2();
      }
      
  4. 리시버
    • 인보커에서 커맨드 객체의 execute() 메서드를 호출하면 리시버에 있는 행동 메서드가 호출된다.
    • action1(), action2()

IoT 리모컨의 첫 번째 커맨드 객체 만들기

  • 커맨드 인터페이스 구현
    • 커맨드 객체는 모두 같은 인터페이스를 구현해야 한다.
      public interface Command {
      public void execute();
      }
      
  • 조명을 켜는 행동에 대한 커맨드 클래스
    • Light 클래스에는 on()off() 2개의 메서드가 있다.
      public class LightOnCommand implements Command {
      Light light;
          
      pubic LightOnCommand(Light light) {
          this.light = light;
      }
          
      public void execute() {
          light.on();
      }
      }
      
  • 커맨드 객체 사용하기
    • 우선 기기를 연결할 슬롯과 버튼이 하나씩 있는 리모컨으로 가정하고 사용해본다.
public class SimpleRemoteController {
    Command slot;
    public SimpleRemoteController() {}
   
   public void setCommand(Command command) {
        slot = command;
   }
   
   public void buttonWasPressed() {
        slot.execute();
   }
}

커맨드 패턴의 정의

  • 커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화하여 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다.
    • 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
  • 커맨드 객체는 일련의 행동을 특정 리시버와 연결함으로써 요청을 캡슐화한다.
  • 행동과 리시버를 한 객체에 넣고, execute() 메서드 하나만 외부에 공개하여, 해당 메서드 호출에 따라 리시버에서 일련의 작업을 처리한다.

리모컨 슬롯에 명령 할당하기

  • 즉 리모컨이 인보커가 되도록 만드는 것
    • 슬롯에 해당하는 버튼을 누르면 그에 맞는 커맨드 객체의 execute() 메서드가 호출되고 리시버에서 특정 행동을 담당하는 메서드가 실행됨

리모컨 코드 만들기

public class RemoteControl {
    Command[] onCommands;
    Command[] offCommands;
    
    public RemoteControl() {
        // 이 리모컨은 7개의 ON/OFF 명령을 처리할 수 있다
        onCommands = new Command[7];
        offCommands = new Command[7];
        
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
    }
    
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }
    
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
    }
    
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
    }
    
    // 리모컨 슬롯 테스트용 메서드
    public String toString() {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("\n---- 리모컨 ----\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuffer.append("[slot " + i + "] " + onCommands[i].getClass().getName()
                + "   " + offCommands[i].getClass().getName() + "\n");
        }
        return stringBuffer.toString();
    }
}

커맨드 클래스 만들기

  • 아까 전의 LightOnCommand 보다 조금 더 복잡한 오디오를 켜고 끄는 커맨드 클래스
    public class StereoOnWithCDCommand implements Command {
      Stereo stereo;
        
      public StereoOnWithCDCommand(Stereo stereo) {
          this.stereo = stereo;
      }
        
      public void execute() {
          stereo.on();
          stereo.setCD();
          stereo.setVolume(1);
      }
    }
    

작업 취소 기능 추가하기

  • 커맨드 객체에서 작업 취소 기능을 만드려면 execute()와 비슷한 undo() 메서드를 만들어야 한다.
  • undo() 메서드에서 execute()에서 한 동작과 정반대의 작업을 처리하면 된다.

조명을 켜는 행동에 대한 커맨드 클래스에서의 취소 기능

public class LightOnCommand implements Command {
    Light light;
    
    public LightOnCommand(Light light) {
        this.light = light;
    }
    
    public void execute() {
        light.on();
    }
    
    public void undo() {
        light.off();
    }
}
  • 하지만 리모컨의 취소 버튼은 사용자가 마지막으로 누른 버튼을 기록하고, 취소 버튼을 눌렀을 때 필요한 UNDO 작업을 해줘야 한다.
public class RemoteControlWithUndo {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;
    
    public RemoteControlWithUndo() {
        onCommands = new Command[7];
        offCommands = new Command[7];
        
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }
    
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }
    
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
        undoCommand = onCommands[slot];     // 사용자가 버튼을 누르면 해당 커맨드 객체의 레퍼런스를 undoCommand 인스턴스 변수에 저장
    }
    
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }
    
    public void undoButtonWasPushed() {
        undoCommand.undo();
    }
    
    public String toString() {
        // toString 코드
    }
}

상태를 사용하는 방법

  • 예를들면 선풍기는 속도를 선택할 수 있다.
    public class CeilingFan {
      public static final int HIGH = 3;
      public static final int MEDIUM = 2;
      public static final int LOW = 1;
      public static final int OFF = 0;
      String location;
      int speed;
        
      public CeilingFan(String location) {
          this.location = location;
          speed = OFF;
      }
        
      public void high() {
          speed = HIGH;
      }
        
      public void medium() {
          speed = MEDIUM;
      }
        
      public void low() {
          speed = LOW;
      }
        
      public void off() {
          speed = OFF;
      }
        
      public int getSpeed() {
          return speed;
      }
    }
    

선풍기 명령어에 취소 기능 추가하기

  • 취소 기능을 추가하기 위해 선풍기의 이전 속도를 저장해두었다가 undo() 메서드가 호출되면 이전 속도로 되돌아갈 수 있도록 디자인한다.
    public class CeilingFanHighCommand implements Command {
      CeilingFan ceilingFan;
      int prevSpeed;
        
      public CeilingFanHighCommand(CeilingFan ceilingFan) {
          this.ceilingFan = ceilingFan;
      }
        
      @Override
      public void execute() {
          prevSpeed = ceilingFan.getSpeed();
          ceilingFan.high();
      }
        
      @Override
      public void undo() {
          if (prevSpeed == CeilingFan.HIGH) {
              ceilingFan.high();
          } else if (prevSpeed == CeilingFan.MEDIUM) {
              ceilingFan.medium();
          } else if (prevSpeed == CeilingFan.LOW) {
              ceilingFan.low();
          } else if (prevSpeed == CeilingFan.OFF) {
              ceilingFan.off();
          }
      }
    }
    

여러 동작 한 번에 처리하기

매크로 커맨드 사용하기

public class MacroCommand implements Command {
    Command[] commands;
    
    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }
    
    public void execute() {
        for (int i = 0; i < commands.length; i++) {
            commands[i].execute();
        }
    }
    
    public void undo() {
        for (int i = commands.length - 1; i >= 0; i--) {
            commands[i].undo();
        }
    }
}

매크로 커맨드 사용하기

  1. 먼저 매크로에 넣을 일련의 커맨드를 만든다.
Light light = new Light("Living Room");
TV tv = new TV("Living Room");
Stereo stereo = new Stereo("Living Room");
Hottub hottub = new Hottub();

LightOnCommand lightOn = new LightOnCommand(light);
TVOnCommand tvOn = new TVOnCommand(tv);
StereoOnCommand stereoOn = new StereoOnCommand(stereo);
HottubOnCommand hottubOn = new HottubOnCommand(hottub);

LightOffCommand lightOff = new LightOffCommand(light);
TVOffCommand tvOff = new TVOffCommand(tv);
StereoOffCommand stereoOff = new StereoOffCommand(stereo);
HottubOffCommand hottubOff = new HottubOffCommand(hottub);
  1. ON 커맨드와 OFF 커맨드용 배열을 각각 만들고 필요한 커맨드를 넣는다.
Command[] allOn = { lightOn, tvOn, stereoOn, hottubOn };
Command[] allOff = { lightOff, tvOff, stereoOff, hottubOff };

MacroCommand macroOn = new MacroCommand(allOn);
MacroCommand macroOff = new MacroComand(allOff);
  1. MacroCommand 객체를 버튼에 할당
remoteControl.setCommand(0, macroOn, macroOff);

커맨드 패턴 더 활용하기

  • 리시버와 일련의 행동을 패키지로 묶어 일급 객체 형태로 전달할 수 있다.
    • 이러면 클라이언트 애플리케이션에서 커맨드 객체를 생성한 뒤 오랜 시간이 지나도 그 리시버와 일련의 행동을 호출할 수 있다.
    • 이 점을 활용해 커맨드 패턴을 스케줄러나 스레드 풀, 작업 큐와 같은 다양한 작업에 적용할 수 있다.
      • 예) 작업 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로 execute() 메서드가 호출된다.
  • 어떤 애플리케이션은 모든 행동을 기록해 두었다가 애플리케이션이 다운되었을 때 그 행동을 다시 복구할 수 있어야 한다.
    • 커맨드 패턴으로 store()load() 메서드를 추가해 이런 기능을 구현할 수 있다.
      • 각 커맨드가 실행될 때마다 디스크에 그 내역을 저장한다.
      • 시스템이 다운된 후에, 객체를 다시 로딩해서 순서대로 작업을 다시 처리한다.