'디자인패턴'에 해당되는 글 1건

  1. 2006.09.19 디자인 개선
dev.log2006. 9. 19. 09:47

설계의 가장 기본은 추상화이다. 이놈과 저놈이 다른게 무엇이고 공통점이 무엇인지를 파악해서 공통점을 묶어서 추상개념으로 승화시켜야 하는 법이다.

앞에서 든 음악 재생의 예는, 추상화를 제대로 하지 못해서 실패한 디자인이다. '재생'과 '정지'를 음악 재생의 한 과정으로서가 아니라, 추상적 실체로 바라보았다면 훨씬 나은 디자인이 되었을 것이다.

단지 음악을 '재생'하고 '정지'하는 기능만 필요한 상황에서는 애초의 설계가 그리 나쁜것은 아니다. 간단한 기능에는 간단한 설계. 이런 간단한 상황에서 Overkill은 금물이다.

 

class music
{
public:
    music();
    ~music();

    void update();
    void play( char* name );
    void stop();

protected:
    void* data;
    bool playing;
};

 

void music::update()
{
    if ( playing )
        continue_playing( data );
}


이제 여기에 '일시정지'와 '계속재생'이 추가된다. 프로젝트에서 이 부분을 맡은 개발자는 다음과 같이 기능을 추가했다.

class music
{
public:
    music();
    ~music();

    void update();
    void play( char* name );
    void stop();
    void pause();
    void resume();

protected:
    void* data;
    bool playing;
    bool paused;
};

void music::update()
{
    if ( playing )
        if ( !paused )
            continue_playing( data );
}

이 순간이 결정적 순간이다. 이 개발자는 이 기능을 추가하는 순간에 한번 고민을 해 봤어야 한다. data는 항상 쓰는 자원이니까 별도로 치면, 이 객체가 처할 수 있는 상태는 4가지이다.

A : playing is true, paused is false

B : playing is true, paused is true

C : playing is false, paused is false

D : playing is false, paused is true

여기에서, 상태A는, 재생이 시작되었고, 일시정지되지 않은 상황이다. 그냥 '재생'되고 있는 상황. 상태 B는, 일단 재생이 시작되었다가 일시정지된 경우이다. 우리가 일상적으로 생각하는 '일시정지'의 개념과 매우 잘 부합한다. 상태C는 재생되고 있지도 않고, 일시정지 되지도 않은 상황이다. 즉, 그냥 '정지'.

문제는 상태 D이다. 객체가 이 상태에 있을 때는 정의되지도 않았고, 우리의 일상적인 개념과 잘 부합하지도 않는다. 어떻게, 음악이, 재생하지도 않았는데, 일시정지될수 있단 말인가.

 

여기에서의 적합한 추상화는, 음악을 상태기계로 보고, '재생', '일시정지', '정지'의 3가지 상태를 만들어 주는 것이다.

class music
{
public:
    music();
    ~music();

    void update();
    void play( char* name );
    void stop();
    void pause();
    void resume();

protected:
    void* data;
    int state;
};

void music::update()
{
    switch ( state )
    {
    case STATE_STOPPED:  do_nothing(); break;
    case STATE_PLAYING:   continue_playing(); break;
    case STATE_PAUSED:    do_nothing(); break;
    default: stop();
    }
}

 

일단 지금은, 보기 흉한 switch 같은 것은 무시하기로 하자. 좀 더 명확해 진 것으로 만족해 보자. 이제 play(), stop(), pause(), resume(), update() ('시간의 경과'역시 입력의 하나로 취급할 수 있다) 등의 함수는 사실은 music이라는 객체의 상태를 바꿔주는 상태전이 입력으로 파악할 수 있는 것이다. 또한 객체가 알 수 없는 상태에 빠졌을 때에도 명시적으로 검증할 수 있는 상태로 편리하게 되돌릴 수도 있는 것이다. (위의 default 케이스)

 

이제 여기에 페이드-인/아웃을 추가해 보자. 이제 간단히 FI/FO상태를 추가하기만 하면 된다.

void music::update()
{
    switch ( state )
    {
    case STATE_STOPPED:  do_nothing(); break;
    case STATE_PLAYING:   continue_playing(); break;
    case STATE_PAUSED:    do_nothing(); break;
    case STATE_FADEIN:      increase_volume(); break;
    case STATE_FADEOUT:   decrease_volume(); break;
    default: stop();
    }
}

이걸로 끝일까? 위에서 update()역시 '시간의 경과'를 나타내는 상태전이 입력의 하나로 볼수 있다고 했다. 따라서 상태가 하나 추가된다면, 다른 모든 입력에 대해서 이 상태가 어떻게 행동해야 하나를 고려해야 한다. 따라서 다음과 같이 고려해야 한다.

void music::play( char* name )
{
    switch ( state )
    {
    case STATE_STOPPED:  read_data(); state = STATE_PLAYING; break;
    case STATE_PLAYING:   break;
    case STATE_PAUSED:    state = STATE_PLAYING; break;
    case STATE_FADEIN:      break;
    case STATE_FADEOUT:   restore_volume(); state = STATE_PLAYIN; break;
    default: stop();
    }
}

void music::stop()
{ ............ }

void music::pause()
{ ............ }

......................

 

'상태'라는 추상 개념을 도입함으로써, 기능이 추가될 때 무엇을 해야 하는지가 명확해 졌다.

이제 문제는 도처에 널부러져 있는 보기 흉한 switch-case이다. 상태가 하나 추가될 때마다 모든 가능한 입력에 대해 행동을 정의해 줘야 하는데, 하나의 상태가 하는 행동이 여러군데에 흩어져 있는 것이다. "Fade-in 할때 이러저러할 걸 바꿔주세요"라는 요청이 있을 때마다, update()에서 한줄 고치고, play()에서 한줄 고치고, pause()에서 또 한줄 고치는 일을 마냥 하다보면, 마우스 휠이 닳아서 못쓰게 될 지경이 될 것이다.

이때에 생각할 수 있는 것은 여러가지이지만 내가 적합하다고 생각하는 쪽은 State 패턴(GoF, p305)이다. 트랙백의 덧글에서처럼 MVC패턴을 적용할 수도 있지만, MVC패턴은 원래 다중 View를 생각해야 할 때 적합한 패턴이고, 음악 재생의 예는 View보다는 객체 자체의 행동을 캡슐화해야 하는 상황인데, 이때에는 State패턴이 보다 적합하리라는 것이 나의 생각이다.

State 패턴을 적용하면, 대략 다음과 같아진다.

class music
{
public:
    music();
    ~music();

    void update();
    void play( char* name );
    void stop();
    void pause();
    void resume();

protected:
    void* data;
    struct state* cur_state;
    struct state
    {
        music* context;
        state( music* m ) : context( m ) {}
        virtual void handle_play( char* name );
        virtual void handle_stop();
        virtual void handle_pause();
        virtual void handle_resume();
        virtual void handle_fadein();
        virtual void handle_fadeout();
        virtual void handle_update();
    };

    struct state_playing : public state
    { .......... };

    struct state_stopped : public state
    { .......... };

    struct state_paused : public state
    { .......... };

    struct state_fadein : public state
    { .......... };

    struct state_fadeout : public state
    { .......... };

};

 

그럼 update()와 다른 함수들은 대략 다음과 같이, 현재 상태로 입력을 포워딩하는 구조로 바뀐다.

void music::update()
{
    cur_state->handle_update();
}

 

void music::play( char* name )
{
    cur_state->handle_play();
}

 

이제부터는 기능이 추가되어야 한다면, 적절한 상태로 추상화 한 후, 상태 객체를 만들어서 끼워 넣는 식으로 디자인 변경이 쉬워진다. 또한 앞으로의 변경사항에 어떻게 대처해야 하는지, 현재 구조를 바꾸지 않고도 기능을 추가할 수 있는 방향을 제시할 수 있다는 점에서 더 나아진 디자인이다.

잘된 디자인이란, 모든 상상 가능한 기능을 구현할 수 있는 '틀'이 아니다. (그러한 것은 존재하지 않는다) 오히려 전혀 예상치 못한 요구가 발생했을 때에, 그 요구를 해결하기 위해 디자인이 어떻게 바뀌어야 하는가에 대한 방향을 제시할 수 있는 디자인이어야 한다고, 나는 생각한다.

 

Posted by uhm