dev.log2006. 11. 4. 23:37

그렇다, 구현은 늘상 변경되기마련이다. "foo.bar"파일을 fopen으로 파일을 열었다가도 어느 순간인가는 CreateFile로 바뀔수도 있고, 한참 지나다보면 또 std::fstream 객체를 만들어야 할 때도 오는 법이다.

얼마전 회사에서 있었던 일 하나.

거대한 테이블을 파일에서 참조해야 하는 상황이다. 대략 평균 60메가 정도의 파일이 80개 가량. 결국 4.8기가의 테이블을 참조해야 하는 것인데, 이만한 크기의 테이블을 메모리에 올릴 수는 없는 노릇이다. 아무리 요새 메모리가 썪어 난다지만, 단일 테이블을 4.8기가나 메모리에 올리는 것은 미친짓이라고 할수밖에;

그래서 내 손에 들어온 애초의 구현은 윈도우의 경우는 MapViewOfFile 함수로 메모리에 파일포인터를 매핑하여 접근하는 것이었고, 리눅스에서는 mmap 함수로 동일한 일을 하고 있었다. 대략적으로, 다음과 같은 식이다.

 

void Table::init( ... )
{
    short* table = MapViewOfFile( ... );
    // short* table = mmap( ... ); // in case of linux
    // ...
}

void certain_function()
{
    short value = table[index];
    // ...
}

void another_function( ... )
{
    short another_value = table[another_index];
    // ...
}

void yet_another_function( ... )
{
    short yet_another_value = table[yet_another_index];
    // ....
}

// goes on and on .....

코드를 보면 뭔가 불안하다. 일단, table 포인터를 얻어오는 과정이 플랫폼마다 다른데, 이걸 이렇게 흩어진 코드에서 마구잡이로 접근한다면 문제가 생길 것이 뻔하지 않은가. 그래서 코드를 받자마자 테이블을 참조하는 작업을 별도의 인터페이스로 빼서 구현을 따로 하였다. 조금 안심이 된다. 구현과 인터페이스를 분리한 것이다.

void Table::init(  ... )
{
   buffer = MapViewOfFile( ... );
}

short Table::lookup(short index)
{
    return buffer[index];
}

void certain_function()
{
    short value = table.lookup(index);
    // ...
}

void another_function( ... )
{
    short another_value = table.lookup(another_index);
    // ...
}

void yet_another_function( ... )
{
    short yet_another_value = table.lookup(yet_another_index);
    // ....
}

 

문제는 윈도우에서 터졌는데, 이놈의 MapViewOfFile 함수가, 파일을 주소공간에 매핑하고 포인터를 얻어와야 하는데, 메모리가 부족하여 매핑을 못하는 사태가 벌어진 것이다. (사용메모리양 2기가) 유력한 용의자는 당연히 MapViewOfFile 함수. 이 함수가 파일을 통째로 메모리에 올릴 것이라는 혐의가 잡혔다. 리눅스에서는 문제가 안 터졌는데, 아마도 64bit 커널을 쓰고 있어서 주소공간이 넉넉하여 별 문제가 발생하지 않은듯 싶다.

 

그래서 메모리맵을 쓰는 대신, 테이블의 개별 레코드를 일정개수씩 캐쉬로 유지하면서 참조하는 전략을 택하기로 했다. 그 전의 구현이었으면 수십개의 함수에 흩어져있는, 테이블 참조 코드를 일일이 고쳐야 했겠지만, 다행히도 그 전에 구현과 인터페이스를 분리해 두었으므로, 참조 인터페이스는 그대로 두고 구현만 고치면 되는 문제였다. Table::init과 Table::lookup 함수, 단 두개.

 

결과는?

"메모리 뻑 고치는데 한 3일이면 될까요?"

"네.. 해보죠 뭐."

구현만 뚝딱뚝딱 고친다. 30분 뒤....

"다됐음. 받으셈"

"브라보! 3일걸린다면서요."

"훗훗훗"

 

구현은 항상 변경된다. 그러므로 구현과 인터페이스는 분리되어야 하는 것이다.

그럼 당신도 인정받는 프로그래머.

 

PS : 혹은, 3일동안 할일을 30분만에 끝내놓고 이틀동안 빈둥빈둥 노는 것도 추천;

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
geek.log2005. 12. 20. 00:51

프로그래머,
특히 C/C++ 프로그래머로 오랫동안 작업을 하면 -_-
특정 영타만 유독 빨라지곤 한다.
뭐 빨라지는건 좋은데 동작이 완전 굳어져서 의도치 않은 오타를 낼 떄가 많다.

이를테면,
in the city는
빨리 치다 보면 거의 예외없이
int the city 가 된다 -_-

initial 같은 단어도
intial 로 치기 일쑤다.

거기에 더하여
gift for him 같은 어구는 거의 예외없이
gift for( him 이 되곤 한다.

제길 -_-


Posted by uhm
misc.log2005. 11. 19. 17:08

MS에서 Visual Studio Express Edition을 무료로 배포하고 있다.

1년간 공짜이고, 상업적 용도로도 사용할 수 있으며, 1년이 지난 후에도 계속 무료로 사용할 수 있다고 한다. 물론, Visual Studio 2005과 비교하면 몇가지 기능은 빠져 있다고 하는데, 설치하려면 서비스팩2를 깔아야 한다고 해서 아직 뭐가 빠져 있는지는 알아보질 못했다.

뭐라할까, 딱, 이런 느낌이다
"정말 돈지랄을 해대는구나"

사실, MSVS계열은 개발 툴 시장에서 상당한 우위를 차지하고 있지만, 이렇게 돈지랄을 해대는 이유는, MSVS의 최대 경쟁자는 MSVS이기 때문이다 -_-

사실 대부분의 학생, 개발자, 개발자 지망생들은 아직도 손에 익은 개발툴로 VC6.0을 꼽고 있고, VS.net은 무겁거나, 혹은 인터페이스가 다르다는 이유로 전환을 고려하지도 않고 있는듯 하다.

학생들이 스킬을 익히는 학창시절에, 조교들은 단지 자기들 손에 익었다는 이유만으로 VC6.0으로 과제를 요구하곤 한다. 그래서 결국 학생들도 VC6.0이 손에 익게 되고, 실무에 나가서도 VC6.0을 고집한다. 또한 그 학생들중 일부는 다시 조교가 되어 VC6.0 유저그룹의 확대 재생산에 기여하게 되는 것이다.

결국 내부의 적이 가장 무섭다고 했던가.
새로 나온 VS2005의 가장 중요한 경쟁상대가 VC6.0인 웃지못할 상황이 된 것이다. 왜냐하면, VC6.0을 쓰고 있는 개발자들은 더이상 MS에게는 수익원이 될 수 없으므로.
단지 업글 수요를 만들기 위해 저런 돈지랄을 해댈 수 있는 MS란 회사가 무섭기까지 하다 -_-


Posted by uhm
dev.log2005. 8. 22. 17:20

오늘 나눈 대화의 결론 :

결국, 전처리기는 바보다.




Posted by uhm
geek.log2005. 6. 4. 00:11

오늘 비주얼 스튜디오에서 흥미로운 사실을 발견.

비주얼 스튜디오에서 DLL프로젝트를 만들때 "기호 내보내기"옵션을 줘서 생성하면
다음과 같은 코드 템플릿을 만들어준다.

// dlltest.cpp : DLL 응용 프로그램에 대한 진입점을 정의합니다.
//

#include "stdafx.h"
#include "dlltest.h"
BOOL APIENTRY DllMain( HANDLE hModule,
                                       DWORD  ul_reason_for_call,
                                       LPVOID lpReserved
                                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}


// 내보낸 변수의 예제입니다.
DLLTEST_API int ndlltest=0;


// 내보낸 함수의 예제입니다.
DLLTEST_API int fndlltest(void)
{
    return 42;
}


// 내보낸 클래스의 생성자입니다.
// 클래스 정의를 보려면 dlltest.h를 참조하십시오.
Cdlltest::Cdlltest()
{
    return;
}


자.. 보이는가! return 42;의 압박이!
크핫핫. 결국 MS의 개발자들도 인생, 우주와 모든 것에 대한 궁극적인 질문의 해답을 알고 있었다는 말!
(그런데 왜 제품은 그모냥으로 만드냐고..)

첨언 : 42에 대해 잘 모르는 분은 여기여기를 방문해 보시길;

Posted by uhm
geek.log2005. 1. 19. 23:19

투명 C++
1장 :

크아아아아아아아아아아아아아~~~!!!!!!!!!!

컴파일러가 울부짖었다.

그 컴파일러는 투명C++ 컴파일러여따

투명C++컴파일러는 졸라 짱쎘다

C가 도망갔다

자바도 도망갔다

C++컴파일러는 졸라 짱쎄기 때문이었다

======================================================

2장:

갑자기 C#이 나타났다

"니가 투명C++이냐? 난 C#이다. 내가 더 쎌껄?"

C#은 닷넷 프레임웍을 C++에게 발사했다.

투명C++은 끄떡도 안햇다. 투명C++은 졸라 짱쎘기 때문이다.

C#이 플랫폼독립IL을 던졌지만 투명C++은 콧방귀만 뀌어따.

"모든 플랫폼에 포팅된 내 g++ 맛을 봐라! 으하하하하"

C#은 g++을 맞고 한방에 나가 떨어져따. 투명C++은 졸라 짱쎄따.

꼐속

=======================================================

3장 :

사실 C#은 자바의 제자여따. 자바는 사부의 도리를 다하려고 다시 투명C++에게 덤벼따.

"니가 쎄면 얼마나 쎄냐! 내가 새로 연마한 JIT 컴파일러 맛을 봐라!"

JIT컴파일러는 자바가 새로 익힌 공격 기술이어따. 하지만 투명C++은 콧방귀만 뀌었다.

JIT 기술로 날린 힙객체는 투명C++이 만들어내는 스택 객체에 나가떨어졌다.

"에잇 이번 승부는 다음으로 미룬다! 두고보자 투명C++!"

자바는 작전상 자기 본거지로 후퇴했다.

투명C++은 의기양양했다.

"나는 역시 졸라 짱쎄"

꼐속


Posted by uhm
dev.log2005. 1. 13. 18:46

[먕] //네트웍 패킷 파싱.
  int header=packet.header;
  Message * pMsg;
  switch(header)
  {
  case HELLO_MSG:
    pMsg=new HelloMsg();
    break;
  case CHAT_MSG: 
    pMsg=new ChatMsg();
    break;
  }
[먕] 잠시
[먕] 위의 예를 보자고
[먕] HelloMsg,ChatMsg 는 모두 Msg 를 상속 받았다고 하고
[먕] 네트웍 패킷을 위처럼 파싱하고 있는데
(엄) Message 겠지;
(엄) 여튼.
[먕] 저런 switch 문을 쓰고 싶지 않고.
[먕] 해당 키 값에 따라 동적으로 클래스 인스턴스를 생성하고 싶으면
[먕] 어케 해야 되나?
(엄) 어차피 어딘가에는 스위치나. 테이블이 들어 가야 해 -_-
(엄) 뭐 그럴때 쓰는게
(엄) 프로토타입 패턴이긴 한데
(엄) 어차피 스위치나 테이블 둘중의 하나가
(엄) 코드에 포함되는건 피할 수 없지 ㅡ_-
(엄) 나도 저 문제로 열라 궁리 많이 했거덩 -_-
[먕] 음...
[먕] c로는 불가능 한거군.
[먕] java 에서는 저런 switch 문 없이
[먕] 동적으로 생성자 맵핑이 되거덩
(엄) 어뜨케?
[먕] 해쉬테이블이 구조체가 하나있고
[먕] hash.registerDecoder(HELLO_MSG,HelloMsg.class);
[먕] hash.registerDecoder(HCHAT_MSG,ChatMsg.class);
(엄) 그게
(엄) 프로토 타입이자너 -_-
[먕] 패턴이름은 모르겠고.
(엄) 뭐. 그렇게 하려면.
(엄) Message 클래스에
(엄) Message* Message::clone()
(엄) 등의 메소드를 선언하고.
(엄) Message* ChatMsg::clone() { return new ChatMsg( *this ); }
(엄) Message* HelloMsg::clone() { return new HelloMsg( *this ); }
(엄) 그리고 각 객체의 프로토 타입을 해쉬테이블에 등록하면 되지
[먕] 흑... 그건 아냐..
[먕] 잠시.
(엄) 어
[먕] 그럼 위에 처럼 할려면
[먕] 메시지 디코더 인스턴스가 하나씩 생성되어 버리는거 아냐?
(엄) 어차피 니가 쓴 자바 코드도
(엄) 해쉬테이블에
(엄) 타입태그와 타입클래스를
(엄) 등록해 놓는거 아냐
[먕] 자바에서는
[먕]
  NetMessageHandlerManager manager=getMessageHandlerManager();
  manager.registerHandler(DefaultChatMessage.makeIdentifier(ChatProtocol.LOGIN_SUCCESS),new UnSupportedMessageHandler());
  manager.registerHandler(DefaultChatMessage.makeIdentifier(ChatProtocol.PLAIN_MESSAGE),new ChatMessageHandler());
  manager.registerHandler(DefaultChatMessage.makeIdentifier(ChatProtocol.CHAT_MESSAGE_RECEIVE),new C
[먕] 이렇게 해놓으면 타입태그+클래스생성자 들이 등록되는거고
[먕] 위의 프로토타입 패턴을 쓰면.
[먕] 타입태그+클래스 인스턴스가 등록되고
(엄) 그르치
[먕] 그나마 깔끔하다.
(엄) 뭐 정 시르면
(엄) 그르니까
(엄) 해당 메시지의 인스턴스가 생기는게 정 시르면
[먕] 시로
(엄) 해당 클래스의
(엄) 오브젝트 팩토리를 등록해 놓든가 -_-
[먕] 잠시 고로케 한번 해볼께
[먕] 근데.
(엄) 어
[먕] 그렇게 하면 마찬가지로
[먕] 타입태그+팩토리 오브젝트가 맵핑되는거 아닌가 -_-
(엄) 그러치
(엄) 어차피.
(엄) 자바 코드도
(엄) Class 클래스의 객체가 등록되는 거자너
(엄) 별로 다를건 없는거 같은데? -_-
[먕] 잠시 비교 들어갔음.;
(엄) 웅
[먕] 근데
[먕] c 에서 팩토리 구현은 어떤 식으로 되는거야 ?
(엄) C에서?
[먕] c++ 이나
(엄) C에서는 좀 많이 돌아가야 되고 -_-
(엄) C++은.. 대개는
class abstract_factory
{
    object* produce() = 0;
};
class chat_factory : public abstract_factory
{
    object* produce() { return new chat_msg(); }
};
(엄) 뭐 이런 식으로 하지
[먕] 아 -_-
(엄) 아
(엄) virtual 빼먹;
class abstract_factory
{
    virtual object* produce() = 0;
};
[먕] 비지니스 오브젝트 생성 없이
[먕] 팩토리 오브젝트 생성으로 대신하는군
(엄) 어
(엄) 내가 그랬자너 -_-
(엄) 어차피 어딘가에서는
(엄) 테이블에 등록되던가 스위치가 들어가던가
(엄) 둘중의 하나가 불가결하다고 -_-
[먕] 절케 하려면
[먕] 또다시 N 개의 팩토리 클래스가 필요하네 -_-
(엄) 어
[먕] 아햏햏
(엄) 뭐, 물론 비즈니스 오브젝트의 불필요한 생성은 막을 수 있고....
(엄) 팩토리 패턴이 가지는 모든 장점도....
(엄) 차후에 적용할 수 있거찌 ㅡ_-
[먕] 아아...자바가 좋아~ =.=
(엄) 자바도 다를바가 없는거 같은데? -_-
(엄) 자바도
(엄) 어차피 클래스 로더가
(엄) 클래스 로딩하면
(엄) Class 클래스의 객체를 만들어 놓자너 -_-
[먕] 오브젝트의 동적타입을 실시간에 알 수 있으므로
[먕] C 보다는 져아~ =.=
(엄) 어. C++도 가능해
(엄) 컴파일할 때 RTTI 켜놓고 컴파일하면
(엄) typeid()던가
(엄) 하는 연산자를 쓸 수 있지;
[먕] RTTI 가 뭐야 ?
[먕] 첨들어 =.=
(엄) runtime type information
[먕] 오오....
[먕] 마이 컸네 C
[먕] =3
(엄) C++98 부터 있던 표준이야 ㅡ_-
[먕] 한번도 본적 없는걸 ㅡ.ㅜ
(엄) http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/_pluslang_typeid_operator.asp
[먕] 저걸로 컴팔이 하면
[먕] 클래스 필드에 뭔가가 추가 되는거냐
(엄) 당연히 뭔가가 추가 되거찌?
[먕] 그럼 예상치 못한 문제도 생길 수 있겠군 -_-
[먕] 여튼..
[먕] 별게 다있군 오..
(엄) 뭐 그래서 로우-레벨한 걸 좋아하는. 대다수의 C++유저는..
(엄) RTTI잘 안쓰지 ㅡ_-
[먕] 특히 네트웍 플그램 에서도 =.=
(엄) 어
[먕] 니가 말한 팩토리 이용한 방법이
[먕] 가장.. 적합한것 같다
(엄) 내 생각도 -_-
[먕] OKI DOKI
(엄) 수고~
[먕] 어 수고~


 ==================================================================

결국, 가능한 최선의 해결책은 결국엔 하나로 귀결된다고나 할까.

저기서 약간의 첨언을 하자면, 각 팩토리 클래스는 템플릿으로 구현하면 약간의 손품을 덜 수 있다..

template <class T>
class concrete_factory : public abstract_factory
{
public:
    object* produce() { return new T(); }
};

위에서 T가 object에서 상속받기만 한다면, 다음과 같이 팩토리 패턴을 그대로 쓸 수 있다.

hash_table.register( ID_TYPE1, new concrete_factory<type1>() );
hash_table.register( ID_TYPE2, new concrete_factory<type2>() );
hash_table.register( ID_TYPE3, new concrete_factory<type3>() );
hash_table.register( ID_TYPE4, new concrete_factory<type4>() );

N개의 팩토리 클래스 대신, 1개의 팩토리 클래스 템플릿으로 대체가 되는 것이다!
템플릿과 함께하는 C++에 축복을! >_<


Posted by uhm
dev.log2004. 2. 27. 12:28
C로 프로그래밍을 처음 배울때 누구나 만들어본 프로그램이 있다. 모두들 짐작할 것이다. 바로 그 유명한 "Hello, world" 프로그램이다. 이 프로그램은 원래 커니건 & 리치가 쓴 The C Programming Language에 등장하는 첫번째 예제로 그후 거의 모든 C입문서의 첫번째 예제로 쓰이고 있다. Hello, world 프로그램은 다음과 같은 간단한 코드이다.

#inlcude <stdio.h>

void main()
{
    printf( "Hello, world\n" );
}

이 예제에서는 Hello, world를 출력하기 위해 printf()를 사용하는데, 아는 사람은 알겠지만, printf는 C의 표준 라이브러리 함수중 가장 복잡한 함수이다. printf()의 모든 기능을 완전히 이해하기 위해서는 variable argument와 호출규약 등 언어의 지저분한(?) 하위레벨 기능까지를 알아야 하는 까닭이다. C에서 가장 처음 배우는 예제가 C에서 가장 복잡한 함수를 사용한다는 것은 일종의 아이러니가 아닐까.
사실, 위의 예제에서 문자열을 출력한다는 용도로는 puts()함수를 쓰는 것이 더 적합할 것이다. 그러나 아마도 커니건 & 리치는 출력에 관한한 거의 만능에 가까운 printf()함수를, 사용자에게 친숙하게 만들고자 일부러 썼을 것이다.
C라는 언어안에서만 이야기 한다면, printf()의 가장 큰 약점은 수행속도이다. printf()는 첫번째 인자로 주어지는 형식지정문자열(format specifier)을 파싱하여 그 뒤에 주어지는 인자의 출력형식을 결정하는데, 여기에 걸리는 오버헤드가 만만치 않다. 간단한 문자열 출력이라면 puts등을 사용하는 것이 적절하다. 물론 문자열, 정수, 부동소수 등이 복잡하게 얽힌 형식으로 출력한다면 이야기가 달라지긴 하지만.

그러면 이제 논의를 C++의 세계로 옮겨 보자.
C++의 표준 출력기능은 이제 printf[함수]가 아닌 cout[객체]로 옮겨졌다. 왜 옮겼을까?
printf()가 가지는 가장 큰 약점은 형안정성을 보증하지 못한다는 것이다.

#inlcude <stdio.h>

void main()
{
    printf( "%f", 1 );
}

위 와 같은 코드는 C환경에서 정상적으로 컴파일되며 실행도 된다. 물론 현대적인 컴파일러나, lint같은 툴을 쓴다면 printf()에 주어진 형식지정자와 실제인자의 타입이 일치하지 않는다는 워닝을 내줄 수 있겠지만, 위의 코드가 작동하며, 또한 언뜻 보기에 의미 없는 값을 출력한다는 점은 변하지 않는다.
이는 printf()함수가 실매개변수의 타입과 출력 형식을 별도의 정보로 지정하기 때문이다. 대개의 출력형식은 실매개변수의 타입에 따라 결정된다. int형의 실매개변수는 거의 90%이상 %d형식으로 출력할 것이며, 절대 %f형식으로 출력하려고 하는 사람은 없을 것이다. 이는 타입정보와 형식정보가 실제로는 별개의 정보가 아니며 둘 사이에 상관관계가 있다는 것을 의미한다. 물론 고의적으로 플로팅포인트 타입의 비트패턴을 보기 위해 float타입을 16진수 정수형으로 출력하는 경우도 있으나 대개의 경우 타입에 따라 출력 형식이 정해짐을 주지해야 한다. 그러나 printf()는 밀접한 상관관계가 있는 두개의 정보를 별개의 정보로 지정함으로써 둘 사이에 미스매치가 발생할 경우 그에 대해 어떠한 식별도 할 수 없다는 맹점을 지니고 있다.
C++에서 가장 강화된 것 중의 하나가 타입시스템이라고 말한바 있다. 따라서 C++의 설계당시부터 표준출력을 담당하는 printf()함수의 위와같은 맹점때문에 C++에서 표준출력을 담당할 객체가 cout으로 정해지게 된다.
#inlcude <iostream>

int main()
{
    std::cout << "Hello, world" << std::endl;
    return 0;
}

첫번째 Hello, world와 같은 일을 하는 프로그램의 C++버전이다.
cout 은 printf()와는 달리 형 안정성을 보장한다. cout객체가 작동하는 방식은 연산자 오버로딩에 기초를 두고 있기 때문이다. 무슨 말인고 하니, cout객체가 출력할 수 있는 모든 타입에 대해 <<연산자가 오버로딩 되어 있다는 것이다. 이제 컴파일러가 cout객체와 << 연산자를 통해 출력하라는 문장을 만나면 출력할 대상의 타입에 따라 오버로딩된 <<연산자 중에서 적절한 연산자를 자동으로 골라서 출력해 준다는 의미가 된다. 즉, 타입에 맞는 출력 형식을 컴파일러가 선택하므로 형 안정성이 보장되는 것이다.


#inlcude <iostream>

int main()
{
    std::cout << 1 << 1. << std::endl;
    return 0;
}


위 프로그램은 오해의 여지가 없이 의미있는 결과만을 출력해 낸다. 플로팅포인트 타입을 엉뚱하게 int타입으로 출력한다든가 하는 일은 발생하지 않는다. C++의 표준출력객체 cou은 형식정보와 실제 타입정보를 하나로 묶어 관리함으로써 의도치 않은 동작이 발생하는 것을 미연에 방지할 수 있다.

여기에 더하여 printf()가 가지는 또하나의 맹점은 사용자정의 타입에 대한 지원을 전혀 할 수 없다는 것이다. 어떤 클래스 A의 객체 a가 있을 때, printf( "%a", a )와 같은 식으로 일관성 있는 조작을 할 방법이 전혀 없다. 하지만, cout객체로는 ostream 클래스에 대해 <<연산자만 오버로딩 하면 어떠한 사용자정의 타입이건 내장 타입과 같은 방식으로, 일관성 있게 사용할 수 있다는 점은 매우 중요한 강점이다.


이로써 C++에서 되도록이면 printf()를 쓰지 말아야할 몇가지 이유가 분명해진 셈이다. 물론 몇가지 특수한 상황에서는 printf()류의 함수가 유용할 때가 있으나, 일반적인 출력기능은 이제 cout에게 맡기는 편이 현명하지 않을까.

Posted by uhm