서로 다른 요구사항(요청, 명령)들을 객체로 캡슐화 시키는 패턴으로, 실행과 요청을 분리해서 유연성을 높이는 방식이다.
Command
인터페이스만 알고 있으니 코드에 변경이 가해지지 않는다.즉 겉으로 보기에는 동일한 명령을 내리는 것처럼 보이지만, 실질적으로 하는 일은 다른 것이다. 커맨트 패턴은 아래와 같은 인터페이스를 가지고 있으며, 모든 요청 객체는 해당 인터페이스를 구현한다.
public interface Command { // 추상화
public void execute();
}
핵심은 추상화를 통한 실행 책임 분리!
리모컨 예시를 통해 위 그림을 설명해보자. 간단하게, 여러 작업을 수행해야 하는 리모컨을 구현해야 하는 상황이다.
public interface Command {
public void execute(); // 요청 실행
public void undo(); // 요청 취소
}
ConcreteCommand
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();
}
}
Receiver
Light
객체를 의미한다.Invoker
Command
으로 실행을 넘긴다. 다시 말하지만 Command
인터페이스만 알고 있고 Command
가 실제 어떻게 실행되는지 내부는 모르는 상태이다.public class RemoteControl { // 리모컨 객체
Command[] onCommands; // Command 객체들을 저장해둔다.
Command[] offCommands; // 여러 종류의 요청들이 저장되어야 하니 배열로 생성한다.
Command undoCommand;
public RemoteControl() {
// 필드 배열 초기화 코드
}
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() { //undo 기능 추가
undoCommand.undo();
}
}
이제 실제로 클라이언트 쪽 리모컨을 사용하는 코드를 보자.
너무 간단해졌지 않는가? Command
인터페이스로 추상화를 시키지 않았다면 얼마나 리모컨 클래스가 복잡해졌을 지 상상도 안간다.
public class RemoteLoader {
public static void main(String[] args) {
RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
Light livingRoomLight = new Light("Living Room");
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
remoteControl.undoButtonWasPushed();
}
}
만약 여러개의 커맨드를 가지고 있는 커맨드를 구현(여러개의 Receiver 처리)하고 싶다면, 아래와 같은 클래스만 생성해주면 된다.
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();
}
}
}
요청 로그 기록에 사용할 수 있다.
interface Command {
execute();
store(); // 요청이 수행되면 실행 히스토리를 기록한다.
load(); // 시스템 다운 시 최근 수행된 작업부터 다시 진행한다.
}
또한 위와 같은 기능을 활용해서 트랜잭션 시스템을 구현할 수 있다. 트랜잭션에서는 모든 작업이 완벽하게 처리되도록 하거나, 문제가 발생했다면 아무것도 처리되지 않게 롤백되어야 하기 때문이다.