dev.log2004. 1. 30. 02:44
스테이트 머신은 복잡한 계의 행동을 모델링하는 간단한 기법으로 널리 애용되곤 한다. 스테이트 머신에 대한 자세한 설명은 여기서는 하지 않겠지만, 상태에 따라 외부의 자극에 다르게 반응하는 계를 모델링하는 기법이라고 설명하면 될듯 하다. 스테이트 머신에는 여러 종류가 있지만, 여기서는 가장 간단한 Deterministic Finite State Automata만을 고려하기로 한다.
오늘 누군가가 스테이트 머신 클래스를 만든 코드라고 한 것을 본 적이 있다. 대략적으로 다음과 같은 형상이었다.
void state_machine::handler()
{
    int state = INITIAL_STATE;
    while ( !is_final( state ) )
    {
        int s = fetch_stimuli();
        switch ( state )
        {
        case STATE0:
            state = on_state0( s );
            break;
        case STATE1:
            state = on_state1( s );
            break;
        // 다른 스테이트를 처리하는 케이스
        // ....
        default:
            state = INVALID_STATE;
        }
    }
}
int state_machine::on_state0( int s )
{
    switch ( s )
    {
    case STIMULI0:
        // 스테이트0에서 입력0를 처리하는 코드
        // ....
        return SOME_STATE;
    case STIMULI1:
        // 스테이트1에서 입력1을 처리하는 코드
        // ....
        return ANOTHER_STATE:
    // 다른 입력을 처리하는 케이스
    // ....
    default:
        return STATE0;
    }
}


이건 클래스만 썼을 뿐이지 C코드 그대로의 설계방식이다. 전혀 객체지향적이지 않다. 문제점이 보이는가?
어떤 스테이트 머신이 있을때, 그 머신의 스테이트0와 스테이트1에서의 행동은 많든 적든 다른 점이 있기 마련이다. 객체지향의 세계에서는 다른 일을 하는 개체는 다른 클래스로 만들어야 한다. 구현하고자 하는 기능을 그냥 <클래스에 때려박는다>고 해서 객체지향이 되는 것이 아니라는 것이다.

혹시 디자인 패턴 쪽을 공부해 보지 않은 분이 있다면 객체지향 설계 방법론에서 디자인 패턴은 한번 봐둘만한 가치는 있음을 말하고 싶다. 여기에서 나타나는 것이 state pattern, 혹은 strategy pattern이다. 스테이트 패턴은 어떤 객체의 스테이트를 하나의 객체로 모델링하는 기법이다.
위 스테이트 머신을 스테이트 패턴을 써서 고친다면 다음과 같이 변할 것이다.

우선, 객체의 스테이트를 나타내는 state 추상 클래스를 선언한다.

class state
{
public:
    virtual state* handle_stimuli( int s ) = 0;
};


이제 각 스테이트를 표현하는 클래스를 선언한다.
class state0 : public state
{
protected:
    state* handle_stimuli( int s )
    {
        switch ( s )
        {
        case STIMULI0:
            return handle_stimuli0();
        case STIMULI1:
            return handle_stimuli1();
        // 다른 입력을 처리하는 케이스
        // ....
        default:
            return new state0();
        }
    }
};


이제 메인 루프에서는 선언된 추상클래스의 인터페이스만 호출하면 된다.

void state_machine::handler()
{
    state* cur_state = new initial_state();
    while ( !is_final( state ) )
    {
        int s = fetch_stimuli();
        state* new_state = cur_state->handle_stimuli( s );
        delete cur_state;
        cur_state = new_state;
    }
    delete cur_state;
}


혹 동적 메모리의 할당과 해제가 빈번히 일어나는 것이 신경쓰인다면, 각 상태 객체를 미리 스테이트 머신 객체에 등록시켜 놓고 빠르게 접근하는 방법을 생각해도 되겠다. 이런 경우에는 Prototype패턴을 응용해 보면 좋을 것이다. 혹은, placement new 연산자를 오버로딩하여 사용하는 방법을 써도 좋다.

구현상의 개선이야 그렇다 치고, 이제는 설계상의 개선을 한번 찾아보자. 위 설계에 개선의 여지가 없는가? 당연히 있다. 문제는 스티뮬리를 처리하는 handle_stimuli()메소드의 구현 방식에 있다. 여기서는 스티뮬리의 값이 따라서 객체의 행동을 선택해주는 방식을 취하고 있다. 그렇다면 행동을 취하는 정보는 state객체에 있는 것이 아니라 스티뮬리에 있는 것이 된다. 그렇다면 행동의 선택은 이미 스티뮬리에 따라 주어진 것인데, 구태여 state객체가 스티뮬리에 따르는 행동의 선택에 관여할 필요는 없다는 결론이 나온다.
즉, 스테이트 객체가 스티뮬리를 처리하는 방식은 스티뮬리에 따라 결정되므로, 스티뮬리가 자기 자신을 처리하는 방식을 알고 있는 것이 합리적인 설계이다. 즉, 스티뮬리를 객체로 모델링하는 것이 정답이다. 여기에는 Command패턴을 쓰는 것이 적절하다.

우선 스티뮬리를 나타낼 추상클래스 stimuli를 선언한다.
class stimuli
{
public:
    virtual state* handle( state* s ) = 0;
};


이제 각 스티뮬리를 표현하는 클래스를 작성한다.
class stimuli0 : public stimuli
{
protected:
    state* handle( state* s )
    {
        return s->handle_stimuli0();
    }
};


그럼 이제 state객체의 선언이 바뀌게 된다.

class state0 : public state
{
protected:
    state* handle_stimuli( stimuli& s )
    {
        return s.handle( this );
    }
};


그러면 메인루프는 다음과 같아진다.

void state_machine::handler()
{
    state* cur_state = new initial_state();
    while ( !is_final( state ) )
    {
        stimuli& s = fetch_stimuli();
        state* new_state = cur_state->handle_stimuli(s);
        delete cur_state;
        cur_state = new_state;
    }
    delete cur_state;
}

처음의 코드와 가장 나중의 코드를 비교해 보면, 어느 쪽이 더 깔끔한지 판단해 보시기 바란다.

Posted by uhm