misc.log2006. 10. 9. 08:45

"지정문답 XXX"놀이. 나에게는 당연한 귀결인지는 몰라도 '프로그래밍'이란 주제가 떨어졌다.

 

최근 생각하는 『프로그래밍』
 화가는 그림으로 자신의 세계에 대한 생각을 표현하고, 소설가는 소설을 씀으로써 자신의 세계에 대한 생각을 표현한다. 마찬가지로, 프로그래머는 프로그래밍을 함으로써 자신의 세계에 대한 생각을 프로그램으로 표현한다.

 화가가 처음 그림을 배울 때는 직선 긋는 훈련, 원을 그리는 훈련, 색을 보는 훈련을 거치고, 음악가가 처음 음악을 배울 때도 음을 구별하는 법, 악기를 다루는 법, 악보를 읽는 법등의 기본기를 거친다. 그 후에는 기술의 유무보다는 저작자 자신의 아이디어와 개성이 최종 저작물의 질을 결정하게 된다. 프로그래밍도 마찬가지여서, 언어의 문법, 개발툴의 사용법, 기본라이브러리, 몇가지 기초 학문의 지식만 있다면, 그 뒤의 작업은 프로그래머 자신에게 달린 문제다. 지금 하고 있는 일만 해도, 5년전에 익힌 기술 이상의 기술을 필요로 하지를 않는다. (하긴, 엔진이 5년전 엔진이니) 새기술의 습득보다는 내 스타일의 확립이 중요하다고나 할까.

 

이 『프로그래밍』에는 감동

"프로그래밍"은 '프로그램을 짜는 작업'을 지칭하는 것이므로, 나로서는 다른 사람이 어떻게 작업을 하는지 알 수 없다. 어떻게 한 소설가가 다른 소설가가 타자기를 놀려 소설을 완성해 나가는 작업을 평가할 수 있겠는가.

다만, "프로그램"이라면 평가의 대상이 될 수 있다. 내가 지금까지 접해본 것중 가장 훌륭한 프로그램을 말하자면, MS엑셀이다. 효율성부터 설계의 일관성까지 어느 것 하나 놓치지 않은 정말 훌륭한 프로그램이다. 나중에 '조엘 온 소프트웨어'를 읽고 나서 속으로 "이런 훌륭한 사람이 참여해서 만든 프로그램이라 훌륭했구나"라는 생각을 했을 정도.

* 효율성 : 엑셀은, 조엘의 말을 빌자면 "번개같이 빠른" 프로그램이다. 방대한 양의 계산을 순식간에 처리해 낸다. 단순히 컴퓨터니까 계산을 빨리 한다는 의미가 아니다. 같은 일을 하는 다른 프로그램에 비했을 때 속도가 월등히 빠르다. 계산 뿐만 아니라, 사용자 인터페이스도 번개같이 빠르다. 아래한글이나 MS워드에서 표를 만들고 표 구분선을 마우스로 드래그해서 움직일 때의 반응성과, 엑셀의 셀 구분선을 드래그해서 움직일 때의 반응성을 비교해보라. 엑셀은 정말 끔찍히도 효율적인 프로그램이다.

* 설계의 일관성 : 엑셀 매크로를 써 본 적이 있는가. 사실 매크로의 기본은 작업 내용을 기록해서 나중에 다시 반복하게 해 주는 기능이다. 그런데 MS에서는 이러한 기본 기능을 확장하고 추상화하여 '작업 내용'을 객체로 간주하고 이를 비주얼 베이직의 문법으로 접근할 방법을 마련했다. 이것이 VBA, visual basic for application 이고, 엑셀에는 당연히 엑셀VBA가 매크로로 탑재되어 있다. 엑셀의 모든 구성요소가 객체 단위로 일관성 있게 구성되어 있기 때문에 가능한 기능이다. 그러기에 엑셀VBA를 이용하면 엑셀을 거의 완전히 내 손에 맞는 전용 툴로 개조할 수 있다. 더군다나 어떤 매크로가 포함된 문서를 여느냐에 따라 외양과 기능이 완전히 달라지는 유연한 전용 툴이 되는 것이다.

보통 프로그램은, 유연하면 속도가 느리게 마련이고, 속도를 빠르게 하려면 유연성이 떨어지게 마련인데, 엑셀은 그 둘을 다 충족시켜 버렸다. 감동적이다.

 

직감적 『프로그래밍』, 좋아하는 『프로그래밍』, 그리고 싫어하는 『프로그래밍』
기획안에서 머릿속에 프로젝트의 모든 구성요소의 설계가 이루어질 때가 있다. 일관성 있게 잘 짜여진 기획안이라면, 코드의 설계도 일관성 있게 잘 짜여지게 된다. 심지어는, 어서 설계를 그려놓고 이 설계에 맞는 코드를 써 넣고 싶어서 안달이 날 지경.

그러한 설계를 구현할 때에는 손가락이 키보드 위를 달리는 느낌, 혹은 손가락과 키보드가 일체가 되는 듯한 느낌, 혹은 손가락이 키보드에게 말하고 있는 듯한 느낌, 혹은 코드가, 전체 설계가, 전체 시스템이, 하나의 세계가 손가락 끝에서 뿜어져 나오고 있는 듯한 느낌.

분명, 이 세계를 신이란 존재가 만들었다면, 분명 신도 그러한 일관성있는 짜임새를 목표로 하고 세계를 만들면서 즐거워 했을 거라고 생각한다.

 

반면, 일관성이 떨어지거나 전혀 종잡을 수 없는 설계로 작업할 수 밖에 없는 상황도 분명 존재한다. 그럴 때는 코드 한줄 한줄 을 쓸 때마다 하품이 나오고 따분하다. 말할 것도 없이 최악의 상황.

 

세계에 『프로그래밍』이 없었다면

프로그래밍이 존재하지 않는다는 말은, 프로그래머나 프로그램 역시 존재하지 않는다는 것이고, 이는 곧 컴퓨터가 존재하지 않는 세상을 암시한다. 그럼 인류는 달에도 가지 못했을 것이고, 원자력 발전소도 수시로 폭발했을 테고, 인간 게놈도 밝혀내지 못했을 테지. 그리고 아마 사람들은 컴퓨터를 붙잡고 있는 대신 친근한 사람들과 조금이라도 더 많은 시간을 보냈을 지도 모른다. (하지만 아마 컴퓨터 대신 TV를 붙잡고 있을 공산이 크다고 생각한다)

나? 컴퓨터가 없는 세상이었다면, 아마 어렸을 적, 컴퓨터를 몰랐던 때의 장래 희망처럼 이론물리학을 하거나... 도서관 사서, 그런 것을 하게 되지 않았을 까 싶다. 더 나은 것인지 아닌지는 모르겠다. 지금도 그리 나쁘진 않으니까.

 

바톤을 받는 5명 (지정과 함께)

- 징 : 무협

- 미러 : 하루키

(더이상 이 블로그에 오는 사람이 없음. 생략)

 

Posted by uhm
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
dev.log2006. 9. 16. 05:18

생물이 시간에 따라 진화한다고는 하지만, 코드 역시 시간에 따라 진화한다. 물론, '진화'가 '진보'를 의미하지는 않으므로, 코드 역시 '퇴보'할 수 있다. 특히 잠깐 신경을 쓰지 않고 코드를 쓰다 보면, 디자인이 퇴보하는 것은 어렵지 않게 볼 수 있다. (열역학제2법칙을 떠올려 보자)

어떤 개발자가 음악을 플레이해볼 수 있는 간단한 툴을 만들고자 했다. 필요한 기능은 그냥 음악을 '틀어보고' '꺼보는' 기능이다. 그래서 다음과 같이 디자인했다.

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

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

protected:
    void* data;
    bool playing;
};

 

동작은 간단하다.

1) play()멤버를 호출하면 name으로 data를 만들어내고, playing 멤버를 true로 바꾼다,

2) stop()멤버는 playing멤버를 false로 바꾸고 data를 지운다. 

3) update()은 playing이 true일 때만 data를 읽어서 재생을 계속한다.정말 간단하다.

 

잘 써먹고 있다가 보니 '일시정지'와 '계속재생'이 필요해졌다. 그래서 '일시정지' 기능을 다음과 같이 추가했다.

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

    vod update();
    void play();
    void stop();
    void resume();
    void pause();

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

동작은 아까와 크게 다름없다.

1) play(), stop()은 똑같다

2) pause()를 부르면, paused멤버를 true로 세팅한다.

3) resume()을 부르면, paused멤버를 false로 세팅한다.

4) update()에서는, paused가 false, playing 멤버가 true일 때만 재생을 계속한다.

 

이렇게 잘 써먹고 있다가 어느 기획자가 오더니 "페이드-인, 페이드-아웃이 반드시 있어야 되는데요"라고 말하고 가버린다. 그래서 이 개발자는 다음과 같이 페이드-인/아웃을 추가했다.

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

    vod update();
    void play();
    void stop();
    void resume();
    void pause();
    void fadein( float duration );
    void fadeout( float duration );

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

    int fade_mode;
    float fade_timer;
    float fade_duration;
};

이제 슬슬 골치가 아파오기 시작한다.

update()에서는, playing이 true, paused가 false일 때만 업데이트를 계속하되, fade_mode를 보면서 시간에 따라 볼륨을 조절해야 한다. 대략 다음과 같이 된다.

void music::update()
{
    if ( playing )
    {
         if ( !paused )
         {
             switch ( fade_mode )
             {
             case FADE_IN:    increase_volume(); break;
             case FADE_OUT: decrease_volume(); break;
             default: continue_playing();
             }
         }
    }
}

여기가 끝이 아니다. 어느날 어떤 기획자가 와서 말한다; "페이드-아웃할 때 음악을 끄지 말고 그냥 일시정지상태로도 할수 있게 해주세요"라고. 또 다른 기획자가 와서는 "3초간 대기한 후에 페이드-인 하면서 시작할 수 있게 해주세요. 아, 대기시간3초는 바뀔 수도 있어요"라고 말하고 가버린다. 그리고 기타 등등등.. 결과는? 걸레가 된 코드다.

 

제발 이 디자인에 문제가 없다고 생각하는 사람이 없기를 빈다. 구멍이 숭숭 뚫려있는 것이 눈에 보인다.

1) 이 객체가 처할 수 있는 모든 가능한 상태에 대한 고려가 없다 : playing이 false이거나, paused가 true일 때의 처리가 명시적이지 않다. 물론 지금이야 코드량이 워낙 적으니 명확하게 알아볼 수 있지만, 코드가 길어지면 명시적이지 않다는 것은, 객체가 알 수 없는 행동을 할 것이라고 봐야 한다. 두더지 게임기의 두더지처럼 버그를 하나 고치면 또 다른 버그가 끝도 없이 튀어나올 것이다 -_-

2) 이 객체에 다른 기능이 추가될 경우엔 구조가 바뀌어야 한다 : 만약 여기에 또 다른 기능, 이를테면 '2배속재생'이라든지, '거꾸로재생' 같은 기능이 추가되어야 한다면 또다시 if의 끝없는 중첩으로 해결할 속셈인 것이다, 이 개발자는. 프로그램의 구조는, 앞으로 발생 가능한 모든 사태를 포용할 수 있는 범용성을 갖추어야 한다. 기능이 추가될때 구조가 바뀌어야 하는 디자인은, 결코 디자인이라고 할 수 없다. 집안에 에어콘을 놓겠다고 벽을 허무는 셈이다. -_-

 

이렇게 짜는 사람이 있을 거라고 생각되지 않을지도 모르지만, 놀랍게도, 이 디자인은 회사에서 쓰고 있는 엔진에 그대로 들어있다. 수십만달러를 받아먹고 (내 어렴풋한 기억에는 50만달러정도였던거 같다) 파는 엔진에도 이런 디자인이 있는 걸 보면, 프로그래머가 밥벌어먹고 살 구석은 아직도 많은 거 같기도 하다 -_-;;


Posted by uhm