1. 커맨드 패턴이란
1.1 커맨드 패턴의 정의
요청을 객체로 캡슐화하여 클라이언트가 보낸 요청을 나중에 이용 할 수 있도록 메서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소 할 수 있게 하는 패턴.
이 정의에서 핵심은 요청을 캡슐화한다는 점이다.
요청이 캡슐화되기 떄문에 로그를 출력하거나 실행을 취소할 때 유용하게 사용 될 수 있다.
1.2 UML
Command - 추상화된 커맨드 인터페이스. execute() 메소드로 실행하고 undo() 메소드로 실행을 취소한다.
ConcreteCommand - Command 인터페이스를 구현한 구상 클래스
Client - ConcreteCommand를 생성하고 Receiver와 커맨드를 연결시키는 클래스
Invoker - 생성된 커맨드를 가지는 클래스.
Receiver - 요구사항을 수행하는 클래스
위 다이어그램에서 클라이언트가 리시버와 커맨드 객체를 생산하고, 커맨드를 보낼 때는 인보커를 통해서 보내게 된다.
2. 적용 예제
Head First Desgin Pattern에 나오는 예제를 수정한 것이다.
주식회사 홈 오토메이션에서는 가정집의 전등, 차고문, 오디오, 선풍기 등을 조종할 수 있는 만능 리모컨을 개발하려고 한다.
만능 리모컨은 전자기기와 연결 할 수 있는 7개의 슬롯과 그 각각을 켜고 끄는 버튼, 그리고 하나의 취소버튼을 가진다.
그런데 문제는 전자기기들의 작동 방식이 서로 다르다는 것이다.
각 전자기기들을 클래스화한 클래스 다이어그램을 보면서 이야기 하도록 하겠다.
켜기/끄기 버튼만을 가진 리모컨으로 조종하기엔 작동 방식이 너무 제각각이다. 전등(Light)의 경우는 켜고 끄는게 다지만, 오디오(Stereo)는 작동시키기 위해 setCD() 메소드로 CD명을 입력하고, setVolume() 메소드로 볼륨도 조절해줘야하고, 선풍기는 high(), medium(), low() 메소드로 속도를 조절해야한다.
리모컨의 몇번 슬롯에 어느 전자기기가 연결될 지 알 수 없기 때문에 버튼이 눌렸을 때 if-else 문으로 슬롯과 연결된 클래스의 타입을 검사하여 실행하도록 구현되어야 할 것이다.
하지만 그렇게 구현하면 새로운 클래스가 추가될 때마다 리모컨에 있는 코드를 고쳐야 되고, 이는 버그 발생률을 높이는 원인이 된다.
이런 문제를 해결하기 위해 커맨드 패턴을 적용해보도록 하자.
2.1 커멘드 인터페이스 및 구상 클래스 정의
커맨드 패턴 적용을 위해 Command 인터페이스를 만들고 모든 커맨드 클래스들이 이 인터페이스를 구현하도록 한다.
Command 인터페이스는 다음과 같이 리시버에세 요청을 실행하라고 전달하는 execute() 메소드와 실행을 취소하라고 전달하는 undo() 메소드를 가진다.
이제 리모컨 클래스는 이 Command 인터페이스를 이용하여 execute(), undo()메소드가 어떻게 구현되었는지는 신경쓰지 않도록 구현한다.
LightOnCommand
//LightOnCommand.h
#pragma once #include "Command.h" #include "../HomeObejcets/Light.h" //불 켜기 커맨드 //light의 name 어트리뷰트를 통해 어느 곳에 있는 전등인지 구별함 class LightOnCommand : public Command { public: LightOnCommand(Light* light); void execute(); void undo(); private: Light * light = NULL; };
//LightOnCommand.cpp
#include "LightOnCommand.h" LightOnCommand::LightOnCommand(Light* light) { this->light = light; } void LightOnCommand::execute() { light->lightOn(); } void LightOnCommand::undo() { light->lightOff(); }
StereoOnCommand
//StereoOnCommand.h
#pragma once #include "Command.h" #include "../HomeObejcets/Stereo.h" //노래 켜기 커맨드 class StereoOnCommand : public Command { public: StereoOnCommand(Stereo * stereo); void execute(); void undo(); private: Stereo * stereo = NULL; };
//StereoOnCommand.cpp
#include "StereoOnCommand.h" StereoOnCommand::StereoOnCommand(Stereo * stereo) { this->stereo = stereo; } void StereoOnCommand::execute() { this->stereo->setCD(*new string("Sonyun Jump")); this->stereo->setVolume(13); this->stereo->stereoOn(); } void StereoOnCommand::undo() { this->stereo->stereoOff(); }
참고로 StereoOnCommand의 execute() 메소드에서 설정하는 CD와 볼륨 값은 임의로 넣은 값이다. 사용자 편의를 고려해서 커스텀하면 된다.
2.3.1 매크로 커맨드 구현
커맨드들 중 간혹 다수의 커맨드를 연속으로 수행해야하는 경우가 있다.
이런 커맨드를 매크로 커맨드라고 부르는데, 매크로 커맨드는 Command 인터페이스에 대한 레퍼런스를 리스트로 가지고, 그 각각의 excute() 메소드를 호출하는 것으로 execute() 메소드가 구현된다.
//MacroCommand.h
#pragma once #include <vector> #include <algorithm> #include "Command.h" using namespace std; //다수의 커맨드를 실행하는 커맨드 class MacroCommand : public Command { public: MacroCommand(vector<Command*>& commands); void execute(); void undo(); private: vector<Command*> commands; };
//MacroCommand.cpp
#include "MacroCommand.h" MacroCommand::MacroCommand(vector<Command*>& commands) { this->commands = commands; //copy(commands.begin(), commands.end(), this->commands); } void MacroCommand::execute() { int size = this->commands.size(); for (int i = 0; i < size; i++) commands[i]->execute(); } void MacroCommand::undo() { int size = this->commands.size(); for (int i = 0; i < size; i++) commands[i]->undo(); }
우리 프로젝트에서는 PartyMode이라는 커맨드를 지원한다. PartyMode는 파티를 위해서 모든 문을 열고, 모든 전등을 켜고, 오디오를 켜는 커맨드로, 매크로 커맨드 클래스로 정의된다.
//main.cpp
vector<Command*> partyOn = { kitchenLightOnCommand,bedroomLightOnCommand,garageDoorOpenCommand,stereoOnCommand }; vector<Command*> partyOff = { kitchenLightOffCommand,bedroomLightOffCommand,garageDoorCloseCommand,stereoOffCommand}; Command * partyOnCommand = new MacroCommand(partyOn); Command * partyOffCommand = new MacroCommand(partyOff);
2.2 Invoker 역할의 RemoteController 정의
이제 위의 Command 인터페이스를 구현한 LightOnCommand, StereoOnCommand 클래스 코드를 예시로 어떻게 구현되는지 보도록 하자.
onCommands, offCommands는 각각 슬롯에 연결된 전자기기를 켜고 끄는 커맨드 클래스들을 저장한다.
각 슬롯에 커맨드를 할당하기 위해서 setCommand() 메소드로 i번 슬롯에 onCommand와 offCommand를 동시에 입력한다.
각 슬롯에 할당된 on/off 버튼을 누르기 위해서는 on/offButtonWasPushed() 메소드를 이용한다.
그리고 실행 취소를 위해서 unDoButtonWasPushed() 메소드를 사용한다.
다음은 구현된 RemoteController 클래스의 구현 코드다.
//RemoteController.h
#pragma once #include <vector> #include <stack> #include <iostream> #include "Commands/Command.h" #include "Commands/NoCommand.h" using namespace std; //여러 커맨드를 묶어서 리모컨처럼 사용하는 클래스 class RemoteController { public: RemoteController(); void setCommand(int slot, Command* onCommand, Command* offCommand); void onButtonClicked(int slot); void offButtonClicked(int slot); void undoButtonClicked(); private: //켜기 커맨드를 모은 벡터 vector<Command*> onCommands; //끄기 커맨드를 모은 벡터 vector<Command*> offCommands; //커맨드 실행 기록 stack<Command*> commandStack; };
//RemoteController.cpp
#include "RemoteController.h" RemoteController::RemoteController() { this->offCommands.resize(7); this->onCommands.resize(7); Command * noCommand = new NoCommand(); for (int i = 0; i < 7; i++) { this->offCommands[i] = noCommand; this->onCommands[i] = noCommand; } } void RemoteController::setCommand(int slot, Command* onCommand, Command* offCommand) { this->onCommands[slot] = onCommand; this->offCommands[slot] = offCommand; } void RemoteController::onButtonClicked(int slot) { this->onCommands[slot]->execute(); this->commandStack.push(onCommands[slot]); } void RemoteController::offButtonClicked(int slot) { this->offCommands[slot]->execute(); this->commandStack.push(offCommands[slot]); } void RemoteController::undoButtonClicked() { if (commandStack.empty()) { cout << "Remote Controller Undo Caution : no command history" << endl; return; } cout << "undo : "; commandStack.top()->undo(); commandStack.pop(); cout << endl; }
생성자에서 NoCommand 라는 클래스를 이용하는데, 이는 Command 인터페이스를 구현한, 아무런 기능을 하지 않는 클래스다.
슬롯이 비어있을 경우 if문을 이용하여 NULL인지 체크하는 것보다 일종의 NULL 객체인 NoCommand를 넣어 아무런 기능도 하지 않도록 구현했다.
이제 필요한 클래스들은 모두 구현되었다.
구현된 구조를 1.2절의 UML과 연관지어보면 다음과 같다.
이 시스템에서 클라이언트는 따로 클래스화 할 필요가 없어보여서 main() 함수가 클라이언트의 역할을 하도록 했다.
LightOnCommand, LightOffCommand는 Command 인터페이스를 구현한 구상 클래스다. 이 두 클래스를 이용해 전등을 켜고 끈다.
RemoteController는 Invoker의 역할이다. 7개의 슬롯에 각각의 커맨드를 할당하고 슬롯 번호로 사용한다.
이제 리모컨의 켜기/끄기 기능은 구현이 완료되었으니 undo 버튼 구현을 위한 내용들을 살펴보도록 하자.
2.3 UnDo 구현
UnDo 버튼은 직전에 실행했던 기능을 취소하는 기능이다.
우선 각 커맨드 구상 클래스에서 Undo 기능이 어떻게 구현되는지 살펴보겠다.
2.3.1 LightOn/OffCommand 클래스의 UnDo 구현
첫 번째는 가장 간단한 LightOnCommand 클래스다.
//LightOnCommand.cpp
void LightOnCommand::undo() { light->lightOff(); }
LightOnCommand는 불을 켜는 커맨드이기 때문에 불을 꺼주는 것으로 Undo 기능이 구현된다.
LightOffCommand는 반대로 불을 켜주는 것으로 Undo 기능을 구현하면 된다.
//LightOffCommand.cpp
void LightOffCommand::undo() { light->lightOn(); }
2.3.2 State와 Stack 이용한 FanSpeedChangeCommand, FanOffCommand 클래스의 Undo 구현
작업취소 기능을 구현할 때는 상태(State)를 저장해야하는 경우가 종종 있다.
선풍기의 경우도 그런데, Fan 클래스의 코드를 보면서 이야기해보도록 하겠다.
//Fan.h
#pragma once #include <string> #include <stack> #include <iostream> using namespace std; //선풍기 클래스 //꺼짐, 느린속도, 중간속도, 빠른속도의 상태를 가짐 class Fan { public: enum FanStatus { Off, LowSpeed, MediumSpeed, HighSpeed }; public: Fan(); /*********선풍기 속도 조절 메소드********/ void high(); void medium(); void low(); void off(); //////////////////////////////////////// //선풍기의 속도를 받아옴 FanStatus getSpeed(); string toString(); private: //현재 선풍기의 상태 FanStatus fanStatus; };
Fan은 Off(꺼짐), LowSpeed(느리게), MediumSpeed(중간속도), HighSpeed(빠르게) 의 상태를 갖는다.
이제 커맨드 클래스에서 작업 취소 기능을 구현하는 방법을 살펴보자. 작업 취소 기능을 구현하려면 커맨드가 실행되기 이전 상태를 알아야 한다.
그러기 위해 커맨드 클래스에 Fan::FanStatus prevStatus; 필드를 추가하면 될 것 같지만, 이렇게 하면 작업 취소를 여러 번 수행 할 수 없다.
Undo버튼을 여러번 눌렀을 때 처리 할 수가 없다는 것이다.
이 문제를 해결하기 위해 스택을 활용한다.
//FanSpeedChangeCommand.h
#pragma once #include <stack> #include "Command.h" #include "../HomeObejcets/Fan.h" //선풍기 속도 조절 커맨드 //execute 시 꺼짐->느림->보통->빠름->느림->보통... 순으로 변경됨 class FanSpeedChangeCommand : public Command { public: FanSpeedChangeCommand(Fan * fan); void execute(); void undo(); private: Fan * fan = NULL; stack<Fan::FanStatus> fanStatusHistory; };
스택을 활용하면 커맨드가 실행되기 이전 상태가 쌓이고, 작업 취소를 수행 할 때 이 스택에서 상태를 pop 하여 작업 취소에 이용하면 된다.
이제 이 스택을 활용해서 작업 취소가 어떻게 구현되는지 살펴보자.
//FanSpeedChangeCommand.cpp
void FanSpeedChangeCommand::undo() { if (fanStatusHistory.empty()) { cout << "FanSpeedChangeCommand undo caution : no previous Ssatus exists" << endl; return; } Fan::FanStatus prev = fanStatusHistory.top(); fanStatusHistory.pop(); cout << "fan undo : (" << fan->getSpeed() << ") -> (" << prev << ")" << endl; switch (prev) { case Fan::Off: fan->off(); break; case Fan::LowSpeed: fan->low(); break; case Fan::MediumSpeed: fan->medium(); break; case Fan::HighSpeed: fan->high(); break; default: break; } }
FanOffCommand 클래스의 Undo 기능도 거의 유사하게 구현된다.
는지 살펴보자.
//FanOffCommand.cpp
void FanOffCommand::undo() { if (fanStatusHistory.empty()) { cout << "FanOffCommand undo caution : no previous Ssatus exists" << endl; return; } Fan::FanStatus prev = fanStatusHistory.top(); fanStatusHistory.pop(); cout << "fan undo : (" << fan->getSpeed() << ") -> (" << prev << ")" << endl; switch (prev) { case Fan::Off: fan->off(); break; case Fan::LowSpeed: fan->low(); break; case Fan::MediumSpeed: fan->medium(); break; case Fan::HighSpeed: fan->high(); break; default: break; } }
2.3.3 Stack을 이용한 RemoteController의 Undo버튼 기능 구현
지금까지 각 커맨드 클래스에서 작업 취소 기능을 구현하는 방법을 살펴보았다.
이제는 RemoteController 클래스에서 실제로 Undo 버튼을 클릭했을 때 어떻게 처리되는지 살펴볼 차례다.
RemoteController에서는 Undo 버튼을 여러번 클릭했을 때 작업 취소가 연속적으로 실행되어야 한다.
이는 앞에서 살펴본 선풍기 관련 커맨드의 실행취소 기능의 구현과 유사해서 다시 스택을 이용해서 구현된다.
RemoteController 클래스의 정의는 위에서 기술되었으니 undoButtonClicked() 메소드만 살펴보자.
//RemoteController.cpp
void RemoteController::undoButtonClicked() { if (commandStack.empty()) { cout << "Remote Controller Undo Caution : no command history" << endl; return; } cout << "undo : "; commandStack.top()->undo(); commandStack.pop(); cout << endl; }
commandStack은 stack<Command*>로 선언되었다.
구현된 프로젝트는 다음의 깃허브 페이지에서 확인할 수 있다.
(https://github.com/InvincibleTyphoon/CommandPatternExample/tree/master)
3. 커맨드 패턴의 장점
- 요구사항을 객체로 캡슐화 할 수 있다.
- 작업 취소 기능을 지원한다.
- 요청 내역을 큐에 저장할 수 있다.
- 요청 내역을 로그로 기록할 수 있다.
요구사항이 캡슐화되므로 요청을 보내는 내용의 코드 중복을 방지 할 수 있다.
작업 취소 기능을 지원하는 것은 앞서 살펴 본 바 있으니 넘어간다.
요청 내역을 큐에 저장하면 스레드 작업에 유용하다.
처리되는데 오래 걸리는 요청의 경우 스레드를 만들어 백그라운드에서 처리하는 것이 반응성을 높이는데 유용하다.
작업 큐에 다수의 커맨드 객체를 저장해두고 스레드를 여러개 만들면 각 스레드는 작업 큐에서 커맨드를 가져와 처리하면 된다. 그러면 어느 스레드가 먼저 작업을 마치든지 신경쓰지 않아도 된다.
어떤 어플리케이션에서는 모든 행동을 기록해놨다가 그 어플리케이션이 다운되었을 경우, 나중에 그 행동들을 다시 호출해서 복구 할 수 있도록 해야한다.
커맨드패턴에서는 로그를 디스크에 저장하는 store() 메소드와 로그를 읽어오는 load() 메소드를 추가하여 복구 기능을 구현 할 수 있다.
4. 레퍼런스
- Head First Design Pattern(O'REILLY media)
- GoF의 디자인 패턴(Pearson Addison Wesley)
'IT 관련 > 디자인 패턴' 카테고리의 다른 글
퍼사드 패턴(Facade Pattern) (2) | 2019.01.18 |
---|---|
어댑터 패턴(Adapter Pattern) (0) | 2019.01.15 |
싱글톤 패턴 (Singleton Pattern) (0) | 2018.11.16 |
추상 팩토리 패턴(Abstract Factory Pattern) (0) | 2018.11.11 |
팩토리 메소드 패턴(Factory Method Pattern) (0) | 2018.11.09 |