본문 바로가기

study/design pattern

[디자인패턴][행위패턴] 상태 State - C++

728x90

[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.


State Pattern

상태에 의해 행동이 결정된다. 상태는 바뀔 수 있고, 상태를 전환시켜 주는 것을 트리거라 한다.

상태 디자인 패턴은 "상태가 동작을 제어하고, 상태는 바뀔 수 있다"는 단순한 아이디어다. 상태 변경 주체가 누구인지 규정하지 않는다.

상태 디자인 패턴은 보통 두 가지 방법이 있다. 실제로는 두 번째 방법이 더 흔히 사용된다. 첫 번째 방법은 일반적인 사고방식과 색다른 면이 있다.

  • 동작을 가지는 실제 클래스로 상태를 정의한다. 상태가 이전될 때 클래스의 변화에 따라 변경된다.
  • 상태와 상태 전이를 단순히 enum 타입처럼 식별자의 나열로 정의한다. 실제 상태 변화는 상태 머신이라는 특별한 컴포넌트를 두어 수행한다.

상태 기반 상태 전이

가장 쉬운 예인 전등을 살펴보자. 전등은 꺼짐과 켜짐 두 상태가 있다. 아무 상태로든 전이할 수 있도록 할 것이다.

여기서 상태 디자인 패턴의 전통적 구현 방식을 사용하지만, 권장하는 방법은 아님에 유의하자.

struct State {
    virtual void on(LightSwitch *ls) {
        cout << "Light is already on\n";
    }
    
    virtual void off(LightSwitch *ls) {
        cout << "Light is already off\n";
    }
};

class LightSwictch {
public:
    LightSwitch() {
        state = new OffState();
    }
    
    void set_state(State *state) {
        this->state = state;
    }
    
private:
    State *state;
};

이 구현은 직관적이지는 않다. 전등을 켜고 끄는 시나리오에서 상태를 별도의 클래스로 정의하는 것 자체가 상식적이지는 않다. 문제점을 꼽아보자.

첫 번째로 State가 추상 타입이 아니다.

두 번째로 State 자체적으로 상태 전이를 할 수 잇도록 하고 있다. 이것은 전이에 대한 상식적인 개념과 거리가 멀다.

세 번째로 State::on/off 동작은 이미 그 상태에 있다는 것을 가정하고 있다. 이는 직관적인 것을 떠나 가장 큰 문제다.

 

이제 on/off에 해당하는  State를 정해보자. 상태 스스로 상태 전환을 할 수 있게 하고 있다.

struct OnState: State {
    OnState() {
        cout << "Light tunred on\n";
    }
    
    void off(LightSwitch *ls) override {
        cout << "Switching light off...\n";
        ls->set_state(new OffState());
        delete this;
    }
};

struct OffState: State {
    OffState() {
        cout << "Light tunred off\n";
    }
    
    void on(LightSwitch *ls) override {
        cout << "Switching light on...\n";
        ls->set_state(new OnState());
        delete this;
    }
}

여기서 C++에서 일반적으로 보기 힘든 delete this; 구문을 갖는다. 이 구문은 해당 객체가 이미 생성 완료되었다는 위험한 가정을 두고 있다. 이 부분을 스마트 포인터로 사용할 수는 있겠지만 포인터의 사용과 힙 메모리 할당이 일어난다는 것 자체가 직접적 상태 소멸이 일어남을 확실히 보여준다.

 

전등 스위치를 통해 상태 전환도 가능해야 한다.

class LightSwitch {
    ...
    void on() {
        state->on(this);
    }
    
    void off() {
        state->off(this);
    }
};

아무튼 위의 접근 방식은 직관적이지 않아 좋지 않다. 목적하는 상태를 알려주면 그 상태로 바뀌는 것이 더 직관적이다. 상태가 상태를 바꾼다는 접근 방식은 전통적이긴 하지만 직관적이지 않다.


수작업으로 만드는 상태 머신

구식 전화기 상태머신을 정의해보자. 전화기의 상태들을 나열해 보고, 상태 간의 전이를 정의해 보자. enum class를 이용해 정의한다.

enum class State {
    off_hook, 		//< 수화기 든 상태
    connecting, 	//< 연결 시도 상태
    connected, 		//< 연결된 상태
    on_hold, 		//< 대기 상태
    on_hook 		//< 수화기 내린 상태
};

enum class Trigger {
    call_dialed, 		//< 전화 걸기
    hung_up, 			//< 전화 끊기
    call_connected, 	//< 전화 연결됨
    placed_on_hold, 	//< 대기
    take_off_hold, 		//< 대기 종료
    left_message, 		//< 메시지 남기기
    stop_using_phone 	//< 전화 사용 종료
};

상태 머신에서 상태 전이가 어떤 규칙으로 이루어져야 하는지에 대한 저보는 어딘가에 저장되어야 한다. 여기선 map을 이용한다. map의 키는 상태 전이 출발 상태이고, 값은 트리거와 도착 상태 쌍들의 집합이다.

map<State, vector<pair<Trigger, State>>> rules;

 

전이 규칙 말고 시작 상태와 종료 상태도 필요할 것이다. 이러한 준비를 기반으로 하면 상태 머신의 구동에 별도 컴포넌트를 만들지 않아도 된다. 전체적으로 관리 통제할 모듈이 필요하다.


Boost.MSM을 이용한 상태 머신

실 세계의 상태 머신은 훨씬 더 복잡하다. 특정 상태에 도달했을 대 어떤 동작을 수행해야 하는 경우도 있다. 조건부로 상태 전이를 해야 할 수도 있다. 어떤 상황에 대한 조건이 합치되어야만 특정 상태로의 전이가 수행되도록 통제해야 할 수도 있는 것이다.

Boost.MSM(Meta State Machine)은 Boost의 상태 머신 라이브러리로, CRTP 형태로 state_machine_def를 상속받아 사용한다.

CRTP 참고

struct PhoneStateMachine : state_machine_def<PhoneStateMachine> {
    bool angry(false);
    ...
};

angry 멤버 변수는 전화를 건 사람이 화가 나서 응답할 수 없는 상황을 표시하기 위한 것이다. 각 상태는 state 클래스를 상속받아야만 상태 머신 안에 들어갈 수 있다.

struct OffHook: state<> {};

struct Connectiong : state<> {
    template <class Event, class FSM>
    void on_entry (Entry const &evt, FSM &) {
        cout << "We are connecting ..." << endl;
    }
    
    // on_exit 생략
};

// 다른 상태 생략

특정 상태 전이를 하는 중에 수행되어야 하는 동작도 정의할 수 있다. 특별히 상속받아야 할 클래스도 없다. 특정 시그니처를 가지는 operator()를 구현해야 한다.

struct PhoneBeingDestroyed {
    template <class EVT, class FSM, class SourceState, class TargetState>
    void operator() (EVT const &, FSM &, SourceState &, TargetState &) {
        cout << "Phone breaks into a million pieces" << endl;
    }
};

마지막으로 보호 조건이 있따. 보호 조건은 상태 전이를 수행하기 전에 유효한 전이인지 아닌지 검사하는 것으로 생각할 수 있다. 이 변수를 보호 조건으로서 MSM에서 사용하기 위해서는 클래스와 operator()로 감싸야한다.

struct CanDestroyPhone {
    template <class EVT, class FSM, class SourceState, class TargetState>
    void operator() (EVT const &, FSM &, SourceState &, TargetState &) {
        return fsm.angry;
    }
};

여기서는 규칙의 정의하기 위해 Boost.MSM은 MPL(메타 프로그래밍 라이브러리)을 사용한다. 그중 mpl;::vector를 이용할 것이다.

뒤의 내용들은 생략했다.


요약

Boost에서 두 종류의 상태 머신 라이브러리를 지원한다는 것을 알아두자. 하나는 여기서 살펴본 Boost.MSM이고 다른 하나는 Boost.Statechart이다. Boost가 아니더라도 많은 상태 머신 라이브러리가 있을 것이다.

두 번째로 단순한 상태 머신 이상의 고차원 상태 머신들이 많이 있다는 것을 알아두자.

마지막으로 현대의 상태 머신은 과거의 전통적 상태 머신 디자인 패턴과 비교해 훨씬 진보되어 있다는 것을 알아두자.

728x90