Superkkt Blog

"실용주의 프로그래머"란 책을 읽으면서 중요하게 생각되는 내용들을 정리해본다.

1. 함수의 매개변수 유효성은 호출자가 확인해야 한다.

되도록이면 호출자가 확인해서 넘겨주도록 하고있지만 가끔 혼란스러울때가 있었다. 그러다보니 호출된 함수쪽에서 매개변수를 모두 확인하는(나름대로 방어적인 프로그래밍이라고 위로하며..) 루틴을 자주 추가하게 되었다. 뭐, 이것 자체로 나쁜다고는 할 수 없지만 2번에서 나올 문제와 합쳐지면 큰 문제가 된다.


2. 절대로 발생 할 수 없는 경우나 입력 될 수 없는 값이 생긴다면 assert를 사용해서 프로그램을 종료하고 프로그래머가 알 수 있도록 한다.

함수 최상단에서 매개변수의 유효성을 검사할때 되도록이면 프로그램을 죽이지 않으려고 어물쩡 넘어가는 경우가 많았다.

char *
function(const char *str, size_t len)
{
    if(!str) {
       return NULL;
    }
...

위 함수가 매칭되는 스트링을 찾아보고 매칭되는것이 있으면 해당 스트링의 포인터를 리턴하고 없으면 NULL을 리턴하는 함수라고 가정하자. 위와같이 유효하지 않은 매개변수가 들어왔을때 NULL을 리턴하면 호출하는 쪽에서는 매칭되는 스트링이 없는것으로 판단한다. 이렇게 코딩을하면 당장은 큰 문제가 발생하지 않지만 발견하기 매우 어려운 버그를 키우는 꼴이 된다.

따라서 유효하지 않은 값이 들어오면 과감하게 assert를 해서 프로그램을 종료하고 프로그래머가 그 사실을 알 수 있도록 하자.

char *
function(const char *str, size_t len)
{
     assert(str != NULL);
...

"죽은 프로그램이 입히는 피해는 절름발이 프로그램이 끼치는 것보다 훨씬 덜한 법이다."

하지만 assert는 stderr에 메세지를 출력하기 때문에 데몬 프로그래밍에서는 따로 로그를 남기는 assert 메크로를 만들어야 하지 않을까? 테스트해보자..


3. 프로그램 출시할때 assert를 제거하지 마라.

프로그래머가 테스트하는 경우에 assert에 걸리는 일이 없었다해서 제품 출시 후에도 그럴거라는 보장은 없다. 내부 테스트는 여러 사용자가 실제로 프로그램을 사용하는것만큼 모든것을 테스트 할 수 없다. 단, 컴파일러 옵션(매크로)을 사용해서 assert를 제거하고 컴파일 할 수 있는 방법은 추가하자.


4. 변수 선언은 함수 중간에서 하면 안된다.

이건 책에서 나온 내용은 아니고 내가 프로그래밍하다가 실수한걸 정리한 것이다. 요즘 설계에 관한 책을 읽고 있는데, 그 책에서 변수선언과 사용은 가까울수록 버그 발생 확률이 줄어든다고 하더라. 내가 생각해봐도 함수가 길어질때 모든 변수가 함수 상단에서 정의되어 있으면 햇갈릴 확률이 많은것 같았다. 실제로 내가 프로그래밍 할때도 그런적이 몇번 있었다.

그래서 아래와 같이 코딩을 했다.

void
function(void)
{
    char *a = "123";
    int return_val = 0;

    ...
    ...

    if(뭔가 에러 발생) {
        Log(로그기록);
        return_val = -1;
        goto cleanup;
    }

   ...
  ...

    char *ptr = NULL;
    ptr = (char *) malloc(1024);

    ...
    ...

cleanup:
    /* 리소스 반납 */
    ...
    ...
    if(ptr) {
       free(ptr);
    }

    return return_val;
}

이렇게 코딩을 하다가 도무지 원인을 알 수 없는 에러를 만났다. 하루종일 디버깅하다가 겨우 찾아낸 이유가..

첫번째 조건문에서 에러가 발생해서 cleanup으로 goto 했을때 당연히 실행되지 않을거라고 생각했던 free(ptr)이 작동했다. ptr을 NULL로 초기화하는 코드까지 제어가 도달하지 않았기 때문에 ptr에 쓰래기값이 들어있어서였다. 할당하지 않은 메모리를 free했으니 프로그램이 살짝 맛이 가서 그 다음에 실행되는 메모리 관련 연산에서 오류가 발생한것이였다.

저렇게 변수선언을 함수 중간에서 하는것이 표준에 어긋나는 행위(예를들어 gcc의 확장)인지는 확인하지 못했지만 아무튼 앞으로는 똑같은 실수하지 말자!!


5. 리소스를 할당한 곳에서 해제하라.

함수 안에서 리소스를 할당하고 사용했다면 그 함수가 종료할때 모든 리소스를 해재해야 한다. 아래와 같은 코딩 스타일은 함수간에 강한 커플링을 만들고 전역변수 사용으로 인해 유지보수 과정에서 복잡성을 증가시킨다. 불가피하게 이런 방식을 취해야 하는 경우도 있지만 되도록이면 피하도록 한다.

FILE *fp;

void
open_function(const char *filename)
{
    fp = fopen(filename, "r");
}

void
write_function(void)
{
     fwrite(fp, "~~~~");
}

void
process(void)
{
     open_function("filename");
     write_function();
}

위 코드는 아래와 같이 수정 할 수 있다.

FILE *fp
open_function(const char *filename)
{
    fp = fopen(filename, "r");

  return fp;
}

void
write_function(FILE *fp)
{
     fwrite(fp, "~~~~");
}

void
process(void)
{
    FILE *fp;

    fp = open_function("filename");
     write_function(fp);
   
    fclose(fp);
}


6. 예광탄 코드를 사용하라.

아무것도 없는 백지 상태에서 뭔가를 작성한다는건 힘든 일이다. 프로그램을 만드는 일에서도 프로토콜 스펙이나 구현 명세서 같은것이 있더라도 그것을 구현하는 사람 입장에서는 막막할 따름이다.

특히나 구현이 복잡한 프로그램의 경우에는 머리속에 구현 방법이 가물가물하기만 하면서 딱히 이거다 하는 방법이 보이지 않을때가 많다. 이런 상태에서 막무가내로 코딩을 하다보면 도입부에서만 맴돌다가 결국엔 아무 쓸모가 없는 코드만 잔뜩 만드는 경우가 생긴다.

이럴때 아주 간단한 기능만을 구현해서 원하는 결과를 보여 줄 수 있는 코드를 만들어라. 그리고 거기에 살을 붙이면서 실제 구현을 한다. 일단 프로그램 제어 플로우가 완성되면, 거기에 필요한 기능을 추가하면서 전체 기능을 구현하는건 백지 상태에서 막무가내로 코딩하는것보다 훨씬 쉽다.

간단한 기능만을 구현하라는것은 이런것이다. 만약 프로그램이 10가지 기능을 필요로 한다고 하자. 일단은 한가지 기능만 작동하는걸 보여 줄 수 있는 프로그램을 만들자. 만드는 과정에서 에러처리나 유효성 체크 등은 크게 신경쓰지 않아도 된다.

단, 어느정도 전체적인 플로우를 머리속에 그린 후에 그에 맞춰서 모듈화를 하면서 작성해야 한다. 이 부분은 굉장히 중요하다. 예광탄 코드라고해서 전체 플로우를 생각하지 않고 함수 하나에 코드를 다 때려박으면 아무 의미가 없다. 이런 상태에서 어떻게 살을 붙이면서 확장을 해나갈 것인가?


7. 우연에 맡기는 프로그래밍을 하지 말라.

프로젝트를 처음 시작해서 백지 상태인 경우, 특히 설계를 하지 않고 코딩하는 경우에 이 문제에 봉착할 가능성이 크다. 그리고 내가 이 책을 읽으면서 가장 뜨끔했던 부분이기도 하다. 지금까지 나는 이렇게 프로그래밍을 해왔다.

1. 상세 구현을 어떻게 할지 몰라 막막해 한다.
2. 일단 생각나는대로 코딩을 해본다. 나름대로 기능별로 함수를 분리하면서 작성했다.
3. 테스트 해보니 잘 돌아간다. 흐뭇해한다.
4. 며칠동안 테스트 하다가 버그를 만난다. 도무지 해결이 안된다. 어디서 문제가 생긴건지 모르겠다.
5. 드디어 문제를 찾았다. 너무 기뻤다. 고치고 테스트 해보니 잘된다. 내가 대단한 프로그래머 같았다.
6. 젠장!! 하나를 고쳤더니 다른 기능이 작동을 안한다. 확인해 보니 함수간 커플링이 너무 강해서 수정 사항이 다른 함수에까지 영향을 미친것이다. 다시 수정했다.
7. 젠장!! 젠장!! 산 넘어 산이다. 또 하나를 고쳤더니 다른 두개의 기능이 작동 안한다. 프로그램이 점점 미쳐간다.
8. 눈 뻘개지면서 디버깅하고.. 또 디버깅하고.. 또 디버깅하고.. 겨우 돌아가는 프로그램을 만들었다. 한숨 자고 일어나서 내가 만든 프로그램을 흐뭇한 눈빛으로 쳐다본다.
9. 왜 이렇게 만들었는지 기억이 안난다. 마치 다른 사람이 만든 코드를 보는듯 하다. 이런 누더기 코드는 처음본다. 만약 다른 사람이 만든 코드가 이따위였다면 난 그 사람을 프로그래머라고 생각하지 않을것이다. 그런데 내가 그런 코드를 만들어냈다.

이게 바로 우연에 맡기는 프로그래밍을 했을때 나타나는 대표적인 증상일 것이다. 그럼 이런 문제를 피하려면 어떻게 해야할까?

코딩부터 시작하기 전에 전체적인 플로우를 머리속에 그린 후 기능별로 모듈을 분리하고 각 모듈이 해야할 일들과 필요한 인자들을 설계한다. 이거 쉬운 작업 아니다. 하루종일 해도 별 성과도 없고 하기도 싫은 작업중에 하나다.

하지만 초기 설계를 잘 해놓으면(물론 구현중에 설계가 변할 가능성이 크지만) 구현이 일사천리로 진행 될 수 있다. 그리고 전체 플로우가 명확하게 그려졌기 때문에 위에서 나열한 문제를 만날 가능성도 낮아진다.

2006/05/23 22:24 2006/05/23 22:24

trackbacks

trackbacks rss

이 글에는 트랙백을 보낼 수 없습니다

  1. M/D R
    ㅎㅎ 5번. 제가 칼같이 지키는 거죵. 할당한 함수/블록 안에서 해제하기. DB 커넥션, 메모리, 파일 핸들러같은거 말이죠.
    • 김기태 2006/05/29 18:17
      M/D
      할당할때 미리 해제하는 코드를 작성해서 밑으로 밀어놔야 하는데.. 자꾸 깜빡해서 문제가 생기네요. 근데 여자친구 사진은 언제 공개하시남요?
  2. M/D R
    음;; 1번을 잘 모르겠네요; 저도 읽어보긴 했는데;;; 몇 장에 나오는 내용인가요? 다시한번 읽어봐야지 -_-;
    • 김기태 2006/11/07 17:40
      M/D
      음.. 저도 읽은지 오래되어서 기억이 안나네요. 책에 없는 내용을 그냥 쓴건가??^^
  3. M/D R
    유효성 검사를 함수를 호출하는 로직에서 수행하게 된다면, 특정 함수의 유효성 검사 부분은 계속해서 중복되지 않을까 싶은데요.. 어떤 장점이 있는지 구체적으로 설명해주실 수 있으세요??^^; 책에서 읽고싶었는데;; 여쭤볼 수 밖에 없네요 ^^;
    • 김기태 2006/11/08 09:21
      M/D
      저도 제가 써놓고 왜 저게 좋은지 모르겠네요. ㅠ.ㅠ 오늘 출근해서 책을 다시 봤는데 그런 내용은 없는것 같고. 아마 이 글 쓸 당시에 다른 책을 같이 읽으면서 어디선가 본 내용 같습니다. 근데 여전히 저게 왜 좋다고 적었는지는 기억이 안나는군요. 지금 보니 별로 좋아보이지도 않고요.

Leave a Comment