6장. 커맨드 패턴
커맨드 패턴
요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 있다.
장점
클라이언트 애플리케이션에서 커맨드 객체를 생성한 뒤 오랜 시간이 지나도 그 계산 로직을 호출할 수 있다. (요청 내역이 객체로 캡슐화)
- 스케줄러, 스레드 풀, 작업 큐, 트랜잭션 등으로 활용 가능
어떤 작업을 요청하는 쪽과 그 작업을 처리하는 쪽을 분리할 수 있다.
- 리시버: 실제로 수행할 행동 로직을 가지고 있는 객체.
- 커맨드: 리시버 객체에 관한 특정 작업 요청을 캡슐화하는 객체.
- 인보커: 커맨드 객체를 저장하는 객체.
요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
- Command에 로그를 저장하는 메소드, 복구하는 메소드를 추가한다.
- execute()가 실행될 때마다 save()를 호출하여 로그를 저장
- 장애 발생 시 커맨드 객체를 다시 load()하고 순차적으로 execute()를 수행하는 방법으로 복구 가능
일반적으로 리시버에 있는 행동을 호출하는 '더미' 커맨드 객체를 만든다.
항상 리시버를 구현할 필요 없이 커맨드 객체에서 대부분의 행동을 처리해도 됨.
그러나 그렇게 되면 인보커와 리시버를 분리하기 어렵고, 리시버로 커맨드를 매개변수화할 수 없게 된다. (단점이 너무 큼)
정리
인보커에 각 요청 내역에 따른 커맨드 객체들이 캡슐화되어 있고, 요청 내역에 따라 객체가 매개변수화되어 실행이 호출된다. 그러면 호출받은 커맨드 객체가 저장하고 있는 리시버의 행위를 호출하게 된다.
책에서 나온 예시: 리모컨을 활용하여 홈 오토메이션 작업 처리
모든 기기를 제어할 수 있는 리모컨을 개발하려고한다. 리모컨의 코드에 각 기기들의 동작을 정의할 수 있다.
→ 리모컨을 누를 때 호출하는 코드와 실제로 일을 처리하는 코드를 분리해야한다
public interface Command { public void execute(); }
public class RemoteControl {
Command slot;
public RemoteControl() {}
public void setCommand(Command command) {
slot = command;
}
public void buttonPressed() {
slot.execute();
}
}
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on()
}
}
RemoteControl remote = new RemoteControl(); // 인보커 객체 생성
Light light = new Light(); // 리시버 객체 생성
LightOnCommand lightOn = new LightOnCommand(light); // 리시버+행위 를 포함한 커맨드 객체 생성
/* 현재 예제에서는 Command 가 함수형 인터페이스여서,
람다로 대체 가능 remote.setCommand(() -> light.on()); */
remote.setCommand(lightOn); // 인보커에 커맨드 저장
remote.buttonWasPressed(); // 커맨드 execute 호출
→ 사용자는 버튼을 누를 때 어떤 조명이 켜질지 신경쓰지 않아도 된다. 그냥 execute() 메소드만 호출하면 원하는 일을 할 수 있다는 사실만 기억하면 됨! 또한 새롭게 다른 기기가 추가되어도 리모컨의 코드는 변하지 않는다.
리모컨에 마지막으로 한 작업을 취소하는 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();
}
}
public class RemoteControlWithUndo {
Command[] onCommands;
Command[] offCommands;
Command undoCommand = new NoCommand(); // Null Object: 리턴할 객체도 없고 null 처리도 하지 않고 싶을 때 활용
public RemoteControl() {}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void buttonPressed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void undoButtonPressed() {
undoCommand.undo();
}
}
리시버의 이전 상태가 필요한 경우, 커맨드에 이전 상태값을 저장하는 변수를 저장해두고 위와 같은 방식으로 구현 가능 (execute() 호출 전에 현재 상태값을 이전 상태값에 저장)
메타 커맨드 패턴
일련의 여러 명령을 포함한 매크로 커맨드 객체로, 한 번에 여러 행위를 실행 가능한 커맨드 패턴
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 = 0; i < commands.length; i++) {
commands[i].undo();
}
}
}
디자인 패턴은 사용하려는 의도가 중요하다
전략 패턴 vs 커맨드 패턴
전략 패턴
- 어떻게 수행되어야 하는가에 좀 더 초점
커맨드 패턴
- 무엇이 완료되어야 하는가에 좀 더 초점
커맨드 패턴 vs 프록시 패턴
프록시 패턴
- 어떤 부가적인 행동이 수행되어야 하는가에 좀 더 초점