헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
Chapter 6. 호출 캡슐화하기 - 커맨드 패턴
예시 - IoT 리모컨
리모컨 세부 사항
- 프로그래밍이 가능한 7개의 슬롯 버튼
- 7개의 슬롯 각각에 대한 ON/OFF 버튼
- 누른 버튼의 명령을 취소하는 UNDO 버튼
제어해야 하는 호출
- 리모컨은 다양한 기기를 제어한다.
- 각 기기들에 대한 객체의 인터페이스들은 공통적인 인터페이스가 있는 것처럼 보이지 않는다.
- 어떤 기기는 다른 기기들과 인터페이스가 많이 다른 점
커맨드 패턴 소개
음식 주문 과정
- 고객이 종업원에게 음식을 주문한다.
createOrder()
- 주문(Order)은 주문서와 그 위에 적혀있는 주문 내용으로 구성된다.
- 즉 Order는 주문 내용을 캡슐화한다.
- 종업원은 주문을 받고, 카운터에 주문을 전달한다.
takeOrder()
orderUp()
: 주문 처리를 준비하는 메서드
- 주방에서 주문대로 음식을 준비한다.
makeBurger()
,makeCoke()
- Order 객체에는 음식 준비를 하기 위해 필요한 모든 지시 사항이 들어있다.
- Order 객체가 주방장에게
makeBurger()
와 같은 메서드 호출로 행동을 지시한다.
커맨드 패턴으로 살펴보기
- 클라이언트
- 클라이언트는 커맨드 객체를 생성해야 한다.
-
createCommandObject()
- 인보커
- 클라이언트는 인보커 객체의
setCommand()
메서드를 호출하면서 커맨드 객체를 넘겨준다. - 그 커맨드 객체는 쓰이기 전까지 인보커 객체에 보관된다.
- 클라이언트는 인보커 객체의
- 커맨드
- 커맨드 객체에는 행동과 리시버의 정보가 들어있다.
- 커맨드 객체에서는
execute()
메서드 하나만 제공한다. execute()
메서드는 행동을 캡슐화하여 리시버에 있는 특정 행동을 처리한다.-
public void execute() { receiver.action1(); receiver.action2(); }
- 리시버
- 인보커에서 커맨드 객체의
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(); } }
- Light 클래스에는
- 커맨드 객체 사용하기
- 우선 기기를 연결할 슬롯과 버튼이 하나씩 있는 리모컨으로 가정하고 사용해본다.
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();
}
}
}
매크로 커맨드 사용하기
- 먼저 매크로에 넣을 일련의 커맨드를 만든다.
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);
- ON 커맨드와 OFF 커맨드용 배열을 각각 만들고 필요한 커맨드를 넣는다.
Command[] allOn = { lightOn, tvOn, stereoOn, hottubOn };
Command[] allOff = { lightOff, tvOff, stereoOff, hottubOff };
MacroCommand macroOn = new MacroCommand(allOn);
MacroCommand macroOff = new MacroComand(allOff);
- MacroCommand 객체를 버튼에 할당
remoteControl.setCommand(0, macroOn, macroOff);
커맨드 패턴 더 활용하기
- 리시버와 일련의 행동을 패키지로 묶어 일급 객체 형태로 전달할 수 있다.
- 이러면 클라이언트 애플리케이션에서 커맨드 객체를 생성한 뒤 오랜 시간이 지나도 그 리시버와 일련의 행동을 호출할 수 있다.
- 이 점을 활용해 커맨드 패턴을 스케줄러나 스레드 풀, 작업 큐와 같은 다양한 작업에 적용할 수 있다.
- 예) 작업 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로
execute()
메서드가 호출된다.
- 예) 작업 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로
- 어떤 애플리케이션은 모든 행동을 기록해 두었다가 애플리케이션이 다운되었을 때 그 행동을 다시 복구할 수 있어야 한다.
- 커맨드 패턴으로
store()
와load()
메서드를 추가해 이런 기능을 구현할 수 있다.- 각 커맨드가 실행될 때마다 디스크에 그 내역을 저장한다.
- 시스템이 다운된 후에, 객체를 다시 로딩해서 순서대로 작업을 다시 처리한다.
- 커맨드 패턴으로