책정리/GoF 디자인 패턴

24장 객체 상태 추가에 따른 행위 수행 변경 문제(State패턴)

GONII 2019. 2. 7. 20:40
    객체의 행위 수행은 해당 객체의 상태에 따라 다양하게 변경될 있다. 때문에 객체의 행위 수행을 구현할 때에는 객체의 상태 값을 비교해서 서로 다른 행위를 수행하게 분기문을 두는 것이 보통이다. 그런데 여기서 문제는 같은 분기문의 경우 객체가 머무를 있는 상태의 종류가 새롭게 추가되었을 이를 반영하기 어렵다는 단점이 있다. 왜냐하면 분기문을 포함하는 경우에는 새로 추가된 상태를 반영하기 위해 기존에 작성된 프로그램을 분석해서 수정해야 하는 부담이 뒤따르기 때문이다.
    장에서는 이런 관점에서 객체가 머무를 있는 상태가 새로 추가되더라도 추가된 상태를 포함해서 객체의 행위 수행 변경이 손쉽게 이루어질 있는 클래스 구조 설계에 대해 살펴본다.
    • 문제 사례 설명
    게임 플레이어가 임의의 공격 명령을 내렸을 자신의 등급 상태에 따라 가능한 공격만을 수행하게 프로그램을 설계한다면 어떤식이 바람직할까?
    • 다양한 접근 방법 및 STATE 패턴
    주어진 문제는 격투 게임을 위한 프로그램에서 게임 플레이어의 공격 유형이 게임 플레이어의 등급에 따라 결정될 , 게임 플레이어가 임의의 공격을 요청할 경우 어떻게 현재 등급에서 가능한 공격만 수행하도록 프로그램을 설계할 것인가 하는 것이다. 문제는 결국 게임 플레이어를 객체로 표현하고, 게임 플레이어의 등급을 객체의 내부 상태로 표현할 객체의 내부 상태가 변경됨에 따라 객체의 행위 수행이 변경되는 것을 어떻게 구현하는 것이 바람직한가에 대한 질문이라고 있다.
    여기서 주의 깊게 살펴야 하는 것은 다른 일반적인 경우와 달리 문제에서는 객체가 머무를 있는 내부 상태의 종류가 계속해서 추가되거나 세분화될 있다는 사실이다. 게임 플레이어가 머무를 있는 등급이 계속 추가되거나 세분화될 있다는 것이다.
    • 기본적인 방법: 상태 비교에 따른 행위 변경 방식
    일반적으로 객체의 내부 상태가 변경되면 객체의 행위 수행도 변경되기 마련이다. 여기서 객체의 내부 상태가 변경되었을 행위 수행도 변경시켜줄 있는 가장 기본적인 방법은 객체 내부에서 상태 비교를 통해 행위 수행을 변경해주는 것이다. 예를 들어 게임 플레이어 객체에 날아차기 공격 명령이 주어졌다고 게임 플레이어 객체는 내부적으로 자신의 등급이 고급인지를 확인하고, 고급 상태일 경우에만 날아차기를 수행해주는 식이 것이다.
    일단 방식을 적용할 경우 객체의 내부 상태가 변경되었을 행위 수행을 변경해주는 것은 별다른 문제없이 처리 가능할 것이다. 그러나 방식의 문제는 새로운 종류의 게임 플레이어 등급이 추가되었을 때이다. 새로 추가된 등급에 따라 객체의 행위가 달라져야 한다면, 객체 상태 비교를 수행하는 모든 곳을 찾아서 수행해야 하는 불편이 따르기 때문이다. 이는 기존 소스코드를 분석하는 작업을 수반하는 것으로 프로그램의 유지, 보수 비용을 높이는 결과를 초래할 것이다.
    • 패턴 활용 방식: STATE 패턴
    객체 내부의 상태 비교를 통해 행위 수행을 변경하는 방식은 일반적이기는 하나, 새로운 종류의 상태가 추가될 경우 기존 소스를 분석해서 수정해야 하는 문제가 있었다.
    객체지향 설계에서 기존의 소스코드에 영향을 주지 않고 새로운 것을 추가하기 위한 대표적인 방법 하나는 바로 클래스 상속을 이용하는 것이다. 왜냐하면 클래스 상속의 경우 최상위 클래스에서 제공하는 인터페이스 형태로만 프로그램을 작성하면 새로운 하위 클래스가 추가되더라도 기존 소스코드의 수정없이 다형성에 의해 추가된 구현 내용을 적용할 있기 때문이다.
    같은 클래스 상속의 장점을 이용하면 새로운 게임 플레이어 등급을 기존 소스코드 수정없이 쉽게 추가할 있을 것이다.
    클래스 상속을 통해 새로운 종류의 상태를 소스코드 수정없이 추가했다면, 추가된 상태를 포함해서 객체의 상태 변화가 있을 때마다 행위 수행 변경을 기존 소스코드 수정없이 처리해주기 위한 방법은 무엇일까?
    게임 플레이어 객체가 직접 등급 변화에 따른 행위 수행 변경을 처리하는 것은 기존 소스코드의 수정을 불러오는 문제가 있었다. 따라서 기존 소스코드 수정없이 등급 변화에 따른 행위 수행 변경을 처리하려면 간접적인 방법을 사용해야 것이다. , 게임 플레이어 객체가 다른 객체에게 등급 변화에 따른 행위 수행 변경을 위임시키는 것이다. 이때 관련 작업을 위임시킬 가장 좋은 대상은 바로 게임 플레이어 등급 객체일 것이다. 왜냐하면 행위 수행 변경이 어차피 등급 정보에 따라 일어나기 때문에 게임 플레이어 등급 객체에게 행위 수행 변경을 위임시키면 내부의 등급 상태 정보에 따라 적절한 행위 수행 변경을 있을 것이기 때문이다.
    이때 새로운 등급이 추가되더라도 기존 소스코드 변경없이 새로운 등급을 포함한 행위 수행 변경이 가능한 이유는 바로 게임 플레이어 등급 클래스들이 상속 구조를 이루고 있고 이를 통해 다형성이 적용될 있기 때문이다. , 새로운 게임 플레이어 등급을 추가하기 위해 하위 클래스가 추가로 정의되더라도 기존 소스코드가 클래스 상속 구조 상의 다형성을 이용해서 작성되어 있으면 새로 추가된 등급의 객체에 대해서도 동일한 소스코드로 동작이 가능한 것이다.


    [그림 24-1]에서 GamePlayer 클래스와 GameLevel 하위 클래스들은 모두 3개의 동일한 인터페이스를 가지고 있는데, 이는 GamePlayer 객체에 각기 다른 유형의 공격 요청이 주어졌을 이에 대한 처리를 GameLevel 하위 클래스에게 위임시키기 위한 것이다. 이때 GamePlayer 객체가 공격 요청을 위임시키기 위한 방법으로는 내부적으로 관리하고 있는 GameLevel 포인터 객체를 통해 다형성을 적용시키는 것이다.
    위에서 살펴본 것처럼 어떤 객체의 내부 상태가 계속 추가될 가능성이 있을 새로운 상태의 추가도 쉽도록 만들어주고, 추가된 상태를 포함해서 객체의 상태 변화 기존 소스코드 변경없이 행위 수행 변경이 가능하도록 객체 상태 정보를 클래스 상속 구조로 정의해서 사용하는 방식을 State 패턴이라고 한다. 이때 State 패턴에 포함된 클래스들은 자신이 표현하고 있는 객체 상태에 따라 어떤 행위를 수행할지를 미리 정의하고 있기 때문에 이들을 이용하는 객체들은 쉽게 상태 변화에 따른 행위 수행 변경을 처리할 있게 된다.
    • 샘플 코드
    [소스 24-1] state패턴을 통해 게임 플레이어 등급 상태 추가 및 행위 변경 문제 해결
    // state.c
    #include <iostream>
    #include <string>
    using namespace std;
     
    class GameLevel
    {
    public:
    static GameLevel* CreateInstance() { return 0; }
    virtual void SimpleAttack() = 0;
    virtual void TurnAttack() = 0;
    virtual void FlyingAttack() = 0;
    protected:
    GameLevel() {}
    };
     
    class GameLevel0 : public GameLevel
    {
    public:
    static GameLevel* CreateInstance()
    {
    if (pInstance_ == 0)
    pInstance_ = new GameLevel0;
     
    return pInstance_;
    }
    virtual void SimpleAttack() { cout << "Simple Attack" << endl; }
    virtual void TurnAttack() { cout << "Not Allowed" << endl; }
    virtual void FlyingAttack() { cout << "Not Allowed" << endl; }
    protected:
    GameLevel0() {}
    private:
    static GameLevel0* pInstance_;
    };
    GameLevel0* GameLevel0::pInstance_ = 0;
     
    class GameLevel1 : public GameLevel
    {
    public:
    static GameLevel* CreateInstance()
    {
    if (pInstance_ == 0)
    pInstance_ = new GameLevel1;
     
    return pInstance_;
    }
     
    virtual void SimpleAttack() { cout << "Simple Attack" << endl; }
    virtual void TurnAttack() { cout << "Turn Attack" << endl; }
    virtual void FlyingAttack() { cout << "Not Allowed" << endl; }
     
    protected:
    GameLevel1() {}
    private:
    static GameLevel1* pInstance_;
    };
    GameLevel1* GameLevel1::pInstance_ = 0;
     
    class GameLevel2 : public GameLevel
    {
    public:
    static GameLevel* CreateInstance()
    {
    if (pInstance_ == 0)
    pInstance_ = new GameLevel2;
     
    return pInstance_;
    }
     
    virtual void SimpleAttack() { cout << "Simple Attack" << endl; }
    virtual void TurnAttack() { cout << "Turn Attack" << endl; }
    virtual void FlyingAttack() { cout << "Flying Attack" << endl; }
     
    protected:
    GameLevel2() {}
    private:
    static GameLevel2* pInstance_;
    };
    GameLevel2* GameLevel2::pInstance_ = 0;
     
    class GamePlayer
    {
    public:
    GamePlayer()
    {
    pGameLevel_ = GameLevel0::CreateInstance();
    }
    void UpgradeLevel(GameLevel* pLevel)
    {
    pGameLevel_ = pLevel;
    }
     
    void SimpleAttack() { pGameLevel_->SimpleAttack(); }
    void TurnAttack() { pGameLevel_->TurnAttack(); }
    void FlyingAttack() { pGameLevel_->FlyingAttack(); }
    private:
    GameLevel* pGameLevel_;
    };
     
    void main()
    {
    GamePlayer user1;
     
    user1.SimpleAttack();
    user1.TurnAttack();
    user1.FlyingAttack();
    cout << "--------------------" << endl;
     
    GameLevel* pGameLevel1 = GameLevel1::CreateInstance();
    user1.UpgradeLevel(pGameLevel1);
     
    user1.SimpleAttack();
    user1.TurnAttack();
    user1.FlyingAttack();
    cout << "--------------------" << endl;
     
    GameLevel* pGameLevel2 = GameLevel2::CreateInstance();
    user1.UpgradeLevel(pGameLevel2);
     
    user1.SimpleAttack();
    user1.TurnAttack();
    user1.FlyingAttack();
    }
    • 구현 관련 사항
    State 패턴은 상태 변화에 따라 행위 수행 변경이 자동으로 이루어지게 만들기 위한 것이지 상태 전환을 쉽게 하기 위해 설계된 것은 아니다. State 패턴에서는 누가 상태 전환을 수행하지를 정의하지 않고 있다. 그러므로 State 패턴을 구현할 상태 전환을 수행할 있는 방법은 Client 직접 수행하는 것과 State 패턴에 포함된 클래스의 객체들끼리 서로 자신의 다음 상태를 지정하는 형태로 상태 변경을 수행하는 것이 있을 있다.
    여기서 전자 방식은 상태 전환을 일일이 Client 수행해야 한다는 불편은 있으나 반대로 Client 임의로 상태 전환을 수행해줄 있다는 장점이 있다. 후자 방식은 Client 상태 전환에 대해 신경쓰지 않아도 되는 장점이 있지만, State 패턴에 포함된 클래스들간에 서로 참조해야 하는 문제가 발생한다. , 새로운 상태를 표현하기 위해 클래스가 추가되면 최소한 새로 정의된 클래스로 상태 전환이 필요한 클래스는 수정이 불가피한 문제가 있다는 것이다.
    상태 전환을 누가 수행하든지 간에 상태 전환 과정을 일괄되게 만들어 주기 위해서는 상태 전환표를 이용하는 것이 효율적이고, 바람직할 있다. 이때 상태 전환표는 각각의 상태에서 모든 가능한 입력에 대해 다음 상태를 지정하는 형태로 구성될 있을 것이다.
    이같은 상태 전환표를 이용하게 되면 프로그램 소스코드의 수정없이 상태 전환표 내의 데이터만 수정함으로써 얼마든지 상태 전환 로직을 변경시킬 있다는 장점이 있다. 반면 상태 전환표를 이용하는 방식의 단점은 상태 전환 , 표를 검색해야 하므로 속도가 느리다는 거소가 상태 전환표 형태로 상태 전환 로직이 표현될 경우 이를 사람이 이해하기 힘들다는 것이다. 또한 상태 전환표를 이용할 경우에는 상태 전환 도중에 특정한 행동을 수행하게 만드는 것이 어렵다는 단점도 있다.
    State 패턴의 구현과 관련해서 또하나 고민하게 되는 것은 상태 정보를 표현하는 State패턴 내의 클래스들에 대해 객체 생성 삭제를 해주는 방식에 대한 것이다. 가지 방법은 매번 객체를 생성했다가 불필요하면 삭제하는 것이고, 다른 방법은 미리 모든 상태 객체들을 생성해두고 삭제하지 않고 이용하는 방식이다.
    밖에 개별 상태 정보가 공유될 있는 경우에는 굳이 객체를 미리 생성해두거나 생성한 객체를 삭제할 필요 없이 [소스 24-1]에서 처럼 Singleton 패턴을 적용하는 것이 유용할 것이다.
    • STATE 패턴 정리
    State 패턴의 가장 장점은 새로운 상태의 추가 추가된 상태를 포함해서 상태 변화에 따른 행위 수행 변경이 손쉽게 이루어질 있다는 것이다. State 패턴이 이같은 장점을 가지는 이유는 크게 클래스 상속과 다형성에 기인한다. 새로운 상태 추가가 쉬운 것은 상태 정보가 클래스 상속 구조 형태로 정의되므로 하위 클래스를 추가하기만 하면 새로운 상태 추가가 가능하기 때문이며, 새로 상태가 추가되더라도 상태 변화에 따른 행위 변경이 쉬운 것은 새로 추가되는 하위 상태 클래스가 최상위의 상태 클래스가 가지고 있는 인터페이스를 자신의 상태에 맞추어 Overriding 하여 구현하기만 하면, 이를 이용하는 객체 입장에서는 별다른 수정 없이 다형성에 의해 원하는 작업을 수행할 있기 때문이다.
    State 패턴이 유용한 경우
    • 어떤 객체의 행위가 객체의 상태에 의존하는데, 변경되는 상태에 따라 실행 시간에 객체의 행위를 독립적으로 변경시켜야 경우 유용하다.
    • 객체의 연산들이 내부 상태 값에 따라 여러 조건문으로 분기되어 처리되는 부분이 많을 경우, 이런 부분이 여러 연산들에게서 반복적으로 나타날 경우 State 패턴을 적용하는 것이 유용하다.
    State패턴의 장단점과 다른 패턴들과의 관계
    • 객체 내부에서 상태 값을 비교하는 문장을 없앨 있게 해준다.
    • 특정 상태와 관련된 행위들을 하나의 객체로 모아주는 역할을 한다. 이를 통해 State 패턴을 동일한 상태에서 이루어지는 행위들을 국지화시켜 유지, 보수할 있게 해준다.
    • State패턴을 적용할 경우 객체의 상태 전환이 명백히 드러나진다. 왜냐하면 State 패턴의 경우 상태 전환이 일어나면 참조하는 객체가 바뀌기 때문이다. 반면 State 패턴을 적용하지 않는 경우에는 객체의 상태 변화가 내부 데이터 멤버 값의 변화로 표현되기 때문에 드러나지 않는다는 단점이 있다.
    • State 패턴은 상태 정보가 일관성을 가지게 만들어 준다. 만약 State 패턴을 적용하지 않고 데이터 멤버로 객체의 상태를 관리하게 되면 데이터 멤버 여러 개를 바꾸는 과정에서 일관성이 깨뜨려지는 현상이 발생할 있지만, State 패턴을 적용할 경우에는 상태의 전환이 객체 하나를 다른 객체로 변화시키는 연산으로 이루어지므로 이러한 문제를 방지할 있다.
    • State패턴에서 상태를 나타내는 클래스들이 내부적으로 데이터 멤버를 가지지 않는다면, 이들은 공유될 있다. 왜냐하면 객체 내부에 데이터 멤버가 없으면 객체 개가 항상 동일한 상태이므로 서로 구분할 필요가 없기 때문이다. 이처럼 상태를 표현하기 위한 클래스의 객체들이 공유될 있을 경우에는 Flyweight패턴이나 Singleton패턴을 적용해서 공유를 수행할 있다.
반응형