책정리/GoF 디자인 패턴

18장 수행할 작업의 일반화 문제(Command 패턴)

GONII 2019. 2. 6. 03:55
    소프트웨어 개발 과정에서 가장 문제 하나는 개발해야 항목이 계속 추가되거나 변경되는 것이다. 심지어 처음에는 언급조차 되지 않았던 요구사항이 개발 막바지에 추가되어 소프트웨어의 설계를 송두리째 뒤흔들 경우도 종종 발생한다. 이런 사태가 발생하면 해당 프로젝트는 대부분 실패하거나, 개발이 완료된다고 하더라도 시간이 많이 지연되고 안정성이 떨어지는 등의 문제를 일으킨다.
    이런 문제를 어떻게 회피할 있을까?
    소프트웨어 공학에서는 프로토타이핑(Prototyping)이나 나선형 모델(Spiral Model) 같은 방법들을 제시하고 있다. 그러나 이런 방법들을 적용시킨다고 해서 요구 사항의 추가나 변경으로 인한 문제가 없어지지는 않는다.
    소프트웨어 설계에서 같은 유연한 설계가 필요한 부분들은 많이 있는데, 장에서는 어떤 이벤트가 발생했을 수행해야 작업을 쉽게 추가할 있게 일반화시키는 문제에 대해 살펴보도록 하자. 이런 범주에 속하는 대표적인 예로 클라이언트와 서버간의 송수신 프로토콜 추가나 그래픽 사용자 인터페이스(GUI) 상에서 메뉴의 추가에 따른 수행 작업 추가 문제를 있을 것이다.
    • 문제 사례 설명
    웹으로 게시판 서비스를 제공하는 프로그램을 작성한다고 가정해보자. 브라우저로부터 전달된 요청이 서버를 거쳐 표준 입력 형태로 게시판 프로그램에 전달되고, 게시판 프로그램에서 표준 출력 형태로 전송한 결과가 서버를 거쳐 이용자가 보는 브라우저의 화면으로 전달되는 형태다. 이때 브라우저로부터 전달되는 요청의 종류는 "cmd=login" 같이 cmd라는 이름의 값으로 지정되는 문자열에 의해 결정된다고 하자.
    여기서 문제는 게시판에서 제공하는 서비스 항목이 처음부터 명확히 정해지지 않고, 개발 도중에 점차 추가될 가능성이 많다는 것이다. 이럴 경우 어떻게 하면 기존에 개발해두었던 부분들을 수정하지 않고 쉽게 새로운 서비스 항목을 추가할 있을까?
    • 다양한 접근 방법 및 COMMAND 패턴
      • 기본적인 방법: 요청 종류에 따른 분기 방식
    문제에서 서로 다른 종류의 요청일 경우 cmd 이름의 값이 달라진다고 하였다. , 표준 입력으로 게시판 프로그램에 요청이 전달될 cmd 이름의 값에 따라 요청의 종류가 구별될 있다는 것이다. 따라서 게시판 프로그램의 입장에서는 cmd 이름의 값에 따라 처리할 작업을 지정하는 것이 가장 기본적으로 떠올릴 있는 방법이다.
    [소스 18-1] 요청의 종류에 따라 분기해서 작업을 수행하는 게시판 프로그램
    #include <iostream>
    #include <string>
    #include <map>
    using namespace std;
     
    static const int NA_POS = -1;
     
    #define CMD_NAME "cmd"
     
    #define LOGIN_VAL "login"
    #define BBSLIST_VAL "bbslist"
    #define BBSREAD_VAL "bbsread"
     
    class Request
    {
    public:
    string GetValue(string name)
    {
    return nvList_[name];
    }
    void SetNameValue(string name, string value)
    {
    nvList_[name] = value;
    }
    private:
    map<string, string> nvList_;
    };
     
    class RequestParser
    {
    public:
    bool GetRequest(string input, Request& req)
    {
    string key, value, str;
    int start = 0, pos = 0;
     
    while (pos != NA_POS)
    {
    pos = input.find("=", start);
    if (pos == NA_POS)
    continue;
     
    key = input.substr(start, pos - start);
    start = pos + 1;
     
    pos = input.find("&", start);
    if (pos == NA_POS)
    value = input.substr(start, input.length() - start);
    else
    value = input.substr(start, pos - start);
     
    start = pos + 1;
     
    if (!key.empty())
    {
    req.SetNameValue(DecodeString(key), DecodeString(value));
    key = "";
    }
    }
     
    return true;
    }
     
    protected:
    string DecodeString(string s)
    {
    string output;
    int len = s.length();
    for (int i = 0; i < len; i++)
    {
    if (s[i] == '+')
    output += ' ';
    else if (s[i] == '%')
    {
    const char* pStr = s.data() + i + 1;
    char ch = Hex2Digit(pStr);
    output += ch;
    i += 2;
    }
    else
    output += s[i];
    }
     
    return output;
    }
     
    unsigned int Hex2Digit(const char* hex)
    {
    register char digit;
    digit = (hex[0] >= 'A' ? ((hex[0] & 0x4F) - 'A') + 10 : (hex[0] - '0'));
    digit <<= 4;
    digit += (hex[1] >= 'A' ? ((hex[1] & 0x4F) - 'A') + 10 : (hex[1] - '0'));
     
    return digit;
    }
    };
     
    class UserManager
    {
    public:
    bool CheckPasswd(Request& req)
    {
    cout << "Passwd OK" << endl;
    return true;
    }
    };
     
    class BBS
    {
    public:
    void DisplayList(Request& req)
    {
    cout << "Display BBS List" << endl;
    }
     
    void DisplayItem(Request& req)
    {
    cout << "Display BBS Item" << endl;
    }
    };
     
    BBS _bbs;
    UserManager _userMan;
     
    void main()
    {
    string input;
    RequestParser parser;
     
    while (1)
    {
    cin >> input;
     
    Request req;
    parser.GetRequest(input, req);
     
    string cmd = req.GetValue(CMD_NAME);
    if (cmd == LOGIN_VAL)
    _userMan.CheckPasswd(req);
    else if (cmd == BBSLIST_VAL)
    _bbs.DisplayList(req);
    else if (cmd == BBSREAD_VAL)
    _bbs.DisplayItem(req);
    else
    cout << "Not Avaliable Command : " << cmd << endl;
    }
    }


    • 패턴 활용 방식: COMMAND 패턴
    요청의 종류에 따른 분기 방식은 크게 가지 문제가 있다. 하나는 Client 프로그램이 요청을 처리할 객체를 일일이 알고 있어야 한다는 점이고, 다른 하나는 새로운 종류의 요청 처리가 필요할 때마다 Client 프로그램을 수정해야 한다는 점이다.
    먼저 Client 프로그램이 요청을 처리할 객체를 일일이 필요가 없는 방법을 생각해보자. 가지 방법은 요청을 처리하는 객체들의 클래스를 모두 동일한 상속 구조에 포함시켜 정의하는 것이다. 예를 들어 UserManager BBS 클래스를 동일한 상속 구조 하에 정의하는 것이다. 이들 클래스들은 동일한 인터페이스를 가지게 되며, 이렇게 되면 Client 처리해야 요청의 종류에 상관없이 동일한 형태로 프로그램 작성이 가능해진다. 그러나 방법은 요청을 처리하는 객체들의 클래스가 별다른 관련도 없는데 같은 상속 구조 하에서 동일한 자료형을 다루어지게 만드는 것이므로 클래스 설계 측면에서는 바람직하지 않은 방법이다.
    다르게 생각해볼 있는 방법은 Client 하던 역할을 대신하는 객체를 두는 것이다. , 요청의 종류에 따라 처리 객체를 지정하는 일을 대신 수행하는 객체를 정의하고 Client 하던 역할을 모두 객체에게 위임시키는 것이다. 방법을 적용할 경우 Client 요청을 처리할 객체에 대해 일일이 몰라도 것이다. 왜냐하면 일을 위임받은 객체가 따로 존재하기 때문이다.
    이때 Client 역할을 위임받은 객체의 클래스는 실제 어떻게 설계되는 것이 바람직할까?
    문제의 핵심은 요청의 종류에 따라 그것을 처리할 객체에 대한 정보를 저장, 관리하도록 만드는 것이다. 이를 위해 생각해볼 있는 가지 방법은 단일 클래스 내에서 요청의 종류에 따라 그것을 처리할 객체에 대한 정보를 저장, 관리하게 만드는 것이다. 그러나 방식은 새로운 종류의 요청이 추가될 때마다 클래스 내부를 수정해야 하는 불편함이 있을 있다.
    문제를 해결할 있는 보다 나은 방식은 요청의 종류별로 별도의 클래스를 정의해서 그것을 처리할 객체에 대한 정보를 저장, 관리하게 하는 것이다. 예를 들어 사용자 로그인 요청과 이를 처리할 UserManager 클래스 객체를 연결시키기 위해서는 LoginCommand 클래스를 정의하고, 게시물 목록 화면 요청을 BBS 클래스 객체와 연결시키기 위해서는 ListCommand 클래스를 정의하며, 게시물 본문 화면 요청을 BBS 클래스 객체와 연결시키기 위해서는 ReadCommand 클래스를 정의하는 식이다. , 이때 Client 프로그램에서 이들 클래스 객체들을 동일한 방식으로 다룰 있게 만들어 주기 위해서는 이들 클래스의 상위에 Command 클래스와 같은 Abstract Base Class 정의해주는 것이 필요할 것이다.
    이같은 방식을 따른다면 Client 요청을 처리할 객체를 일일이 알아야 하는 문제는 해결될 있을 것이다. 그렇다면 두번째로 새로운 요청이 추가되더라도 Client 수정되지 않게 하려면 어떻게 하면 될지 생각해보자.
    새로운 요청이 추가되더라도 Client 수정되지 않게 만들려면 [소스 18-1] 존재하는 조건 비교 문장이 사라져야 한다.
    [소스 18-1]에서 조건 비교는 cmd 이름의 값을 기준으로 일어난다. 따라서 조건 비교를 없앨 있는 가지 방법은 cmd 이름으로 주어질 있는 값들과 그에 따라 실행해야 모듈들을 미리 등록해두었다가 요청이 들어오면 요청의 cmd 값과 일치하는 모듈을 찾아서 곧바로 실행시켜주는 것이다. 그런데 방법을 적용하려면 먼저 cmd 이름으로 주어지는 값에 대해 실행해야 묘듈들을 일반화시켜 저장, 관리할 있는 자료형이 필요하다. , UserManager클래스의 CheckPasswd(), BBS 클래스의 DisplayList() DisplayItem() 모듈들을 일반화시켜 저장할 있는 자료형이 필요한 것이다.
    이런 문제를 해결하면서 cmd 주어지는 값에 따라 불려질 모듈을 일반화시킬 있는 다른 방법으로는 불려질 모듈별로 각각 별도의 클래스를 정의하고 이들이 동일한 상속 구조하에 놓이도록 하는 것이다.


    이처럼 요청과 요청을 처리할 객체를 중계하기 위한 클래스 상속 구조를 Command 패턴이라고 한다. 여기서 Command 클래스는 요청을 처리할 객체를 내부적으로 저장, 관리하면서 요청이 주어졌을 요청을 처리할 객체의 멤버 함수를 불러주는 역할을 한다. 이때 요청을 처리할 객체는 Command 클래스의 객체가 생성되는 시점에 지정되며, Client 요청을 전달하는 방식은 Command 하위 클래스들이 제공하는 공통된 인터페이스를 통해 이루어진다. Command 하위 클래스들이 이처럼 공통된 인터페이스를 제공하는 이유는 Client 입장에서는 구체적으로 어떤 클래스의 객체가 사용되는지와 상관없이 다형성을 적용해서 프로그램 동작이 일어날 있게 만들기 위해서다.
    • 샘플 코드
    [소스 18-2] CGI 형태의 게시판 프로그램에 대해 Command 패턴이 적용된 샘플 코드
    #include <iostream>
    #include <string>
    #include <map>
    using namespace std;
     
    static const int NA_POS = -1;
     
    #define CMD_NAME "cmd"
     
    #define LOGIN_VAL "login"
    #define BBSLIST_VAL "bbslist"
    #define BBSREAD_VAL "bbsread"
     
    class Request
    {
    public:
    string GetValue(string name)
    {
    return nvList_[name];
    }
    void SetNameValue(string name, string value)
    {
    nvList_[name] = value;
    }
    private:
    map<string, string> nvList_;
    };
     
    class RequestParser
    {
    public:
    bool GetRequest(string input, Request& req)
    {
    string key, value, str;
    int start = 0, pos = 0;
    while (pos != NA_POS)
    {
    pos = input.find("=", start);
    if (pos == NA_POS)
    continue;
     
    key = input.substr(start, pos - start);
    start = pos + 1;
     
    pos = input.find("&", start);
    if (pos == NA_POS)
    value = input.substr(start, input.length() - start);
    else
    value = input.substr(start, pos - start);
     
    start = pos + 1;
     
    if (!key.empty())
    {
    req.SetNameValue(DecodeString(key), DecodeString(value));
    key = "";
    }
    }
    return;
    }
    protected:
    string DecodeString(string s)
    {
    string output;
    int len = s.length();
    for (int i = 0; i < len; i++)
    {
    if (s[i] == '+')
    output += ' ';
    else if (s[i] == '%')
    {
    const char* pStr = s.data() + i + 1;
    char ch = Hex2Digit(pStr);
    output += ch;
    i += 2;
    }
    else
    output += s[i];
    }
    return output;
    }
    unsigned int Hex2Digit(const char* hex)
    {
    register char digit;
    digit = (hex[0] >= 'A' ? ((hex[0] & 0x4F) - 'A') + 10 : (hex[0] - '0'));
    digit <<= 4;
    digit += (hex[1] >= 'A' ? ((hex[1] & 0x4F) - 'A') + 10 : (hex[1] - '0'));
     
    return digit;
    }
    };
     
    class UserManager
    {
    public:
    bool CheckPasswd(Request& req)
    {
    cout << "Passwd OK" << endl;
    return true;
    }
    };
     
    class BBS
    {
    public:
    void DisplayList(Request& req)
    {
    cout << "Display BBS List" << endl;
    }
     
    void DisplayItem(Request& req)
    {
    cout << "Display BBs Item" << endl;
    }
    };
     
    class Command
    {
    public:
    virtual void Execute(Request& req) = 0;
    };
     
    class LoginCommand : public Command
    {
    public:
    LoginCommand(UserManager* pUserMan)
    {
    pUserMan_ = pUserMan;
    }
    void Execute(Request& req)
    {
    pUserMan_->CheckPasswd(req);
    }
    private:
    UserManager* pUserMan_;
    };
     
    class ListCommand : public Command
    {
    public:
    ListCommand(BBS* pBbs)
    {
    pBbs_ = pBbs;
    }
    void Execute(Request& req)
    {
    pBbs_->DisplayList(req);
    }
    private:
    BBS* pBbs_;
    };
     
    class ReadCommand : public Command
    {
    public:
    ReadCommand(BBS* pBbs)
    {
    pBbs_ = pBbs;
    }
    void Execute(Request& req)
    {
    pBbs_->DisplayItem(req);
    }
    private:
    BBS* pBbs_;
    };
     
    BBS _bbs;
    UserManager _userMan;
    map<string, Command*> _req2cmd;
     
    void RegisterCommand()
    {
    _req2cmd[LOGIN_VAL] = new LoginCommand(&_userMan);
    _req2cmd[BBSLIST_VAL] = new ListCommand(&_bbs);
    _req2cmd[BBSREAD_VAL] = new ReadCommand(&_bbs);
    }
     
    void 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);
    else
    cout << "Not Available Command : " << cmd << endl;
    }
    }
    Command 클래스들은 요청 처리 필요한 객체에 대한 정보를 별도의 자료구조로 참조하고 있으며, Client 요청을 전달할 인터페이스도 Command 클래스의 종류에 상관없이 Execute() 라는 멤버 함수를 공통으로 이용하고 있음을 있다. 또한 비교 문장을 없애기 위해 미리 요청별로 Command 클래스 객체를 등록해두고 사용하는 형태를 취하고 있다.
    • 구현 관련 사항
    • COMMAND 패턴 정리
    Command 패턴이 유용한 경우
    • GUI 등을 구현 메뉴나 버튼에 수행할 작업을 일반화시켜 설정하고자 유용하다. 경우에 따라 동일한 메뉴에 대해서 동적으로 다른 Command 클래스 객체를 설정함으로써 동일한 메뉴나 버튼이 선택되더라도 상황에 따라 다른 작업을 수행하게 만들 수도 있다.
    • 작업 수행을 요청한 시점과 실제 작업을 수행하는 시점을 달리하고 싶을 때에도 유용하다. Command 패턴은 직접 수행할 작업에 대한 함수를 불러주는 형태가 아니라 수행할 작업에 대한 객체를 먼저 생성하고, 나중에 작업을 수행을 요청하는 형태이므로 수행할 작업을 등에 쌓아두었다가 작업 수행이 필요한 시점에 작업을 수행하는 것이 가능하기 때문이다.
    • Undo Redo 기능을 지원하고자 유용하다.
    • 시스템 작업 과정에서 각각의 단계를 기록으로 남겨두었다가 작업 도중 시스템 장애가 발생했을 이전 상태로까지 시스템을 복구하는 용도로 Command 패턴을 사용할 수도 있다.
    • Command패턴과 Composite 패턴이 복합된 MacroCommand 여러 작업을 한꺼번에 처리하기 위한 스크립트 작업 처리에 유용할 있다.
    Command 패턴의 장점
    • 작업 수행을 쵸어하는 객체와 실제 작업을 수행하는 객체를 분리시켜주므로 시스템의 결합도를 낮출 있으며, 객체가 독립적으로 변경 가능한 장점을 가진다.
    • Command 하위 클래스는 기존 클래스와 무관하게 확장이 가능하며, 확장된 클래스들은 Client 별다른 수정없이 사용이 가능하다. 그래서 새로운 종류의 작업 처리를 추가하기가 쉽다.
    • MacroCommand 클래스처럼 Command패턴에 Composite 패턴을 복합해서 적용할 경우 복잡한 작업을 기본적인 작업들로 구조화시켜 다룰 있는 장점이 있다.
반응형