Extremely Agile/TDD2008.01.11 17:01

사실 이번 글은 독립적인 글이라기 보다는 이전 글의 연장선상에 있는 글이라고 보는 것이 좋겠네요. C에서 assert 관련 기능은 assert.h에 정의되어 있습니다. assert는 흔히 '프로그램 수행 중에 반드시 충족되어야 하는 조건을 디버깅 단계에서 검증하기 위해 사용하는' 루틴입니다. Java라면 VM을 실행할 때 -ea를 옵션으로 주고 실행시키면 assert 기능이 활성화되도록 만들 수 있고, java -ea:com.wombat.fruitbat와 같이 하면 특정한 package에 대해서만 assert 기능이 활성화되도록 만들 수 있습니다.

예전에 assert 기능을 release 모드에서 꼭 제거할 필요가 있느냐, 라는 논쟁이 있었던 것으로 기억합니다. 제거해야한다는 입장을 취하는 사람들은 "이미 디버깅이 끝난 소프트웨어에 assert 코드를 남겨두는 것이 무슨 의미가 있는가?"라는 주장을 했구요, 제거하지 말아야 한다는 주장을 하는 사람들은 "정말로 디버깅이 100% 완료되었는지 확신할 수 있는가?  그리고 assert 문을 남겨두는 것이 성능에 그렇게 해가 된다고는 보지 않는다"는 주장을 했었죠.

사실 assert를 통해 검사하는 조건들은 "프로그램 수행 중에 응당 발생할 수 있는 예외적 조건"들이 아닙니다. 예외적 조건들에 대한 처리 루틴은 프로그램 안에 반드시 들어가 있어야 합니다만, assert를 통해 검사하는 것은 "절대로 발생하면 안되는 조건이 실제로 발생하였는지"를 검사하는 것입니다. "절대로 발생해서는 안되는 조건이 발생했다는 것"은 프로그램에 있어서는 안되는 버그가 있다는 이야기이고, 그런 버그는 반드시 제거되어야 합니다.

그러니 "release 모드로 컴파일 할 때에는 assert 관련 코드는 반드시 제거되어야 한다"는 주장은 나름대로 타당성이 있죠. 하지만 "프로그램에서 버그를 100% 완벽하게 제거한다"는 것이 달성하기 힘든 목표이고 보면, 릴리즈 되는 최종 소프트웨어에 assert 문을 남겨두는 것이 크게 나쁘다고는 볼 수 없을 것 같아요. 릴리즈 된 소프트웨어를 설치하고 운영하는 중에 예기치 못한 문제가 발생한 경우, assert가 있으면 그런 문제를 해결하는 것이 분명 편해지거든요. 그래서 저는 릴리즈 모드로 컴파일된 프로그램에도 '굳이 필요하다면' assert를 남겨둘 수 있도록 하려고 해요.

자. 그러면 지난 시간에 살펴본 코드들로 돌아가서, 실제 코드를 보도록 하죠. 지난 시간에는 _DEBUG를 사용해서, 디버그 모드에서 사용할 디버그 매크로들이 활성화되도록 했었습니다. _DEBUG가 정의되어 있지 않으면, 이런 매크로들은 실제 코드에서는 사라지도록 했었죠.

그런데 assert에는 이미 이런 기능이 내재되어 있습니다. 컴파일시에 NDEBUG라는 상수가 정의되어 있으면 assert는 코드에서 사라집니다. (완전히 사라지지는 않습니다. assert 함수의 body 부분만 사라지죠. 그래서 NDEBUG를 정의하더라도 assert를 호출하는 오버헤드를 100% 제거할 수는 없습니다. 컴파일러가 최적화를 나이스하게 해 주지 않는 한.) NDEBUG가 정의되어 있지 않으면, assert는 자신이 하도록 되어 있는 일을 합니다. 자신이 검사하는 expression의 값이 0이 되면 파일명과 라인수를 출력하고 프로그램을 강제 종료시켜버리는 거죠.

그런데 기왕에 _DEBUG를 쓰는 마당에 NDEBUG라는 엇비슷하게 생겨먹은 상수를 또 쓴다는 게 어쩐지 좀 맘에 안드는 군요. 그러니 NDEBUG대신 DISABLE_ASSERT라는 상수를 사용하도록 한번 고쳐보겠습니다. 다른 디버깅 함수들은 대문자인데 assert는 소문자라는 것도 좀 맘에 들지 않는 부분이니, 그 부분도 고쳐보겠습니다. assert가 소문자이기 때문에, 자칫 실수하면 Heisenbug라는 버그가 발생할 수도 있거든요. http://ideathinking.com/blog-v2/?p=56 에 실제 그런 문제를 겪으신 분의 포스팅이 있습니다. http://sunsite.ualberta.ca/jargon/html/H/heisenbug.html 에 가면 Heisenbug의 정의가 있으니 참고하시기 바랍니다.

그럼 실제 코드를 볼까요?

#ifndef DISABLE_ASSERT
#define ASSERT(X) assert(X)
#else
#define ASSERT(X)
#endif

이런 식으로 해 놓고 assert 대신 ASSERT를 쓰면, "assert의 인자로 '반드시 수행되어야만 하는 코드'를 넣어서 발생하는 문제"(즉, Heisenbug)를 어느 정도는 방지할 수 있겠습니다. 설사 생기더라도 교정하기는 좀 쉽겠죠. 대문자로 되어 있으니, 찾아내기도 좀 더 쉬워질 테구요.

ASSERT 매크로를 _DEBUG와는 무관하게 구현하였기 때문에, 컴파일 시에 _DEBUG를 정의하지 않고 컴파일하더라도 (즉, 릴리즈 모드로 컴파일하더라도) ASSERT를 코드 안에 그대로 남겨둘 수 있습니다. ASSERT를 전부 제거하고 싶으면 DISABLE_ASSERT까지 정의해서 (-DDISABLE_ASSERT) 컴파일을 해야만 하죠.

자. 그러면 '반드시 실행되어야 하는 코드의 실행 결과를 검사하려면 어떻게 해야 하죠? 그건 ASSERT로는 좀 곤란해요. DISABLE_ASSERT를 정의하고 컴파일 하는 순간, 그 코드는 오브젝트 파일 안에서는 사라져버릴 것이거든요. 그런 경우에는 VERIFY같은 매크로를 추가로 정의해서 사용하는 편이 더 낫습니다. VERIFY의 구현에 대해서는 http://www.developerfusion.co.uk/show/1719/7/ 를 참고하시는 것이 좋을 것 같습니다. :-)


[4부로 이어집니다]

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^