IT_Programming/C · C++

[C++] assert

JJun ™ 2009. 11. 12. 07:26

------------------------------------------------------------------------------------

 출처 : http://www.winapi.co.kr/clec/cpp2/App-A-4.htm

------------------------------------------------------------------------------------

 

assert

 

assert는 코드 차원에서 프로그램의 안정성을 높이는 역할을 한다. assert라는 단어를 영한 사전에서 찾아 보면 "단언하다, 확실히 하다"라는 뜻을 가지고 있는데 코드가 정확하게 동작할 수 있는 상황이라는 것을 확인한다. 함수는 입력과 출력을 가지는데 입력이 정확하면 출력도 항상 정확하지만 호출부에서 틀린 값을 주면 함수를 아무리 잘 만들어도 안정적인 동작을 할 수 없다. 예를 들어 다음 함수를 보자.

 

int divide(int a, int b)

{

     return a/b;

}

 

이 함수는 인수로 주어진 두 정수 a와 b의 나누기 연산을 하여 그 결과를 리턴하는데 divide(6,3)을 호출하면 당연히 2라는 결과를 리턴할 것이다. a와 b만 정확하다면 이 함수가 절대로 틀릴 수 없겠지만 만약 나누는 수 b가 0이면 이 함수는 치명적인 에러를 일으키고 다운되어 버릴 것이다. 이런 에러를 방어할 때는 통상 if문을 사용하는데 if는 어디까지나 에러를 피해 다니는 방법이지 에러를 근본적으로 수정하는 방법은 아니다.

이런 에러를 수정하려면 결국 호출부가 b로 0을 전달하지 않도록 해야 하며 개발자는 이런 상황이 발생했을 때 호출부를 수정해야 한다. divide 함수에 필요한 코드는 b가 0이 되었을 때를 적발해 내는 감시 코드인데 이럴 때 assert문을 사용한다. assert는 assert.h 헤더 파일에 정의되어 있는 매크로 함수이므로 이 헤더 파일을 인클루드해야 한다.

 

: Assert

#include <Turboc.h>

#include <assert.h>

 

int divide(int a, int b)

{

     assert(b!=0);

     return a/b;

}

 

void main()

{

     divide(6,3);

     divide(1,0);

}

 

assert 함수는 표현식을 인수로 전달받아 이 인수가 참인지를 점검한다. 조건이 참이면 이 함수는 아무 일도 하지 않지만 거짓이면 에러 발생 위치와 표현식 등으로 구성된 상세한 에러 메시지를 출력하고 프로그램을 강제로 종료시킨다. assert는 괄호안의 조건식이 확실히 맞는지 확인하는 역할을 하는데 assert (b!=0); 문은 b가 0이 아니라는 것을 확실히 하라는 뜻이다. 특정 시점에서 반드시 참이어야 하는 조건을 assert 의 인수로 작성한다. 그렇다면 다음과 같이 쓰는 것과는 무엇이 다를까?

 

int divide(int a, int b)

{

     if (b==0) exit(-1);

     return a/b;

}

 

이 코드는 b가 0일 때 프로그램을 강제로 종료함으로써 아래의 a/b 연산을 하지 않도록 하기는 하지만 왜 프로그램이 종료되었는지는 알려 주지 못한다. 반면 assert는 버그가 있다는 것 뿐만 아니라 프로그램이 어디서 어떤 이유로 종료되었는지를 자세히 알려준다. 이 예제의 main에서 b로 0을 넘기고 있으므로 이대로 실행하면 다음과 같은 에러 메시지가 출력된다.

 

Assertion failed: b!=0, file C:\CExam\Assert\Assert.cpp, line 6

 

Assert.cpp의 6번째 줄에서 b!=0 조건이 맞지 않아서 프로그램이 종료되었다는 것을 표시하고 있다. 콘솔 프로젝트는 이 메시지가 stderr 표준 출력(결국 화면)으로 나타나지만 그래픽 프로젝트에서는 다음과 같은 대화상자가 나타나며 이 대화상자로 에러 위치와 원인을 정확하게 알 수 있다. 

 

 

개발자는 이 메시지를 받았을 때 다시 시도 버튼을 누른 후 중단된 시점의 콜 스택과 주요 변수의 상태를 확인하여 에러의 원인을 쉽게 알 수 있다. 위치만 알면 원인과 해결책은 금방 파악된다. 디버깅은 버그를 고치는 작업이라기보다는 버그를 찾아 내는 것이며 찾기만 하면 고치는 것은 아주 쉽다.

 

그렇다면 assert가 없을 때와는 또 무엇이 다를까? 어차피 이 예제를 실행하면 바로 다음 행의 나누기 연산식에서 에러가 발생하며 프로그램은 강제로 종료된다. 프로그램이 죽는다는 것을 알면 디버거로 단계 실행해서 죽은 위치와 원인을 알아 내는 것도 가능하다. 그러나 예외가 발생하는 시점과 예외의 원인이 발생하는 시점이 이 예제처럼 인접해 있는 경우보다는 그렇지 못한 경우가 훨씬 더 많다. 다음 예제를 보자.

 

: Assert2

#include <Turboc.h>

#include <assert.h>

 

size_t getsize()

{

     int size;

 

     size=0;

     return size;

}

 

void main()

{

     char *p;

     int size;

 

     size=getsize();

     p=(char *)malloc(size);

     strcpy(p,"test");

     free(p);

}

 

getsize 함수는 어떤 대상의 크기를 조사하며 main은 이 함수가 조사한 크기만큼 메모리를 할당해 사용한다. 어떤 에러로 인해 getsize가 크기를 제대로 조사하지 못해 0으로 조사했다고 하자. 이럴 때 프로그램이 죽는 위치는 getsize가 아니라 이 잘못된 크기를 사용하는 곳인데 이 예제의 경우 strcpy 또는 free에서 죽을 수 있다. 에러의 원인은 getsize가 제공했지만 이 에러가 문제가 된 곳은 main인 것이다. 이럴 때 getsize에 assert 문을 작성한다.

 

size_t getsize()

{

     int size;

 

     size=0;

     assert(size >= 6);

     return size;

}

 

이렇게 해 두면 getsize에서 문제가 발생하는 즉시 assert가 이대로 두면 위험하다는 것은 적극적으로 알린다. 좀 더 대규모의 프로젝트에서는 최초 발생한 에러가 수천줄 이후에나 말썽을 일으키기도 하는데 이럴 때 죽은 자리의 코드만 봐서는 어디서부터 꼬였는지 찾기 대단히 어렵다. 그래서 에러의 원인이 될만한 곳에 assert를 삽입하여 미리 오동작을 발견하고자 하는 것이다.

 

assert는 가급적 많이 사용하는 것이 좋다. 조금이라도 의심이 가는 부분에 대해서는 항상 assert문을 삽입하여 조건을 확실히 만족하는지 점검해야 한다. assert는 에러를 잡기 위한 일종의 덫인 셈인데 덫을 많이 놓을수록 에러가 걸려들 확률은 높아지고 프로그램의 안전성이 향상된다. assert는 또한 문서화에도 도움을 주는데 코드를 읽는 사람에게 함수가 동작하기 위한 전제 조건을 잘 설명한다. 주석보다 오히려 assert가 더 간결한 설명문이다.

 

assert는 아무리 많이 써도 최종 프로젝트의 성능이나 크기와는 상관이 없다. 왜냐하면 assert 는 조건부 컴파일로 정의되어 있는 매크로 함수이기 때문이다. assert 매크로가 어떻게 정의되어 있는지 assert.h 헤더 파일을 보자. 이 정의문에 있는 조건부 컴파일 지시자와 # 연산자 등에 대해서는 다음에 상세하게 배울 것이다.

 

#ifdef  NDEBUG

#define assert(exp)     ((void)0)

#else

#define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) )

#endif  /* NDEBUG */

 

디버그 버전일 때 assert는 인수로 주어진 exp를 평가하고 이 값이 참이면 _assert 함수를 호출한다. 쇼트 서키트 기능에 의해 exp가 참이면 전체 식이 이미 참으로 판명났으므로 _assert 함수는 호출되지 않는다. _assert는 에러가 발생한 수식과 위치를 콘솔 또는 대화상자로 출력하고 프로그램을 종료하는 진짜 함수이되 exp가 거짓일 때만 호출된다.

 

릴리즈 버전일 때 assert는 그냥 0으로 평가되는 빈 문장이므로 프로그램의 속도를 감소시키지도 않고 크기를 늘리지도 않는다. 조건부 컴파일 지시자에 의해 assert를 한 번에 솎아낼 수 있는 장치가 마련되어 있으므로 필요한 곳에 마음껏 써도 상관없다. 그래서 완벽주의를 지향하는 개발자의 소스를 보면 코드만큼이나 assert가 많이 포함되어 있는 경우도 있다. 다음은 assert문의 주의 사항이다.

 

1. assert 매크로는 디버거 버전에서만 컴파일되므로 assert의 인수로는 생략해도 상관없는

   조건식만 넣어야 한다. 릴리즈 모드에서도 실행해야 하는 의미 있는 동작을 assert 문에

   작성해서는 안된다. 다음 코드를 보자.

 

assert((pSet=DoQuery())!=NULL);

pSet의 결과 출력

 

이 코드는 데이터 베이스에 질의를 보내 결과 셋을 받는데 결과 셋이 NULL이 아니라는 것을 확인하기 위해 assert문을 사용했다. 디버거 모드에서는 이 문장이 제대로 실행되지만 릴리즈 모드로 바꾸면 DoQuery 함수가 호출되지 않으므로 pSet은 쓰레기값을 가질 것이다. 이 코드가 확인하고자 하는 것은 결과 셋이 제대로 조사되었는가 아닌가이므로 DoQuery 호출문은 빼고 pSet값을 비교하는 조건문만 assert에 넣어야 한다.

 

pSet=DoQuery();

assert(pSet!=NULL);

pSet의 결과 출력

 

일단 DoQuery를 호출하여 결과를 조사하고 assert문으로 결과가 제대로 조사되었음을 확인했다.

pSet != NULL은 단순한 조건문일 뿐이므로 릴리즈 모드에서 이 조건문이 빠져도 아무 상관이 었다.

assert는 어디까지나 확인용 함수일 뿐이므로 변수의 값을 바꾸거나 프로그램의 상태를 변경하는 코드는

assert안에 둘 수 없다.

 

2. assert는 절대로 발생해서는 안되는 조건에 대해서 사용하는 것이지 정상적인 에러 상황을

   처리하는 문장이 아니다. 위 예에서 DoQuery 함수는 반드시 결과를 돌려 주는 것으로 가정하고

   있으며 만약 질의 결과에 해당하는 레코드가 하나도 없다면 빈 결과셋이라도 리턴할 것이다.

   DoQuery가 실패하는 상황도 정상적이라면 assert 대신 if문을 사용해야 한다. 다음 예를 보자.

 

ch=getch();

assert(isalphs(ch));

입력한 영문자에 따른 작업

 

이 코드는 ch로 반드시 영문자만 입력하도록 요구하며 사용자는 반드시 영문자 중 하나를 입력하도록 강요한다. 사용자가 설사 입력을 잘못했다고 해서 프로그램이 종료되어서는 안되므로 여기에 사용된 assert문은 적합하지 않다. 잘못 입력했으면 다시 입력하라는 메시지를 출력하는 것이 정상적이지 프로그램이 종료되면 어떻게 되겠는가? 사용자는 언제나 실수할 가능성이 있으며 이런 상황은 아주 정상적인 처리 과정일 뿐 예외가 아니므로 여기에는 if문을 사용해야 한다. 

 

3. assert 문에 조건을 작성할 때는 가급적이면 한 조건당 하나의 assert를 쓰는 것이 좋다.

   줄 수를 줄이고자 여러 개의 조건을 하나의 assert에 넣는 것은 좋지 않다.

 

assert (a!=0 && p!=NULL && b==0);

 

이렇게 하면 셋 중 하나라도 거짓일 때 에러 메시지가 출력되기는 하지만 에러 메시지만으로는 셋 중 어떤 문제로 인해 프로그램이 정지되었는지 바로 알 수 없다. 세 개의 assert 문으로 각각 분리해 놓으면 어떤 조건이 거짓인지를 바로 알 수 있다. assert는 아무리 많아도 성능에 영향을 주지 않으므로 굳이 한 줄로 압축할 필요없이 에러 메시지로부터 원인을 바로 알 수 있도록 하는 것이 좋다.

 

C 라이브러리가 제공하는 표준 assert 매크로 외에도 각 라이브러리나 언어, 개발툴이 제공하는 고유한 확인 함수들이 있다. 예를 들어 MFC 라이브러리는 ASSERT, _ASSERT 등의 매크로를 제공하는데 약간의 기능 차이와 출력하는 메시지의 내용이 다를 뿐 사용하는 방법이나 목적은 동일하다.

 

-------------------------------------------------------------------------------------------------