[디자인 패턴] - 커맨드 패턴
이번에 정리할 디자인 패턴은 커맨드 패턴이다. 개인적으로 멀티쓰레드 기반의 프로그램 코드를 작성할 때 중요한 디자인 패턴이라고 생각한다. 물론 싱글쓰레드 프로그램에서도 유용하다.
위키백과에 커맨드 패턴은 아래와 같이 정의되어 있다.
커맨드 패턴(Command pattern)이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다.
커맨드 패턴의 요지는 2가지이다. 먼저 첫째, 요청을 객체의 형태로 캡슐화 한다. 둘째, 사용자가 보낸 요청을 나중에 이용할 수 있도록 필요한 정보를 저장 또는 로깅, 취소할 수 있게 한다. 대충은 알겠지만 말로만 들어선 알 수 없다. 예시코드를 보자.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Command{
public:
virtual void execute(void) =0;
virtual ~Command(void){};
};
class Ingredient : public Command {
public:
Ingredient(string amount, string ingredient){
_ingredient = ingredient;
_amount = amount;
}
void execute(void){
cout << " *Add " << _amount << " of " << _ingredient << endl;
}
private:
string _ingredient;
string _amount;
};
class Step : public Command {
public:
Step(string action, string time){
_action= action;
_time= time;
}
void execute(void){
cout << " *" << _action << " for " << _time << endl;
}
private:
string _time;
string _action;
};
class CmdStack{
public:
void add(Command *c) {
commands.push_back(c);
}
void createRecipe(void){
for(vector<Command*>::size_type x=0;x<commands.size();x++){
commands[x]->execute();
}
}
void undo(void){
if(commands.size() >= 1) {
commands.pop_back();
}
else {
cout << "Can't undo" << endl;
}
}
private:
vector<Command*> commands;
};
int main(void) {
CmdStack list;
//Create ingredients
Ingredient first("2 tablespoons", "vegetable oil");
Ingredient second("3 cups", "rice");
Ingredient third("1 bottle","Ketchup");
Ingredient fourth("4 ounces", "peas");
Ingredient fifth("1 teaspoon", "soy sauce");
//Create Step
Step step("Stir-fry","3-4 minutes");
//Create Recipe
cout << "Recipe for simple Fried Rice" << endl;
list.add(&first);
list.add(&second);
list.add(&step);
list.add(&third);
list.undo();
list.add(&fourth);
list.add(&fifth);
list.createRecipe();
cout << "Enjoy!" << endl;
return 0;
}
위키백과에 있는 예시코드를 보자. 대충봐서는 상속을 이용해 적절히 클래스 코드를 작성한 것 같지만 디자인 패턴이라는 관점에서 봤을 때 커맨드 패턴이 맞다.
위의 코드는 여러가지 요리 재료를 추가하고 마지막에 추가한 요리 재료들로 요리를 만드는 형태의 코드이다. 먼저 재료들에는 여러종류가 있을 수 있다. 가령 위의 코드처럼 식용유 2스푼, 쌀 3컵, 케찹 1병 등이 있지만 중요한 것은 이 재료들을 사용(execute)한다는 공통적인 메소드가 존재한다는 것이다. 물론 사용을 하는 과정 자체는 몇 스푼, 몇 컵 등 양이나 방법이 다르지만 이것을 요청하는 입장에서는 이 내용을 알 필요가 없다. 요리를 실제로 만드는 입장에서나 필요한 내용이지 이런 세부적인 내용(커맨드)는 알 필요가 없기에 상속을 통해 캡슐화 시킨다.
앞서 언급하였듯이 커맨드 패턴의 첫번째 요지, 요청을 객체의 형태로 캡슐화 한다는 위와 같은 의미로 쓰여있는 말이었다. 이어서 두번째 요지에 대해 살펴보자.
class CmdStack{
public:
void add(Command *c) {
commands.push_back(c);
}
void createRecipe(void){
for(vector<Command*>::size_type x=0;x<commands.size();x++){
commands[x]->execute();
}
}
void undo(void){
if(commands.size() >= 1) {
commands.pop_back();
}
else {
cout << "Can't undo" << endl;
}
}
private:
vector<Command*> commands;
};
전체 코드중 CmdStack이라는 클래스를 발췌해왔다. 커맨드 클래스들을 담는 스택 클래스로 앞서 언급하였듯이 createRecipe라는 함수를 통해 Command 클래스들의 execute함수를 실행시켜(커맨드 실행) 요리를 만든다.
일반적인 스택 클래스라 생각할 수도 있지만 중요한 것은 add를 하는 시점과 execute하는 시점이 다르다는 것이다.
한번 생각해보자. add를 하는 시점에 execute를 바로 호출해서 재료를 추가하는 방식으로 구현을 할 수도 있었다. 실제로 결과만 놓고 보았을 땐 그렇게 하여도 똑같은 요리가 완성되어 나온다. 다만 이런식으로 재료를 추가(요청)하는 시점과 추가한 재료들로 실제로 요리를 하는 시점(커맨드 실행)이 분리가 되어 있기에 만약 재료를 잘못 넣었다면 CmdStack에서 뺄 수도 있으며 createRecipe함수를 한번 호출 함으로써 매 재료마다 execute를 호출할 필요 없이(해당 코드에서는 스택에 있는 재료를 한번에 추가하는 기능이 없어 for문을 돌리고 있지만) 한번에 요리를 완성할 수 있다.
이것이 앞에서 언급하였던 커맨드 패턴의 두번째 요지인 사용자가 보낸 요청을 나중에 이용할 수 있도록 필요한 정보를 저장 또는 로깅, 취소할 수 있게 한다는 의미이다.
다시 정리하자면 커맨드 패턴의 요지는 두가지다.
1. 요청을 객체의 형태로 캡슐화 한다.
2. 사용자가 보낸 요청을 나중에 이용할 수 있도록 필요한 정보를 저장 또는 로깅, 취소할 수 있게 한다.
그리고 사실 이 커맨드 패턴은 이전에 서버 코드를 짜면서 사용했던 적이 있었다.
https://teachingforme.tistory.com/29
[Network] - Java로 구현한 소켓 통신 서버(2)
https://teachingforme.tistory.com/25 [Network] - Java로 구현한 소켓 통신 서버(1) 졸업 작품에서 모바일(안드로이드)과 메인 스테이션(데스크탑 컴퓨터)간의 통신을 구현해야해서 자바로 서버 코드를 작성하
teachingforme.tistory.com
Session 클래스 내부의 Send메서드 에서 메시지를 바로 보내지 않고 메세지 큐에 담아 놨다가 전송 요청이 완료된 후에 다시 큐에 있는 패킷들을 이어서 전송하는 부분에서 이미 커맨드 패턴을 사용하고 있었던 것이다.
메시지 전송을 요청하는 시점과 그것을 실제로 수행하는 부분이 분리되어 있고 큐라는 자료구조를 통해 관리되고 있기 때문에 이것또한 커맨드 패턴을 사용하였다고 할 수 있다. 멀티쓰레딩 프로그램에서 커맨드 패턴이 유용하다고 언급하였던 이유가 이런 이유이기도 하다. 요청 시점과 실행 시점을 분리 시키지 않으면 애초에 예외를 뱉기 때문에 실행도 안된다. 그리고 멀티쓰레딩 프로그램에서는 특히 일감을 모아놨다가 한번에 처리하는 것이 성능에 좋기에 이런 커맨드 패턴은 무의식중에라도 사용할 수 밖에 없게 만든다.