Programming/C++

C++ 커맨드 패턴

_SYPark 2021. 5. 30. 16:01
728x90

소프트웨어를 개발하다 보면 개발 도중 요청이 추가되거나 수정되는 부분이 가장 문제인데 이런 경우가 많이 발생하면 프로젝트가 안정적이지 못하게 됩니다. 가장 좋은 방법은 이런 일이 없게 완벽히 정리하여 설계 후 시작하는게 좋지만 대부분의 경우 이를 완벽히 지키는 어렵습니다. 차선으로 좋은 방법은 추가 혹은 삭제하더라도 이를 쉽게 작업할 수 있도록 설계를 해주는 것입니다. 

 

웹에서 사용자가 요청하는 종류에 따라 다른 화면을 보여줘야 한다고 할 때 그 요청을 cmd 단위로 구분하여 처리하는 구조가 있습니다. 여기서 문제는 제공되는 서비스 항목이 처음부터 정해지지 않고 개발 도중에 추가 또는 삭제될 수 있다는 점입니다. 이럴 때 간단히 요청 종류를 파싱하여 해당 종류에 따라 조건을 타서 처리하는 방법이 있습니다.

class Request { string GetValue(string name) { return nvList_[name]; }
    private:
map <string, string> nvList;
};
class RequestParser {
    void GetRequest(string input, Request& req) {
        ...;	// input에 대한 처리
		req.setNameValue();		// req에 등록
    }
};
class UserManager { bool checkPasswd(Request& req) { } };
class BBSManager { void DisplayList(Request& req) {}
                   void DisplayItem(Request& req) {} };
int main()
{
    string input;
    RequestParser parser;
    UserManager UserMgr;
    BBSManager BBSMgr;
    
    while(1) {
        cin >> input;
        
        Request req;
        parser.Getrequest(input, req);
        
        string cmd = req.getValue("cmd_name");
        if ( cmd == "login" ) { UserMgr.CheckPasswd(req); }
        else if ( cmd == "bbslist" ) { BBSMgr.DisplayList(req); }
        else if ( cmd == "bbsitem" ) { BBSMgr.DisplayItem(req); }
    }
}
            

어떤 요청(input)이 있을 때 RequestParser가 그에 따른 요청을 파싱하여 Request에 등록하고 그 Request에 등록된 cmd를 체크하여 분기로 처리를 해주는 방법입니다. 이럴 경우 Client가 요청에 따른 처리를 위해 UserManager나 BBSManager같은 객체를 모두 관리해줘야 하고 새로운 요청이 추가될 때마다 새로운 분기를 추가해줘야 합니다.

 

Client가 모든 객체를 관리하지 않고 요청이 수정될 때마다 Client를 수정하지 않는 방법이 바람직합니다. 이를 위해서 첫번째로 요청을 처리하는 객체들을 하나의 부모 클래스를 통해 접근하여 같은 인터페이스를 가지도록 하게 하면 Client는 요청의 종류에 상관없이 하나의 인터페이스를 통해 처리를 해주게 됩니다. 하지만 이 방법은 관련이 없는 클래스를 묶게 되는 방법이 될 수 있어 좋지 못한 설계가 이루어질 수 있습니다. Client가 요청을 처리할 객체를 만들어 위임을 해주고 그 객체들은 요청 종류별로 묶어 별도의 클래스를 정의해서 사용하는 방법이 있습니다. 

 

두번째로 요청이 수정될때마다 Client가 수정되지 않으려면 main처럼 조건 비교 문장이 없어야 합니다. 그러기 위해서는 각 요청에 대한 클래스의 상위로 클래스를 하나 정의해서 하나의 인터페이스를 통해 접근하도록 해야 합니다.

 

두가지 조건을 만족하게 설계한다면 아래와 같은 구조를 가지게 됩니다. 만약 새로운 종류의 요청이 추가, 삭제 된다면 Command를 상속받는 Command를 추가, 삭제하면 됩니다.

class Request { string GetValue(string name) { return nvList_[name]; }
    private:
map <string, string> nvList;
};
class RequestParser {
    void GetRequest(string input, Request& req) {
        ...;	// input에 대한 처리
        req.setNameValue();		// req에 등록
    }
};
class UserManager { bool checkPasswd(Request& req) { } };
class BBSManager { void DisplayList(Request& req) {}
                   void DisplayItem(Request& req) {} };

class Command { virtual void Execute(Request& req) = 0; }
class LoginCommand : public Command{
    LoginCommand(UserManager* pUserMgr) : m_pUserMgr(pUserMgr) {}
    void Execute(Request& req) { m_pUserMgr->checkPasswd(req); }
private:
    UserManager* m_pUserMgr;
};
class ListCommand : public Command{
    LoginCommand(BBSManager* pBBSMgr) : m_pBBSMgr(pBBSMgr) {}
    void Execute(Request& req) { m_pBBSMgr->DisplayList(req); }
private:
    BBSManager* m_pBBSMgr;
};
class ReadCommand : public Command{
    LoginCommand(BBSManager* pBBSMgr) : m_pBBSMgr(pBBSMgr) {}
    void Execute(Request& req) { m_pBBSMgr->DisplayItem(req); }
private:
    BBSManager* m_pBBSMgr;
};
UserManager UserMgr;
BBSManager BBSMgr;
map<string, Command *> req2cmd;

void RegisterCommand() {
    req2cmd["login"] = new LoginCommand(&UserMgr);
    req2cmd["bbslist"] = new ListCommand(&BBSMgr);
    req2cmd["bbsitem"] = new ReadCommand(&BBSMgr);

int main()
{
    string input;
    RequestParser parser;
    
    RegisterCommand();
    while(1) {
        cin >> input;
        
        Request req;
        parser.Getrequest(input, req);
        
        string cmd = req.getValue("cmd_name");
        Command* pCmd = req2cmd[cmd];
        if ( pCmd != NULL) { pCmd->Execute(req); }
    }
}
            

만약 클래스 별로 Execute 시 전달해야 하는 인자가 다를 경우 오버로딩을 통해 다형성을 적용하다면 하나의 인터페이스를 통해 접근하려는 설계에 맞지 않게 됩니다. 이럴 경우에는 클래스의 생성자에 인자로 Execute에서 사용할 인자를 미리 전달해두는 방법이 있고 전달될 인자를 각각의 클래스로 정의해서 이를 똑같이 Command를 상속받는 클래스로 만드어 Execute로 호출이 되도록 하는 방법이 있습니다. 

 

추가로 실행한 Execute를 다시 Undo 시키거나 Undo 한 액션을 다시 Redo 시키기 위해서는 History List를 구성해서 매 작업마다 수행된 Command 객체들을 복제해 리스트 안에 저장해 두었다가 처리하는 방법이 있습니다.

 

그리고 컴포지트 패턴을 활용해서 특정 액션을 매크로로 묶어 실행시킬 수 있습니다. 그렇다면 아래와 같은 구조를 가집니다.

이처럼 커맨드 패턴은 요청을 처리할 작업을 일반화 시켜 요청의 종류와는 무관하게 처리할 수 있게 해주는 패턴입니다. 커맨드 패턴을 사용한다면 요청이나 어떤 조건에 따른 분기를 타지 않고 하나의 인터페이스를 통해 상황에 따른 다른 작업을 해줄 수 있습니다.

728x90