참고: Head First Design Patterns
아래과 같은 원격 조정 장치를 개발중이다.
이제 슬롯에 넣을 수 있는 가전 기기들의 클래스를 살펴보자. 그런데, 이런! 이들 사이에 공통된 인터페이스가 없어 보인다. 뿐만 아니라, 미래에 이런 서로 다른 메소드를 가진 클래스들이 더 추가될 수도 있겠다. 이런 경우에는 어떻게 접근해야 할까?
우리는 이미 아래와 같은 구현은 좋지 않다고 배웠다.
if(slot1 == light)
light.on()
else if(slot1 == hottub)
hottub.jetsOn();
else if ...
이유야 뻔하다. 유지보수. OCP(Open-Closed Principle) 원칙에 크게 어긋나기 때문이다.
버튼을 누르면, 우리의 원격 조정 장치가 가전 기기에게 행동을 요구할 것이고, 그 행동을 실제로 하는 것은 가전 기기일 것이다. 커맨트 패턴은 이용한다면, 이 둘 사이에 커맨드 객체(Command Object)를 둠으로써 행동의 요청자와 실제 행동을 하는 객체를 분리시킬 수 있다.
public interface Command {
public void execute();
}
커맨드 객체를 위한 인터페이스이다. execute()
라는 하나의 메소드만을 갖는다.
위 인터페이스를 이용해 형광등을 위한 커맨드 객체를 만들어보자.
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
light
객체를 주입받아 .on()
을 실행하는 간단한 커맨드이다. 이 커맨드가 실제로 어떻게 사용되는지 살펴보자.
public class SimpleRemoteControl {
Command slot;
public void setCommand(Command command) { slot = command; }
public void buttonWasPresseed() {
slot.execute();
}
}
SimpleRemoteControl
은 행동을 할 객체에 대한 정보를 전혀 알지 못한다. 즉, 버튼이 눌렸을 때 .execute()
하는 커맨드가 light
의 커맨드인지 아닌지조차 모른다. 위에서 설명한대로 요청자 SimpleRemoteControl
과 실제 행동을 하는 light
를 성공적으로 분리시켰다.
커맨드 패턴은 아래와 같이 정의한다.
요청 자체를 객체로 캡슐화하는 디자인 패턴
또한, 아래와 같은 구조를 가진다.
execute()
를 실행하는 객체. 우리의 SimpleRemoteControl
이 여기에 해당한다.light
등의 가전 기기가 여기에 해당한다.이들 간의 관계를 클래스 다이어그램으로 나타내면 위와 같다.
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
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;
}
}
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();
}
}
위에서 하나의 커맨드를 사용한 SimpleRemoteControl
을 살펴보았으니, 이를 발전시켜 커맨드 패턴을 적용한 RemoteControl
을 구현해보았다.
RemoteControl
은 7개의 슬롯을 가지고, 한개의 슬롯은 각각 ON 버튼과 OFF 버튼을 가진다. 따라서, 7개짜리 Command
배열 두개를 선언한다. 전원을 켤 커맨드들을 onCommands
에, 전원을 끌 커맨드들을offCommands
에 보관할 것이다.
setCommand()
메소드는 슬롯 번호, 켜기 커맨드, 끄기 커맨드를 입력 받아 onCommands
, offCommands
에 저장해준다.
buttonWasPushed()
메소드를 ON 버튼과 OFF 버튼에 해당하는 두 개의 메소드로 나누고, 슬롯 번호를 인자로 받아 해당 커맨드의 .execute()
메소드를 호출한다.
NoCommand
는 대체 뭐지?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;
}
}
우리의 생성자는 반복문을 이용하여 모든 커맨드를 NoCommand
로 초기화해주고 있다. 먼저 코드를 보자.
public class NoCommand implements Command {
public void execute() {}
}
바로 그 목적으로 만든 클래스이다. 만약에 초기화가 되지 않은 커맨드의 execute()
메소드를 호출하려 한다면, 널 포인터 예외가 발생할 것이다. 이를 방지하기 위해서는 buttonWasPushed()
메소드에 commands[slot] != null
임을 검사하는 조건문을 추가해야 할 것이다.
그러나, 생성자 단계에서 이러한 아무것도 안하는 객체로 미리 초기화를 해준다면, 버튼 메소드는 커맨드의 널 여부를 검사할 필요가 없다. 만약 커맨드의 널에 대한 핸들링이 필요하다면, NoCommand
내부에서 처리가 가능하다. 이런 객체를 널 객체(Null Objects)라고 한다
널 객체는
null
에 대한 핸들링 책임을 클라이언트가 갖지 않도록 하고싶을 때 유용하다.
이제 어느정도 프로그램의 구조는 다 갖춰간다. 남은 것은 UNDO 버튼이다.
우선, 인터페이스에 메소드를 하나 추가해보자.
public interface Command {
public void execute();
public void undo();
}
자, 그렇다면 이를 구현한 커맨드 역시 수정해줘야 할 것이다.
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() { light.on(); }
public void undo() { light.off(); }
}
생각보다 단순하다. light
는 불을 켜고 끄는 것이 전부이므로, 불 켜기의 undo()
는 당연히 불 끄기겠다.
똑같은 원리로 LightOffCommand
도 구현할 수 있겠다. on과 off만 반대로 해주면 된다.
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() { light.off(); }
public void undo() { light.on(); }
}
마지막으로, RemoteControl
에 UNDO 기능을 넣어보자.
Command[] onCommands;
Command[] offCommands;
Command undoCommand;
마지막으로 누른 커맨드는 곧 UNDO 대상이므로, undoCommand
변수를 만들어서 마지막 커맨드를 저장한다.
undoCommand
를 널 객체로 초기화해주는 문장을 생성자에 추가해줘야 한다.undoCommand = noCommand;
다음으로는 ON, OFF 버튼 메소드에 undoCommand
를 업데이트하는 문장을 넣어준다.
public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void offButtonWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
UNDO 버튼 메소드는 undoCommand
의 .undo()
를 호출해준다.
public void undoButtonWasPushed() {
undoCommand.undo();
}
더 많은 기능을 구현해보며 커맨드 패턴의 응용 연습을 해보자.
불을 켜고 끄는 것에 대한 UNDO는 단순했다. 그런데 만약, 예를 들어 3가지 강도를 가진 실링팬(Ceiling Fan)이라면 어떨까?
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; }
}
CeilingFan
이 위와 같이 구현되어있다고 생각해보자. HIGH
, MEDIUM
, LOW
의 3가지 강도를 가지며 강도 0을 OFF
라고 한다. 이제 CeilingFan
의 커맨드를 구현할 차레인데, 총 4가지 단계가 있으므로 4개의 커맨드가 필요할 것이다.
public class CeilingFanHighCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanHighCommand(CeilingFan ceilingFan) {
this.ceilingFan = ceilingFan;
}
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.high();
}
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();
}
}
prevSpeed
라는 변수를 만들어, execute
하기 전의 강도를 저장한다. undo()
메소드는 이 변수에 저장된 직전 강도대로 CeilingFan
의 상태를 바꿔준다.
CeilingFanMediumCommand
, CeilingFanLowCommand
와 CeilingFanOffCommand
는 위 코드에서 딱 한 줄만 수정해주면 된다. execute()
메소드의
ceilingFan.high();
만 각각 .medium()
, .low()
, .off()
로 바꿔주자.
위와 같이 커맨드를 구현한다면 상태를 갖는 커맨드의 undo()
도 다룰 수 있다.
이번에는 한 번의 버튼 클릭으로 여러 커맨드를 execute
하도록 구현해보자. 우리의 RemoteControl
은 버튼 하나 당 하나의 커맨드만이 매핑되어있는데, 그럼 RemoteControl
을 뜯어 고쳐야 되나?
놀랍게도, 여러 커맨드를 실행 시키기 위한 하나의 커맨드를 만들 수 있다. 이를 매크로 커맨드라고 한다.
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();
}
}
원리는 무척 단순하다. 커맨드들을 담을 자료구조와, 이들을 실행시킬 반복문을 가진 execute()
메소드면 된다.
생성자를 통해 실행시킬 커맨드들을 주입만 시켜준다면, 우리는 macroCommand.execute()
한 번만으로 원하는 만큼의 커맨드 실행을 얼마든지 할 수 있다.
남은 건 undo()
메소드 뿐이다. 근데 당연하게도, 반복문으로 커맨드들의 .undo()
를 호출해주면 그만이다.
public void undo() {
for (int i = 0; i < commands.length; i++
commands[i].undo();
}
undo()
메소드를 응용하면 최근 커맨드 취소 뿐만 아니라 커맨드 히스토리를 관리하며 취소할 수도 있다. 어떻게?
간단하다. Command undoCommand
대신 적절한 자료구조, 이를테면 스택을 이용하여 더 이상 취소할 게 없을 때까지 undo()
할 수 있도록 만들어 줄 수 있다.
지금까지 커맨드 패턴과 여러가지 활용 방법들을 알아보았다. 마지막으로 커맨드 패턴의 몇가지 사용 예시를 살펴보면서 마무리하겠다.
요청 큐(Queuing Requests)
잡 큐(Job Queue)에 커맨드 객체를 넣어 순서대로 execute
하도록 구현할 수 있겠다.
요청 로그(Logging Requests)
커맨드 인터페이스에 store()
, load()
두 가지 메소드를 추가해서 execute
할 때 마다 디스크에 히스토리를 store
한다. 이런 식의 구현은 크래시가 발생했을 때, 디스크에 저장된 커맨드를 다시 load
하여 실행할 수 있도록 해준다. 일종의 체크포인트라고 볼 수 있다.
오빠 너무 멋있어용 잘보고 있어용