suyeonme

[디자인 패턴] 커맨드패턴(Command Pattern) 본문

프로그래밍👩🏻‍💻/디자인 패턴

[디자인 패턴] 커맨드패턴(Command Pattern)

suyeonme 2022. 7. 10. 22:33
헤드 퍼스트 디자인 패턴을 읽고 정리한 내용입니다.

 

1. 커맨드패턴(Command Pattern)이란?


https://cdn-images-1.medium.com/fit/t/1600/480/1*xQ2peVm9CmcIgYY2UyyBYg.png

요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러한 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.

 

커맨드 패턴 활용 예시

1. 스케줄러, 스레드 풀, 작업 큐와 같은 작업에 사용할 수 있다.

 

작업 큐를 예시로 들면, 큐 한쪽 끝은 커맨드를 추가할 수 있고, 다른 쪽 끝에는 커맨드를 처리하는 스레드들이 대기하고 있다. 각 스레드는 우선 execute()를 호출하고 호출이 완료되면 커맨드 객체를 버리고 새로운 커맨드 객체를 가져온다. 작업 큐 클래스는 계산 작업을 하는 객체들과 완전히 분리돠어 있다. 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로 execute()가 호출된다.

 

2. 로그 기록을 남기고 애플리케이션이 다운되었을 때 최근 수행된 작업을 복구할 수 있다. 

 

커맨드 패턴을 사용하여 store()와 load()를 추가해서 이러한 기능을 구현할 수 있다. 로그 기록은 어떤 명령을 실행하면서 디스크에 실행 히스토리를 기록하고, 애플리케이션이 다운되면 커맨드 객체를 다시 로딩하여 execute()를 자동으로 순서대로 실행하는 방식으로 작동한다. 

 

동작 순서

  1. 클라이언트는 커맨드 객체를 생성한다. 커맨드 객체는 Receiver 객체에 전달할 일련의 행동으로 구성된다.
  2. 커맨드 객체에는 행동과 리시버(Receiver)의 정보가 들어있다. 커맨드 객체에서 제공하는 메소드는 execute() 하나뿐이다. 이 메소드는 행동을 캡슐화하며 리시버에 있는 특정 행동을 처리한다.
  3. 클라이언트는 Invoker 객체의 setCommand()를 호출하는데, 이 때 커맨드 객체를 넘겨준다. 
  4. Invoker 객체에서 커맨드 객체의 execute()를 호출한다.
  5. Receiver 객체에 있는 행동 메소드가 호출된다.

 

1. 커맨드 객체 생성

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

2. 커맨드 클래스의 구현 클래스 생성

// 조명을 켤 때 필요한 커맨드 클래스
public class LightOnCommand implements Command {
  Light light;
  
  public LightOnCommand(Light light) {
    this.light = light;
  }
  
  public void execute() {
    light.on();
  }
  
  public void undo() {
    light.off();
  }
}

// 조명을 끌 때 필요한 커맨드 클래스
public class LightOffCommand implements Command {
  Light light;
  
  public LightOnCommand(Light light) {
    this.light = light;
  }
  
  public void execute() {
    light.off();
  }
  
  public void undo() {
    light.on();
  }
}

// 오디오를 켜고 끌 때 필요한 커맨드 클래스
public class StereoOnWithCDCommand implements Command {
  Stereo stereo;
  
  public StereoOnWithCDCommand(Stereo stereo) {
    this.stereo = stereo;
  }
  
  public void execute() {
    stereo.on();
    stereo.setCD();
    stereo.setVolumn(1);
  }
  
  public void undo() {
    stereo.off();
    stereo.offCD();
    stereo.setVolumn(0);
  }
}

3. 커맨드 객체를 사용할 Invoker객체 생성 

public class RemoteControl {
  Command[] onCommands;
  Command[] offCommands;
  Command undoCommand;
  
  public RemoteControl() {
    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];
  }
  
  public void offButtonWasPushed(int slot) {
    offCommands[slot].execute();
    undoCommand = offCommands[slot];
  }
  
  public void undoButtonWasPushed() {
    undoCommand.undo();
  }
}

3.1 널 객체(Null Object) 생성

Null 객체는 리턴할 객체가 없고 클라이언트가 null을 처리하지 않게 하고싶을 때 활용한다. 

public class NoCommand implements Command {
  public void execute() {}
}

// 널객체를 사용하지 않는 경우, null 체크를 해줘야한다.
public void onButtonWasPushed(int slot) {
  if(onCommans[slot] != null) {
    onCommans[slot].execute;
  }
}

// 널객체를 사용하여 초기화 
Command noCommand = new NoCommand();
for(int i = 0; i < 7; i++) {
  onCommands[i] = noCommand;
  offCommands[i] = noCommand;
}

4. 리모콘 코드 테스트 

public class RemoteLoader {
  public static void main(String[] args) {
    RemoteControl remoteControl = new RemoteControl();
    
    // 장치 생성 (Receiver)
    Light livingRoomLight = new Light("Living Room");
    GarageDoor garageDoor = new GarageDoor("Garage");
    CeilingFan ceilingFan = new CeilingFan("Living Room");
    
    
    // 조명용 커맨드 객체
    LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
    LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
    
    GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);
    GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);
    
    CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
    CeilingFanOffCommand ceilingFanOff = new CeilingFanoffCommand(ceilingFan);
    
    // 커맨드 로드
    remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
    remoteControl.setCommand(0, garageDoorUp, garageDoorDown);
    remoteControl.setCommand(0, ceilingFanOn, ceilingFanOff);
    
    // 슬롯 키고 끄기
    remoteControl.onButtonWasPushed(0);
    remoteControl.offButtonWasPushed(0);
    remoteControl.undoButtonWasPushed();
    
    remoteControl.onButtonWasPushed(1);
    remoteControl.offButtonWasPushed(1);
    remoteControl.undoButtonWasPushed();
    
    remoteControl.onButtonWasPushed(2);
    remoteControl.offButtonWasPushed(2);
    remoteControl.undoButtonWasPushed();
  }
}

4.1 람다 표현식을 사용하여 코드 간결하게하기

람다 표현식을 쓰면 객체의 인스턴스를 생성하는 것대신 그 자리에 함수 객체를 사용할 수 있어서 모든 구상 커맨드 클래스를 지울 수 있다.

이 방법은 Command 인터페이스에 추상 메소드가 하나뿐일 때만 사용할 수 있다. 

RemoteControl remoteControl = new RemoteControl();
Light livingRoomLight = new Light("livingRoom");

// 람다 표현식을 사용하지 않은 경우
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightoffCommand(livingRoomLight);
remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);

// 람다 표현식을 사용한 경우
remoteControl.setCommand(0, () -> livingRoomLightOn.on(), () => livingRoomLightOff.off());

 

여러 동작을 한번에 처리하기(Macro Command)


1. 매크로 커맨드 객체 생성

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();
    }
  }
}

2. 매크로에 넣을 일련의 커맨드 생성

Light livingRoomLight = new Light("Living Room");
GarageDoor garageDoor = new GarageDoor("Garage");
CeilingFan ceilingFan = new CeilingFan("Living Room");

LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);    
CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);

3. 매크로 커맨드 객체에 생성한 커맨드 넣기

Command[] on = { livingRoomLightOn, garageDoorUp, ceilingFanOn };
Command[] off = { livingRoomLightOff, garageDoorDown, ceilingFanOff }

MacroCommand onMacro = new MacroCommand(on);
MacroCommand offMacro = new MacroCommand(off);

remoteControl.setCommand(0, onMacro, offMacro)

4. 동작 확인 

remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);

 

Comments