티스토리 뷰



오늘은 커맨드 패턴에 대해 포스팅 하겠습니다.




1. Command 패턴이란?

커맨드 패턴(Command pattern)이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다.

- 출처 : 위키백과


command : 명령, 지휘라는 뜻을 가지고 있는 단어 입니다. 

항상 디자인 패턴을 배울 때 마다 보는 단어가 캡슐화입니다. 매번 달라지는 부분을 클래스로 엮는데, 이번엔 요청(Command)이 바뀌어서 이 부분을 분리를 하는 패턴입니다.

커맨드 패턴의 예시에 대해 먼저 이야기 해 보도록 하겠습니다. 


고객사에게 리모컨의 API 디자인을 의뢰받았습니다. 각 버튼은 집 안의 선풍기, 에어컨, 거실 불, 부엌 불을 켜고 끄는 기능을 가지고 있고, 반드시 마지막에 작동했던 기능을 중지할 수 있는 Undo기능이 있어야 한다고 합니다. 제어해야 하는 객체의 인터페이스 정보는 제공을 받은 상태입니다.

하지만 제공받은 기능들이 너무 제각각이라 한 인터페이스로 묶을 수 없는 상태입니다. 가령 TV라면 끄고 키는 기능이 있을 것이고, 선풍기는 강, 중, 약으로 켜기, 끄기 등이 있겠죠?

물론, 이에 대해 전부 리모컨에 기능별로 구현을 해도 지금 당장은 잘 동작할 것입니다. 물론 지금은 선풍기, TV, 거실, 부엌 방 불 정도가 전부이지만, 앞으로 제어해야 할 장치가 얼마나 더 늘어날 지 모르는 일입니다. 이로 인해 수정 중에 버그가 발생할 수도 있고, 여러 코드를 고쳐야 하는 불상사가 생길 수 있겠죠.

어떻게 해야 가장 효율적인 방법으로 리모컨을 디자인할 수 있을까요?


이 때 쓰는 패턴이 바로 커맨드 패턴입니다.

커맨드 패턴은 작업 요청 - 작업 처리를 분리시키는 데 아주 좋은 패턴입니다. 생각해 보면, 굳이 리모컨이 어떠한 기능을 하는지 잘 알 필요가 없습니다. 요청을 처리하는 부분은 '요청이 발생했다'와 '발생하지 않았다'만 알면 되기 때문에 이를 분리시켜 보려고 합니다.


1) 커맨드 패턴에서 알아야 할 용어

커맨드 패턴에서는 총 네가지 용어를 사용합니다.

① 수신자 (Receiver) : 행동을 하는 객체 : 제어 장치 (여러 할 수 있는 행동이 담겨 있을거임) -> 장치

② 커맨드 (Command) : 수신자(Receiver)의 정보 + 행동이 들어 있는 객체 -> 버튼의 행위

③ 발동자 (Invoker) : 커맨드를 저장함 -> 버튼이 어떠한 행동을 할지 설정해 주는 행위

④ 클라이언트 (Client) : 커맨드 객체를 생성, 발동자(Invoker)를 통해 수신자(Receiver)에게 할 행동을 전달함 -> 리모컨


2) 커맨드 패턴의 동작 순서

 1) 클라이언트에서 커맨드 객체를 생성

 2) 인보커로 커맨드 객체를 저장

 3) 클라이언트에서 인보커를 통해 행동 요청을 전송

 4) 수신자가 행동을 함.

커맨드 패턴의 동작 방식은 대략 위 순서로 진행이 됩니다. 더 깊은 이야기는 제가 구현한 코드와 함께 설명하도록 하겠습니다.




2. 코드 구현

이미지출처 : 위키백과

통상적인 커맨드 패턴의 클래스 다이어그램입니다. 위에서 설명했던 말들이 한 번에 잘 나와있는데, Client가 Receiver 정보와 함께 실행함수 (execute()) 정보를 가지고 있는 커맨드 객체를 생성하고, 이 행위는 인보커에 의해 설정되어져서 요청이 왔을 때 실행이 되는 구조입니다.

위에서는 여러 객체를 얘기하였으나, Command는 LightOn과 Off만 구현을 해 보았습니다.

먼저 Light 리시버에 대한 구현입니다.

<CLight.h>

1
2
3
4
5
6
7
8
9
10
class CLight {
public:
    CLight(char *lightLoc);
    ~CLight();
    void On();
    void Off();
 
private:
    char mLightLocation[64];
};
cs

Light는 거실 불이 될 수도 있고, 부엌 불이 될 수도 있기 때문에 mLightLocation 변수에 장소 이름을 저장시켜 놓습니다. 리시버 생성시 위치에 대한 정보를 담을 수 있도록 생성자에 위치 이름을 붙여야 하게 구현하였습니다.

<CLight.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
CLight::CLight(char *lightLoc)
{
    strcpy(mLightLocation ,lightLoc);
}
 
void CLight::On() {
    printf("%s Light is On\n", mLightLocation);
}
 
void CLight::Off(){
    printf("%s Light is Off\n", mLightLocation);
}
cs

구현부는 특별히 볼 내용이 없습니다. On 함수 적용시 해당 위치의 Light가 켜지고, 꺼진다는 내용이 적혀 있습니다.


<CCommand.h>

1
2
3
4
5
6
7
8
class CCommand {
public:
    CCommand();
    virtual void Execute() = 0;
    virtual void Undo() = 0;
public:
    char mCommandName[256];
};
cs

기본 Command 인터페이스입니다.

기본적으로 커맨드 객체는 Execute()를 가지고 있어야 하고, 위에서 리모컨의 Undo() 기능이 있어야 한다고 했기 때문에, 이를 가상함수로 구현해 놓았습니다. 그리고 나중에 해당 커맨드의 이름을 알고 싶어 command 이름을 상속시킬 수 있게 커맨드 클래스에 구현 해놓았습니다.


<CLightOnCommand.h>

1
2
3
4
5
6
7
8
9
10
11
class CLightOnCommand : public CCommand{
 
private:
    CLight *mLight;
 
public:
    CLightOnCommand();
    void Execute();
    void LightOnCommand(CLight *light);
    void Undo();
};
cs

Command 인터페이스를 상속받은 LightOn Command입니다.

Command 객체에는 Receiver 정보와 행동정보가 있는데, 행동 정보는 Execute에 구현되어 있습니다. 그리고 Receiver인 mLight 가 있는 것을 확인하실 수 있습니다.

<CLightOnCommand.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CLightOnCommand::CLightOnCommand()
{
    strcpy(mCommandName, "Light On Command");
}
 
void CLightOnCommand::LightOnCommand(CLight *light)
{
    mLight = light;
}
 
void CLightOnCommand::Execute()
{
    mLight->On();
}
 
void CLightOnCommand::Undo()
{
    mLight->Off();
}
cs

LightOnCommand 을 통해 Receiver 정보를 기록하고, LightOn 요청이기 때문에 Execute() 에는 해당 리시버의 On() 행동을 담고 있습니다. Undo() 에선 해당 행동을 revert 시켜야 하기 때문에 반대 행동 정보를 가지고 있습니다.


이와 동일하게 CLightOffCommand도 구현하였습니다.

<CLightOffCommand.h>

1
2
3
4
5
6
7
8
9
10
11
class CLightOffCommand : public CCommand{
 
private:
    CLight *mLight;
 
public:
    CLightOffCommand();
    void Execute();
    void LightOffCommand(CLight *light);
    void Undo();
};
cs


<CLightOffCommand.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CLightOffCommand::CLightOffCommand()
{
    strcpy(mCommandName, "Light Off Command");
}
 
void CLightOffCommand::LightOffCommand(CLight *light) {
    mLight = light;
}
 
void CLightOffCommand::Execute()
{
    mLight->Off();
}
 
void CLightOffCommand::Undo()
{
    mLight->On();
}
cs


이번엔 리모컨에 대한 구현입니다.

리모컨엔 각 슬롯이 있고, 슬롯에 명령을 할당시키는 방식으로 구현하였습니다.

<CRemoteControl.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CRemoteControl {
 
private:
    int mNumCommand;
    CCommand** mOnCommand;
    CCommand** mOffCommand;
    stack<CCommand *> mCurCmdList;
 
public:
    CRemoteControl(int numCmd);
    void PrintCurCommandList();
    void SetCommand(int slotIdx, CCommand *onCommand, CCommand *offCommand);
    void OnButtonClicked(int slotIdx);
    void OffButtonClicked(int slotIdx);
    void Undo();
 
private:
    void InitRemoteCtrl();
 
};
cs

슬롯의 개수를 유동적이게 할 수 있도록 구현 해 놓았습니다. 생성자에서 넣는 파라미터 값이 슬롯의 개수가 되도록 만들어 놓았습니다.

SetCommand() Invoker를 통해 각 슬롯이 어떤 행동을 하게 만들지 지정하고, 각 슬롯의 버튼을 클릭 했을 때 미리 슬롯에 지정해 놓은 Command를 실행할 수 있도록 구조를 만들어 놓았습니다.

그리고 클라이언트에서 마지막에 어떤 일을 했는지 알아야 Undo() 를 할 수 있으니 마지막에 어떤 행동을 했는지 저장하기 위해 행동한 커맨드의 포인터를 저장한  mCurCmdList 스택 변수도 선언해 놓았습니다.


<CRemoteControl.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
CRemoteControl::CRemoteControl(int numCmd)
{
    mOnCommand = (CCommand **)malloc(sizeof(CCommand** numCmd);
    mOffCommand = (CCommand **)malloc(sizeof(CCommand** numCmd);
 
    mNumCommand = numCmd;
 
    InitRemoteCtrl();
}
 
void CRemoteControl::InitRemoteCtrl() {
 
    for(int i = 0; i < mNumCommand; i++)
    {
        CNoCommand *noOnCommand = new CNoCommand();
        CNoCommand *noOffCommand = new CNoCommand();
 
        mOnCommand[i] = noOnCommand;
        mOffCommand[i] = noOffCommand;
    }
 
}
 
void CRemoteControl::PrintCurCommandList() {
 
    printf("----------- Current Command List -----------\n");
    for(int i = 0; i < mNumCommand; i++)
    {
        printf("OnCommand  %d's button : %s \n", i + 1,mOnCommand[i]->mCommandName);
        printf("OffCommand %d's button : %s \n", i + 1, mOffCommand[i]->mCommandName);
    }
    printf("--------------------------------------------\n\n");
}
 
void CRemoteControl::SetCommand(int slotIdx, CCommand *onCommand, CCommand *offCommand)
{
    if(mOnCommand[slotIdx])
    {
        free(mOnCommand[slotIdx]);
    }
 
    if(mOffCommand[slotIdx])
    {
        free(mOffCommand[slotIdx]);
    }
 
    mOnCommand[slotIdx] = onCommand;
    mOffCommand[slotIdx] = offCommand;
}
 
void CRemoteControl::OnButtonClicked(int slotIdx) {
    mCurCmdList.push(mOnCommand[slotIdx]);
    mOnCommand[slotIdx]->Execute();
}
 
void CRemoteControl::OffButtonClicked(int slotIdx) {
    mCurCmdList.push(mOffCommand[slotIdx]);
    mOffCommand[slotIdx]->Execute();
}
 
void CRemoteControl::Undo()
{
    CCommand *cmd = mCurCmdList.top();
    cmd->Undo();
    mCurCmdList.pop();
}
cs

RemoteControl 생성시 슬롯 개수만큼 동적으로 할당해 주고, 처음 버튼에 아무런 행동을 하지 않는 Command 객체를 할당하여 줍니다. 

SetCommand()를 통해 각 슬롯의 On, Off시 동작을 붙이도록 해 놓았고, OnButtonClicked() , OffButtonClicked() 동작시 버튼에 할당된 행동이 동작하게 되고, 행동에 대한 포인터를 동작시 저장해서 스택에 쌓을 수 있게 해 놓았습니다.

그리고, Undo() 작동시 마지막에 동작한 Command의 Undo 함수를 실행시키고, pop을 이용하여 Undo를 하였으니 이전 행동을 제거합니다.


그렇게, 아래와 같이 main함수를 동작시키면,

<main.cpp>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main() {
 
    CRemoteControl *remoteCtrl = new CRemoteControl(7);
    remoteCtrl->PrintCurCommandList();
 
    CLight *livingRoomLight = new CLight("Living Room");
    CLightOnCommand *livLightOnCmd = new CLightOnCommand();
    CLightOffCommand *livLightOffCmd = new CLightOffCommand();
 
    CLight *kitchenLight = new CLight("Kitchen Room");
    CLightOnCommand *kitLightOnCmd = new CLightOnCommand();
    CLightOffCommand *kitLightOffCmd = new CLightOffCommand();
 
    livLightOnCmd->LightOnCommand(livingRoomLight);
    livLightOffCmd->LightOffCommand(livingRoomLight);
 
    kitLightOnCmd->LightOnCommand(kitchenLight);
    kitLightOffCmd->LightOffCommand(kitchenLight);
 
    remoteCtrl->SetCommand(0, (CCommand *)livLightOnCmd, (CCommand *)livLightOffCmd);
    remoteCtrl->SetCommand(1, (CCommand *)kitLightOnCmd, (CCommand *)kitLightOffCmd);
    
    remoteCtrl->PrintCurCommandList();
 
    remoteCtrl->OnButtonClicked(0);
    remoteCtrl->OffButtonClicked(0);
 
    remoteCtrl->OnButtonClicked(1);
    remoteCtrl->OffButtonClicked(1);
 
    remoteCtrl->Undo();
}
cs



----------- Current Command List -----------

OnCommand  1's button : Not Registered Command 

OffCommand 1's button : Not Registered Command 

OnCommand  2's button : Not Registered Command 

OffCommand 2's button : Not Registered Command 

OnCommand  3's button : Not Registered Command 

OffCommand 3's button : Not Registered Command 

OnCommand  4's button : Not Registered Command 

OffCommand 4's button : Not Registered Command 

OnCommand  5's button : Not Registered Command 

OffCommand 5's button : Not Registered Command 

OnCommand  6's button : Not Registered Command 

OffCommand 6's button : Not Registered Command 

OnCommand  7's button : Not Registered Command 

OffCommand 7's button : Not Registered Command 

--------------------------------------------

----------- Current Command List -----------

OnCommand  1's button : Light On Command 

OffCommand 1's button : Light Off Command 

OnCommand  2's button : Light On Command 

OffCommand 2's button : Light Off Command 

OnCommand  3's button : Not Registered Command 

OffCommand 3's button : Not Registered Command 

OnCommand  4's button : Not Registered Command 

OffCommand 4's button : Not Registered Command 

OnCommand  5's button : Not Registered Command 

OffCommand 5's button : Not Registered Command 

OnCommand  6's button : Not Registered Command 

OffCommand 6's button : Not Registered Command 

OnCommand  7's button : Not Registered Command 

OffCommand 7's button : Not Registered Command 

--------------------------------------------

Living Room Light is On

Living Room Light is Off

Kitchen Room Light is On

Kitchen Room Light is Off

Kitchen Room Light is On


등록 후 슬롯의 리스트가 바뀌는 것을 확인할 수 있고, 버튼 동작시 각 Receiver가 행동하는 것을 확인할 수 있습니다. 또 Undo도 정상적으로 동작하는 것을 확인하실 수 있습니다.



3. 정리


1) 장점 / 단점

 - 항상 말하는 장점이지만, 커맨드 패턴을 활용하게 요청부와 동작부를 분리시켜주기 때문에 시스템의 결합도를 낮출 수 있으며, 각 객체들이 수정되어도 다른 객체가 영향을 받지 않습니다.

-  하지만 리시버의 동작이 추가된다면 그 동작에 대한 클래스를 만들어야 하기 때문에, 다소 많은 잡다한 클래스들이 추가된다는 단점이 있습니다.


2) 활용

 - 작업큐 : 커맨드 리스트를 작업 큐에 저장 후 스레드는 큐로부터 커맨드를 하나씩 받아 각 work의 Execute()만 수행하면 됩니다.

 - 로그 기록 : 디스크에 실행 히스토리를 기록 : 애플리케이션이 다운되었을 경우 커맨드 객체를 다시 로드하여 Execute()만 실행하면 됩니다.



이상으로 커맨드 패턴 포스팅을 마치겠습니다.

혹시 부족하거나, 이상한 부분이 있다면 지적해 주시면 감사하겠습니다.

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함