커맨드 패턴(Command Pattern)
-
요구사항을 객체로 캡슐화 할 수 있으며 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을수 있다.
-
사용자의 요청을 객체화하고 그 객체만 있으면 해당 커맨드가 어떤 작업을 수행했는지 알 수 있다.
-
그렇기 때문에 요청 내역을 큐에 저장하거나 로그로 기록할 수 있으며 작업 취소 기능도 지원 가능하다.
-
커맨드 객체는 일련의 행동을 특정 리시버하고 연결시킴으로써 요구사항을 캡슐화한다.
-
이렇게 하기 위해 행동과 리시버를 한 객체에 집어넣고 메소드 하나만 외부에 공개하는 방법을 사용한다.
-
커맨드 패턴은 행위 패턴 카테고리에 속하며 행동을 캡슐화하여 미리 요청을 가지고 있다가 요청할 때 사용할 수 있도록 한다.
-
따라서 요청과 수행의 관계가 느슨하여 SOLID의 DIP(The Dependency Inversion Priciple)를 따른다.
다양한 예시
-
손님이 웨이터에게 주문을 한다.
-
웨이터가 고객의 주문을 주문서에 적는다.
-
웨이터는 주문서를 주방에 전달하여 주문을 요청한다.
-
요리사는 주문서에 적힌 주문대로 음식을 자신의 노하우로 만든다.
-
손님 == 클라이언트
-
웨이터 == 인보커 객체
-
주문을 하는것 == setCommand()
-
주문서 == 커맨드 객체
-
주문을 주방장에게 전달하여 요리하는 것 == execute()
-
주방장 == 리시버 객체
-
클라이언트는 커맨드 객체를 생성한다.
-
클라이언트에서 인보커 객체 안에 있는 setCommand() 메소드를 호출해서 커맨드 객체를 넘겨준다.
-
이 인보커 객체 안에 커맨드 객체가 쓰이기 전까지 보관된다.
-
인보커 객체안에 있는 커맨드 객체의 execute() 메소드를 호출하면 리시버에 있는 특정 행동을 하는 메소드가 호출된다.
-
커맨드 패턴에서는 나중에 클라이언트에서 인보커에게 그 명령을 실행시켜 달라는 요청을 한다.
처음 본다면 당연히 이해가 안될테니 천천히 아래 예시를 통해 개념을 익히고 다시 보자.
그러면 이해가 될 것이다.
-
커맨드 객체가 제공하는 메소드는 execute() 메소드 하나 뿐이다.
-
이 안에는 행동과 리시버에 대한 정보가 같이 들어있다.
public void execute{
receiver.action1();
receiver.action2();
}
리모컨 만들기
1) 커맨드 인터페이스 정의하기
public interface Command {
public void execute();
}
-
커맨드 객체에는 execute() 메소드 하나밖에 지원 하지 않는다.
-
하지만 나중에 여기에 작업 취소 기능을 위해 undo() 메소드를 추가할 수 있다.
2) 리시버 객체(여기서는 전등) 만들기
public class Light {
public void on() {
System.out.println("전등 켜짐");
}
public void off() {
System.out.println("전등 꺼짐");
}
}
3) 인터페이스를 상속받는 커맨드 객체 만들기
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
4) 인보커 객체 만들기
public class SimpleRemoteControl {
Command slot;
public SimpleRemoteControl() {}
public void setCommand(Command command) {
slot = command;
}
public void buttonPressed() {
slot.execute();
}
}
-
커맨드 객체를 저장하는 인보커 객체를 만든다.
-
클라이언트 객체에서 buttonPressed() 메소드를 호출하면
저장된 커맨드 객체(= slot)에서 execute() 메소드를 호출하게 된다.
5) 클라이언트 객체 만들기
public class RemoteControlTest {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOn = new LightOnCommand(light);
remote.setCommand(lightOn);
remote.buttonPressed();
}
}
-
사용자는 커맨드 객체(= LightOnCommand lightOn)를 만들어
인보커 객체인 SimpleRemoteControl 객체로
setCommand() 메소드를 통해 커맨드 객체를 전달한다. -
그리고 클라이언트 객체가 buttonPressed() 메소드를 호출하면
execute() 메소드가 호출되면서
전등의 on()메소드가 연달아 호출된다. -
그렇게 되면 콘솔에 “전등 켜짐” 이라고 나타나게 된다.
복합 리모컨 만들기
1) Command 인터페이스 정의 하기
public interface Command {
public void execute();
}
2) 리시버 객체 만들기 (전등 및 음악 플레이어)
public class Light {
private String location;
public Light(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " 전등 켜짐");
}
public void off() {
System.out.println(location +" 전등 꺼짐");
}
}
public class MusicPlayer {
public void on() {
System.out.println("뮤직 플레이어 켜짐 및 최근 들은 곡 재생");
}
public void off() {
System.out.println("뮤직 플레이어 꺼짐");
}
}
3) 커맨드 객체 만들기
// 전등 On 커맨드
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
// 전등 Off 커맨드
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
}
// 음악 플레이어 On 커맨드
public class MusicPlayerOnCommand implements Command {
private MusicPlayer musicPlayer;
public MusicPlayerOnCommand(MusicPlayer musicPlayer) {
this.musicPlayer = musicPlayer;
}
@Override
public void execute() {
musicPlayer.on();
}
}
// 음악 플레이어 Off 커맨드
public class MusicPlayerOffCommand implements Command {
private MusicPlayer musicPlayer;
public MusicPlayerOffCommand(MusicPlayer musicPlayer) {
this.musicPlayer = musicPlayer;
}
@Override
public void execute() {
musicPlayer.off();
}
}
// 빈 명령 슬롯을 초기화 시키기 위한 Dummy 커맨드
public class NoCommand implements Command {
@Override
public void execute() {
System.out.println("명령 슬롯이 초기화 되어 있지 않습니다.");
}
}
4) 인보커 객체 만들기
public class MultipleRemoteControl {
Command[] onCommands;
Command[] offCommands;
public MultipleRemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
NoCommand 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 onButtonPressed(int slot) {
onCommands[slot].execute();
}
public void offButtonPressed(int slot) {
offCommands[slot].execute();
}
}
- setCommand 메소드로 호출 시 파라미터로 slot번호를 지정해줘야한다.
5) 클라이언트 객체 만들기
public class RemoteControlTest {
public static void main(String[] args) {
MultipleRemoteControl remote = new MultipleRemoteControl();
//리시버 및 커맨드 객체 생성
Light livingLight = new Light("거실");
LightOnCommand livingLightOn = new LightOnCommand(livingLight);
LightOffCommand livingLightOff = new LightOffCommand(livingLight);
Light kitchenLight = new Light("부엌");
LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
MusicPlayer musicPlayer = new MusicPlayer();
MusicPlayerOnCommand musicPlayerOnCommand = new MusicPlayerOnCommand(musicPlayer);
MusicPlayerOffCommand musicPlayerOffCommand = new MusicPlayerOffCommand(musicPlayer);
//인보커 객체의 커맨드 배열에 커맨드 저장
remote.setCommand(0, livingLightOn, livingLightOff);
remote.setCommand(1, kitchenLightOn, kitchenLightOff);
remote.setCommand(2, musicPlayerOnCommand, musicPlayerOffCommand);
//인보커 객체 에서 커맨드 객체의 execute() 메소드 호출
remote.onButtonPressed(0); // 거실 전등 켜짐
remote.onButtonPressed(1); // 부엌 전등 켜짐
remote.onButtonPressed(2); // 뮤직 플레이어 켜짐 및 최근 들은 곡 재생
remote.offButtonPressed(0); // 거실 전등 꺼짐
remote.offButtonPressed(1); // 부엌 전등 꺼짐
remote.offButtonPressed(2); // 뮤직 플레이어 꺼짐
remote.onButtonPressed(4); // 명령 슬롯이 초기화 되어 있지 않습니다.
remote.offButtonPressed(4); // 명령 슬롯이 초기화 되어 있지 않습니다.
}
}
작업 취소(Undo) 기능 만들기
1) 인터페이스에 undo 메소드 추가하기
public interface Command {
public void execute();
public void undo();
}
2) 커맨드 객체에 undo 메소드를 오버라이드
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
/*
이 객체는 On되어있는 상태에서 On시키는 객체이다.
그렇기 때문에 undo() 호출 시
light.off() 메소드를 호출한다.
*/
public void undo() {
System.out.println("-----작업 취소-----");
light.off();
}
}
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
/*
이 객체는 On되어있는 상태에서 Off시키는 객체이다.
그렇기 때문에 undo() 호출 시
light.on() 메소드를 호출한다.
*/
public void undo() {
System.out.println("-----작업 취소-----");
light.on();
}
}
활용
요청 내용을 로그에 기록하기
- 위 예로 설명해보면
//인보커 객체의 커맨드 배열에 커맨드 저장
remote.setCommand(0, livingLightOn, livingLightOff);
remote.setCommand(1, kitchenLightOn, kitchenLightOff);
remote.setCommand(2, musicPlayerOnCommand, musicPlayerOffCommand);
//인보커 객체 에서 커맨드 객체의 execute() 메소드 호출
remote.onButtonPressed(0);
remote.onButtonPressed(1);
remote.onButtonPressed(2);
-
remote호출하였을 경우 그 요청들을 원하는 자료 구조(큐 또는 스택)에 저장시킨다.
-
그렇다면 0,1,2라는 순서로 선택한 자료구조에 쌓이게 된다.
-
즉 요청한 내용을 로그로 기록할 수 있게 된다.
작업 취소 기능
-
자료 구조에 요청한 내용이 저장된 상태라고 할 때
-
작업 취소를 위해 가장 마지막에 수행된 작업에 대해 undo를 수행한다.
-
이런식으로 요청한 작업에 대해서 취소가 가능해진다.