try catch문 사용예 3

[초보 강좌] Access Violation을 잡아보자!!!


안녕하세요, 여러분.
강좌를 하려고 합니다.
이번 한 번의 강좌로 마칩니다.

(에디트 플러스에서 작성하였기 때문에 아무 곳에서나 읽으시면 탭 길이가 안 맞을 수도 있겠네요... ㅡㅡ;)


0. 들어가는 말

초보 분들은 프로그래밍을 하고서 프로그램을 실행하면 Access Violation 에러가 났다는 메세지를 참~~ 자주 만납니다. 저 역시 마찬가지죠. 시도 때도 없이 만나는 이 놈의 Access Violation... 자료 복사를 할 때, 포인터 문제가 발생할 때, 등등... 버그이긴 한데 정말 잡기도 짜증나고 완전히 없애기는 불가능하며 릴리즈 모드로 실행시킬 때면 가장 큰 부담감으로 다가오는 놈입니다. ㅡㅡ; 그렇다면 이 놈의 발생 횟수를 어떻게 하면 줄일 수 있을까요??(완전히는 불가능하겠죠...)

1-3장까지는 기본적인 내용을 수박 겉핥기로 알려드리겠습니다. 자세한 것은 책을 참조하시길...
4장부터가 제가 말하려는 진짜 주제입니다.


====================================================================
Part 1. 기본을 일단 간단하게...
====================================================================

1. 일단 한 번 시스템 관련 에러를 시험 삼아 처리해 봅시다.

SEH(Structured Error Handling)를 이해하셔야 합니다. 그렇게 어려운 문법은 아닙니다. 단, C언어이기 때문에 C++ 프로젝트에서 사용하기에는 문제가 있죠.

func(){
    __try{
        int a=0, b=1;
        b/=a;
    }
    __except(EXCEPTION_EXECUTE_HANDLER){
        AfxMessageBox("0으로 나누기했네요" );
    }
}

위의 예제는 간단히 보여준 예외상황 처리 루틴입니다. 먼저 __try{}에 쌓은 부분은 VC++에 의해서 보호되는 형태로 컴파일됩니다. 예외가 발생하게 되면 __except의 소괄호 안의 명령을 따르게 되도록 하죠. 위의 예제에서는 소괄호(예외필터라고 부릅니다) 안에 EXCEPTION_EXECUTE_HANDLER가 사용되었으므로 중괄호 안(예외 핸들러라고 부릅니다.)의 명령을 수행하게 됩니다.

포인트는 이겁니다. 위의 예에서는 절대로 무슨 일이 있어도 경고 창이 뜨면서 프로그램이 죽지 않는다는 겁니다. 물론, __except의 예외핸들러에서 또 다시 예외가 발생하면... ㅡㅡ; 그럼 죽겠죠.

하여간 가능성은 여기서 확인할 수 있습니다. __try 블럭 안에는 스트링 카피나 허가되지 않은 포인터 엑세스, 파일 오픈 에러 등등 각종의 예외상황이 발생했을 때 __except로 제어권이 넘어갑니다. 이것이 핵심이죠.



(1) __except()의 소괄호 안에 들어갈 수 있는 것들.


그냥 다음 내용으로 넘어가자니 설명할 수가 없는 내용들이 있어서 몇 가지만 간략히 짚어보죠.
자세한 것은 MSDN을 참고하시거나 여기에 관련된 책을 찾아보시구요. 여기서는 아래의 것만 언급하겠습니다.

-- 예외필터에서는 반드시 하나의 문장(expression)만을 사용할 수 있습니다. 여러가지 체크를 하려면 콤마 연산자나 ? : 연산자를 사용하도록 하세요. 함수도 사용가능합니다.

-- GetExceptionCode() -> 내제함수(특정 영역에서만 사용되는, 무늬만 함수인 일종의 키워드)로서 예외 필터와 예외 핸들러에서만 사용가능합니다. 에러 코드를 리턴하죠. 접근 금지 오류냐, 메모리 할당 실패냐 하는 따위의 메세지에 대한 값이 리턴됩니다. 이 값을 체크하여 여러가지 처리를 할 수 있죠.

-- GetExceptionInformation()-> LPEXCEPTION_POINTERS 라는 구조체의 포인터를 리턴합니다. OS는 예외가 발생했을 때 그 정보를 어딘가 저장해 두고 그 포인터를 위 함수로 가져갈 수 있게 해줍니다. 단, 위 함수는 단지 예외필터에서만 사용가능합니다. 예외 핸들러에서도 사용할 수 없어요. 어쨌든 이 함수가 리턴하는 포인터가 가리키는 구조체는 예외가 발생했을 때의 모든 상황을 담아두고 있습니다. 이걸 사용해서 그 때의 메모리 상황을 덤프할 수도 있다고 하네요.

-- 예외필터에는 위의 함수들과 사용자 정의 함수들을 사용하여 다음 세 가지 중의 하나를 결과값으로 주어야 합니다.
   - EXCEPTION_EXECUTE_HANDLER : 필터가 이 값을 받게 되면 예외 헨들러를 수행합니다.
   - EXCEPTION_CONTINUE_SEARCH : 이 값을 받으면 중첩된 예외상황이라 가정하고 다른 예외필터를 찾습니다. __try 안에 또 __try가 있을 경우에 주로 사용됩니다.
   - EXCEPTION_CONTINUE_EXECUTION : 다시 한 번 수행을 시도합니다.(필터값을 계산하면서 변수 값을 변경할 수 있기 때문에 다시 시도할 때에는 문제가 없을 수도 있죠.)

이해를 하셨을지 모르겠습니다. 예는 들지 않겠습니다. 예외 처리를 강의하는 것 자체가 주제가 아니기 때문에 간단 간단히 넘어가려고 합니다. 설명이 많이 부족한데... __try, __finally라는 것도 있고... ㅡ.ㅡa 하지만 예외 처리에 대한 문법을 설명한 부분은 대부분의 책에서 짤막하게 다루니까 그리 어렵지 않게 익히실 수 있을 거예요.(저도 위의 문법을 숙지하는 데에는 10분? 20분? 정도밖에 안 걸렸습니다.) 함 찾아보시길 권해드립니다. 하지만 아래 강좌 내용에서는 모르셔도 상관없습니다.




2. C++에서는 어떻게 할 것인가...


예. 이 것이 일단 첫 번째 장벽입니다. 조금만 C++을 깊게 공부해 보셨다면 try, throw, catch는 이미 접해보셨을 겁니다. 하지만 이 키워드들은 synchronous한 상황만 지원합니다. 무슨 말인고 하니 프로그래머가 던지지 않은 예외상황은 잡아내지도 않는다는 겁니다.(물론 방법은 있습니다. 그게 뒤에 나올 주제죠.) 그런데 우리가 잡으려고 하는 예외상황들, 즉, 메모리 엑세스 에러 같은 것들은 OS에서 보내주는 예외상황이며 이것은 프로그램의 흐름과는 상관없이 발생하는 asynchronous 예외입니다. 스트라우스트럽의 책은 이 주제에 대해서는 다루지 않고 있습니다.


일단 간단한 예부터...

class CTest
{
public:
    int a;
    
    CTest(){};
    CTest(int k):a(k){};
    ~CTest(){};

};

// CTest 멤버에 대한 정의들
// 생략.

void ErrorFunction()
{
    int c=0, b=1;

    if(c==0)
        throw CTest();
    else
    {    b/=c;
        return;
    }
}

main()
{
    try{
        ErrorFunction();
    }
    catch(CTest e){
        cout<< e.a;
    }
    catch(string s){
        cout << s;
    }
    catch(...){
        cout << "음... 모르는 예외네요... ㅡㅡ;"
    }
}


main함수의 try블럭에서 함수를 실행하였는데 그 함수는 문제가 있으면 throw 해버립니다. 그러면 다시 main 함수의 catch가 그 놈을 받아서 에러를 처리합니다. __try, __except 와의 다른 점이 느껴지시는지요. 예. 생각하시는 바로 그겁니다. 예외상황을 프로그래머가 직접 발생시킨다는 것이죠. 또 한 가지 다른 점은 throw, catch를 할 때 type을 주고 받는 것이 가능하다는 겁니다. __try, __except에서는 그냥 "예외가 생겼으니 내가 주는 포인터에 있는 내용을 보고 에러가 뭔지 알아서 확인하고 처리하게나"라고 말합니다. 둘 사이에는 상당한 괴리감이 있어 보입니다...

그리고 만약 ErrorFunction이 CTest가 아닌 다른 타입의 에러를 리턴할 경우에는 CTest는 그냥 통과하고 다음 catch 블럭을 체크합니다. 위의 예에서는 그것이 STL의 string이라고 가정했습니다. 만약 string도 아니면?? 그러면 (...)이 쓰여진 곳에 있는 핸들러를 수행합니다. 마치 switch()처럼 동작하죠? 그런데 만약 catch(...){} 블럭이 없고 적절한 예외 핸들러도 없다면 기본적으로 terminate 함수가 불려지게 되어 있습니다. 즉... 프로그램이 종료되는 것이죠. ㅡㅡ;

역시 자세한 내용은 책을 참조하세요. 이 부분에 관해서만큼은 Stroustrup의 14장 Exception Handling 부분을 읽어보시길 강력하게 권해드립니다. 일단 8장의 해당 부분을 꼼꼼하게 보시면 간단한 사용법은 익히실 수 있구요, 고급 주제에 대해서는 14장을 보시면 됩니다. 14장에서는 일단 auto_ptr까지는 읽어보시는 것이 좋아 보이네요.




3. SEH와 C++의 EH 사이의 괴리를 어떻게 없애는가...


지금 당장 MSDN을 띄우신 후에 색인 탭을 누르시고 exception handling 항목의 하위 항목인 differences between SEH and C++ EH 를 읽어보시기 바랍니다. 읽으셨으면 이번 장은 스킵하셔도 됩니다. MSDN이 없으신 분을 위해서 아주 간략하게 요약하겠습니다.

일단 거기에 나와있는 예제를 보겠습니다.



#include <iostream.h>

void SEHFunc( void );

int main()
{
    try
    {
        SEHFunc();
    }
    catch( ... )
    {
        cout << "Caught a C exception."<< endl;
    }
    return 0;
}
void SEHFunc()
{
    __try
    {
        int x, y = 0;
        x = 5 / y;
    }
    __finally
    {
        cout << "In finally." << endl;
    }
}




main함수는 SEHFunc()를 호출하며 여기서는 0으로 나누는 예외가 발생합니다. ( __finally는 예외에 상관없이 무조건 실행되는 부분입니다.) 그러면 SEH에서 정의된 예외가 발생합니다. 하지만 말씀드렸다시피 이 예외는 asynchronous 예외이기 때문에 synchronous 예외만 처리하는 try, throw, catch에서는 잡아낼 수가 없습니다.... 그러나!!! catch(...)는 이 예외를 잡아냅니다. 다음은 위 프로그램의 수행결과입니다.

In finally.
Caught a C exception.


*********** 자축!!! ***************

일단 이것만으로도 대부분의 Access Violation들을 잡아낼 수 있다는 것을 아셨을 겁니다. 이 예제를 봤을 때의 기분을 아직도 잊을 수가 없네요... ㅜ.ㅜ

어쨌거나... 아직은 많이 부족합니다. 하여간 잡아낼 수는 있게 되었으니 프로그램의 이상 종료 상황은 많이 줄일 수 있습니다.


각설하고... MSDN에는 위의 것 말고도 하나의 방법을 더 알려줍니다. 바로 C 스타일 예외를 C++ 스타일로 바꿔주는 방법입니다. 감동의 물결입니다. 아래의 예를 보시죠.

#include <stdio.h>
#include <eh.h>
#include <windows.h>

class SE_Exception {
private:             // 버그입니다. public:으로 바꾸셔야 컴파일됩니다.
        // 다른 예제에는 없는 버그인데 하필이면 이 예제에 걸려서 시간 낭비 좀 했습니다.
    SE_Exception() {}
    SE_Exception( SE_Exception& ) {}
    unsigned int nSE;
public:
    SE_Exception(unsigned int n) : nSE(n) {}
    ~SE_Exception() {}
    unsigned int getSeNumber() { return nSE; }
};

void SEFunc(void);
void trans_func( unsigned, _EXCEPTION_POINTERS*);

int main()
{
    _set_se_translator( trans_func );
    try
    {
        SEFunc();
    }
    catch( SE_Exception e )
    {
        printf( "Caught a __try exception with SE_Exception.\n" );
        printf( "nSE = 0x%x\n" , e.getSeNumber() );
    }
    return 0;
}
void SEFunc()
{
    __try
    {
        int x, y=0;
        x = 5 / y;
    }
    __finally
    {
        printf( "In finally\n" );
    }
}

// 아래 함수의 인자와 리턴 타입은 VC++에 의해 정의되어 있습니다. 형태를 바꾸지 마세요.
void trans_func( unsigned int u, _EXCEPTION_POINTERS* pExp )
{
    printf( "In trans_func.\n" );
    throw SE_Exception( u );
}



(위의 예제에는 버그가 있습니다. SE_Exception의 private를 public으로 바꿔야 제대로 동작합니다.)

중요한 부분은 main함수의 _set_se_translator( trans_func ); 입니다. try, catch를 사용하기 직전에 이 함수를 사용하여 trans_func라는 놈을 등록해 주는데 OS는 예외가 발생할 경우 _set_se_translator()에 의해 등록된 놈을 수행하게 됩니다. 이를테면 전처리기를 등록해 주는 거죠. 이렇게 하면 trans_func에서 대신 throw를 하는 것이 가능해집니다!!! 이 때의 throw는 당연히 C++의 try, throw, catch에서와 마찬가지로 어떤 형태의 type이든지 throw할 수 있습니다. 위의 예에서는 SE_Exceptin 형의 예외 객체를 던지는군요...

헐... 여기까지 이해를 하셨다면 거의 모든 내용을 다 이해하신 겁니다. Access Violation?? 일단 발생하는 부분만 알아내면 위의 방법을 사용하여 샥 없애버릴 수 있습니다. 얼마나 멋집니까... ㅜ.ㅜ







====================================================================
Part 2. 프로그램을 작성할 때 위의 방법을 어떻게 적용하는 것이 효율적일까??
====================================================================



이번에 강좌를 하게 된 주제가 이겁니다. 사실 3장까지는 대체로 실전에서 일을 하신 분들이라면 다 아시는 내용이구요, 그다지 정보를 얻는 것도 어렵지 않습니다. 아래 내용은 제목처럼 "어떻게 해야 효율적으로 Access Violation을 차단할 수 있을까?"가 주제입니다.




4. 상황 설정


약간 큰 프로그램을 작성한다고 가정해 보죠. 그러면 수많은 정보들이 클래스 간에 교환되기도 하고 복사 되기도 하고 전달되기도 합니다. 그러다 보면 포인터를 잃어버리기도 하고 어쩌다 보면 이상한 영역을 엑세스하다가 다운되기도 합니다. 디버그 모드에서 잘 동작하다가도 릴리즈로 실행하는 순간 첫 창이 뜨기도 전에 뻑이 난다거나... ㅡㅡ; 하여간 문제가 많습니다.

버그를 잡아서 그 부분을 수정한다고 해도 다른 부분을 수정하고 나면 다시 에러가 날 수도 있으며 프로그램이 커짐에 따라 도대체 어디에서 잘못된 연산을 한 것인지 도저히 알아낼 수가 없을 수도 있게 됩니다.


그리하여... 방법을 찾다 찾다 보니 1~3장 까지의 방법을 알게 되었고 이제는 어떤 예외상황도 처리해 나갈 준비가 되어 있습니다. 하지만 이것도 문제네요. 위의 방법을 적용할 곳이 너무 많습니다. 수천 라인이나 되는 코드를 일일이 쫓아다니면서 에러가 발생할 만한 곳에 모두 적용하자니 기가 찰 노릇이고 적용을 하지 말자니 언제 다운될지 모르는 불안정한 프로그램이 될 것 같아 걱정이고... ㅡㅡ;

어떻게 하면 좋을까요???



모든 것은 설계의 문제로 종결된다는 것이 결론입니다만, 일단 좀 더 상황을 살펴 봅시다.

이 강좌에서 관심있는 asynchronous 예외 상황이 발생하는 것은 주로 언제일까요?... 논리적 오류가 있는 곳? 아닙니다. 함수를 사용하는 때에는 예외가 별로 발생하지 않습니다. 물론 전혀는 아닙니다만 상대적으로 숫자가 매우 적습니다. 정확히 말하자면 프로그램의 코드 영역을 엑세스할 때에는 에러가 별로 발생하지 않습니다. 하지만 데이터 영역을 엑세스 할 때에는 굉장히 자주 발생합니다. 포인터를 사용하는 것은 데이터를 엑세스할 때이고 특히 허가받지 않은 힙이나 스택을 쓰고 읽기 할 때 문제가 발생하는 거죠. 그 외에도 디스크를 읽는다거나 주변기기에 대한 엑세스 허가를 받지 않고 주변 기기를 건드리거나 할 때 OS에서 예외를 발생시킵니다. 대부분의 코드 영역 예외 상황은 논리적 오류입니다. ㅡㅡ; 이건... 잘 설계하는 수밖에 해결책이 없습니다.

어쨌든 결론적으로 말씀드려서 데이터 영역을 다루게 될 때 대부분의 에러가 발생하는 겁니다. 가장 대표적인 예를 보여드리죠.

CString *str= new CString;
....
....
int i = str->GetLength();

만약 str이 ...부분에서 이상 동작이 수행되어 엉뚱한 영역을 가리키게 되었다면? 당연히 마지막 줄에서 예외가 발생하게 됩니다. 이런 상황이 전혀 없을 거라고 생각하면 안 됩니다. 흔한 예를 들어서,

CArchive * ar = new CArchive;
...
AfxMessageBox("..." );

이 문장은 무슨 에러를 발생시킬 지 알 수 없는 코드입니다. 경험적으로 안 것이지만 CArchive형 포인터와 AfxMessageBox는 하나의 함수 안에서 동시에 사용하면 안 됩니다. ar에 갑자기 NULL이 들어가더라구요. 그러므로 반드시 CArchive ar;과 같이 스택에 저장되는 변수를 설정하여 사용하는 것이 안전한 방법입니다.


그 외에 이런 예도 있습니다.

CMyClass a(init);
CMyClass b;
...
b=a;

여기서는 CMyClass의 operator= 가 호출되고 그 안에서 복사가 수행되게 됩니다. 그런데 만약 a의 멤버 중에 포인터가 있고 그 포인터가 제대로 된 곳을 가리키지 않는다면 복사 중에 에러가 발생합니다. 물론 그것을 체크하는 루틴을 넣을 수도 있겠지만 쉽지 않을 겁니다.

또 위의 예에서 a.GetStringData() 라는 문장을 수행했다고 가정해보죠. 이 때 GetStringData()가 이상한 영역을 읽게 되면 역시 에러가 발생합니다.


결론적으로... 교환, 접근, 복사, 전달, 할당 등등 거의 대부분의 에러는 "데이터 영역"에서 발생한다는 겁니다.




5. 그렇다는 것은...?


=============================================
예. 결론적으로 말씀드려서 데이터 관련 작업들만 따로 잘 모아두고 거기에서만큼은 예외처리를 확실하게 해주자는 겁니다.
=============================================


그렇다면 어떻게 하는 것이 잘하는 것일까요?
바로 처음 프로그램 디자인을 할 때 데이터 클래스 디자인과 서비스 클래스 디자인을 분리시키는 겁니다.


서비스 클래스는 자기 자신을 데이터로 가정하여 다른 서비스 클래스로 복사되는 경우가 거의 없습니다. 극단적인 예를 들어서 CMainFrame을 생각해 보세요. 이 클래스가 프로그램 동작 중에 다른 인스턴스로 복사가 되는 것을 상상하기는 힘드실 겁니다.(Windows는 fork를 지원하지 않죠.) 이렇듯 서비스 클래스들의 인스턴스는 하나의 프로세스 혹은 쓰레드 내부에 대체로 하나만 존재합니다. 두세 개가 있을 필요가 없죠. 클래스의 목적 자체가 서비스이기 때문입니다. 곁얘기지만 서비스 클래스들은 new로 생성하지 않는 것이 좋습니다. 굳이 new로 생성시킬 필요가 없기 때문입니다. new를 사용할 필요가 없다면 안 사용하는 것이 백 배 낫습니다.

하여간 서비스 클래스는 대부분의 경우 스택에 존재하며 자기 자신의 데이터를 다른 곳에 복사할 필요가 별로 없는 클래스이며 따라서 복사 연산자(operator=)나 복사 생성자(copy constructor)를 만들 필요 역시 거의 없는 클래스입니다. 때문에 서비스 클래스의 대부분의 함수들은 자기 자신의 스택에 있는 멤버를 엑세스하거나 함수 자체, 즉 코드 영역을 사용하거나 하는 정도의 일만 하게 되고 따라서 예외도 그다지 많지 않습니다.


하지만 데이터 클래스는??? 전혀 상황이 다르죠. 워드를 한참 쓰다 보면 가상 메모리가 부족하다는 둥, 복사가 잘 안 된다는 둥, 말이 많기도 하고 혹은 갑자기 화면에 에디트한 것의 일부가 없어져 버린다거나(요즘엔 그런 일이 별로 없지만) 하는 일도 있습니다. 얼마나 황당합니까? 이런 에러에 관련되는 클래스들이 거의 모두 데이터 클래스입니다.

결국 데이터 클래스에 안정성을 부여하는 것이 가장 효과적으로 예외처리를 하는 첩경인 셈이죠. 


종합하자면 서비스 클래스에는 손실, 복사, 오류 가능성이 있는 멤버를 추가하는 것을 자제하여야 하며 그 값이 아주 자주 바뀌거나 다른 것으로 교체될 가능성이 항상 존재하는 것일 경우 그것들을 데이터 클래스로 옮길 좋은 방법이 없는지 생각해 봐야 한다는 겁니다. 이렇게 디자인하여 얻어진 데이터 클래스... 거기에 안정성을 부여하는 방법을 6장에서 설명합니다.



6. copy(CMyData & other) 를 잡아라!!!


지난 번 STL 강좌와 C/C++ 공부 가이드에서도 말씀드렸지만 Annotations on C++은 참 좋은 C++ 문서인 것 같습니다. 이번에 보여드릴 예에서도 이 문서에서 제시한 데이터 클래스 구조의 뛰어남이 보여집니다.

일단 여러분께서 위의 문서에서 제시한 구조, 즉, "생성자, 복사 생성자, copy, destroy, 소멸자, 복사 연산자" 구조를 알고 계신다고 가정하겠습니다. 모르시는 분은 위 문서의 Ch.5 를 참고하셔서 꼭 알아 두시기 바랍니다. 여기 강좌란에 김현우(whytok)님이 help 파일로 올려 두셨으니 그것을 다운 받아서 보시면 편할 겁니다.

어쨌든 데이터 클래스는 위의 구조를 따르시는 것이 좋습니다. 처음 클래스 작성 시에 아무리 귀찮다 하더라도 꼭! 꼭! 저 구조를 만들어 두시는 것이 나중에 편합니다.




(1) 기본적인 문제 정리.


다시 본론으로 돌아와서...

일단 다음의 상황을 다시 고려해 보겠습니다.
이것들이 가장 자주 발생하는 문제이기 때문입니다. 문제는 간략화했습니다. 실제로 이런 코드를 짜는 사람은 없겠지요.


// 상황 1.
CMyData * a;
CMyData b(init);
*a = b;

// 상황 2.
CMyData *a;
CMyData b(a);

// 상황 3.
CMyData *a;
int i= a->GetNumber();

// 상황 4.
CMyData a;
char *c;
a.SetString(c);


위의 예들은 너무나 매정하게도 간단히 에러를 발생시킵니다.
어쨌거나 많은 상황들을 요약하면 위의 네 가지 중에 하나로 귀결됩니다. 실제로는 에러없는 코드라고 생각되는 것들도 디버거를 돌려보면 아직 할당되지 않는 객체로부터 복사를 해 오거나 혹은 자신이 아직 제대로 영역 할당이 되지 않았거나 하는 경우가 많습니다.(주의할 점은 자신의 내부 함수들은 그대로 실행이 된다는 겁니다. 에러 발생 순간은 맴버 함수들이 할당되지 않은 맴버 변수에 접근하는 순간입니다.) 그리고 상황 3,4는 상황 1,2의 일부를 보인 겁니다. 상황 1,2는 3,4의 경우들을 모두 포함하고 있죠. 저는 상황 1,2의 해결책을 한 가지 제안할 것입니다. 이걸 이해한다면 3,4의 경우에는 적당히 고쳐서 적용하실 수 있습니다.



(2) 상황 1,2의 분석.

서론이 너무 길었네요... ㅡㅡ;

상황1의 경우는 자기 자신이 아직 메모리 할당을 받지 않은 상황에서 유효한 데이터를 멤버에 쓰려고 할 때 발생하는 예외입니다. 반면, 상황2는 부적절한 메모리로부터 데이터를 읽으려고 할 때 발생하는 문제죠.

일단 상황 1,2 모두 다음 코드로 기본적인 예외처리가 가능합니다. 일단 여러분께서 기본 데이터 클래스 구조를 아신다고 가정한다면 다음과 같이 copy()함수를 수정할 수 있습니다.


void CMyData::copy(CMyData const &other)
{    _set_se_translator(trans_func);
    try{
        m_strTimeStampSend = other.m_strTimeStampSend;
        m_strTimeStampReceive = other.m_strTimeStampReceive;
        m_flFrom = other.m_flFrom;
        m_flOriginalTo = other.m_flOriginalTo;
    
        return;
    }
    catch(SE_Exception e){
        TRACE("\nCMyData.copy.catch: Reading error. Error no. is %d\n\n" ,e.getSeNumber());
    }
    catch(...){
        TRACE("\nCMyData.copy.catch: Unknown error...\n\n" );
        // throw;
    }
}

일단 _set_es_translator()를 사용하여 등록한 후 복사를 수행합니다. 만약 이 과정에서 에러가 있다면 그것이 의미하는 바는 other 객체가 적절하지 않은 메모리에 있거나 자신이 제대로 메모리 할당을 받지 못한 객체라는 뜻입니다.(그 에러가 아닐 수도 있지만 그런 경우는 흔치 않죠.) 그러므로 SE_Exception 객체가 throw 될 것이고 첫 번째 catch에서 그걸 받아 처리하게 됩니다. 만약 OS 에서 발생시킨 예외가 아니라면 두번째의 (...)에서 처리하게 됩니다. 혹시 원하는 다른 예외상황이 있다면 적절하게 (...) 이전에 추가하십시오. 그리고 e는 적당한 함수를 두어서 OS의 예외값에 따라 여러가지 처리를 할 수 있다는 사실을 기억해 두십시오. 위의 방법은 단지 뼈대일 뿐입니다.(저는 그대로 사용하고 있지만... ^^;)


그런데 의문이 생깁니다. 생성자 CMyClass(CMyClass &other) 와 복사 연산자 CMyClass & operator=(CMyClass & other) 에서는 예외가 발생하지 않을까요? 정답은 "예외가 발생할 가능성은 상당히 적다"입니다.(아니면 어쩌나... ^^;) 두 함수를 잘 살펴보면 멤버를 직접 엑세스하는 코드가 없다는 사실을 알 수 있습니다. 멤버 엑세스는 copy()를 사용하죠. 따라서 두 함수는 코드 영역만을 사용하게 되며 결국 Access Violation을 발생시킬 가능성은 별로 없는 겁니다. 그러니 예외 처리는 copy에서만 해주면 되는 겁니다.


(3) 손질하기


*** 여기가 중요합니다. ***

위의 코드로 완전한 것일까요? 아닙니다. 문제가 조금 남아 있습니다. 예를 들어서 부적절한 영역에서 읽어오는 경우(상황2) 일부의 맴버 변수는 이미 복사가 된 상태에서 에러가 날 수 있습니다. 그럴 경우 다시 자신의 값을 초기화하고 싶거나 아니면 다른 값으로 변경하고 싶을 겁니다. 그러니 이런 상황에 대처할 코드를 넣어야 합니다. "하지만 이 코드를 실행하려면 자기 자신이 유효한 메모리를 가지고 있는지 반드시 체크하여야 합니다." 왜냐하면 copy() 함수는 자기 자신이 유효한 지 아닌 지 알고 있지 않기 때문입니다. 그렇다면 어떻게 해야 그것을 체크할 수 있을까요? 뭐, 별로 새로울 것은 없습니다.

아래의 방법을 사용하십시오.


void CMyData::copy(CMyData const &other)
{    _set_se_translator(trans_func);
    try{
        m_strTimeStampSend = other.m_strTimeStampSend;
        m_strTimeStampReceive = other.m_strTimeStampReceive;
        m_flFrom = other.m_flFrom;
        m_flOriginalTo = other.m_flOriginalTo;
    
        return;
    }
    catch(SE_Exception e){
        TRACE("\nCMyData.copy.catch: Reading error. Error no. is %d\n\n" ,e.getSeNumber());
        try{
            destroy();
            CTest();
        }
        catch(SE_Exception e){
            AfxMessageBox("Init again fails" );
        }
    }
    catch(...){
        TRACE("\nCMyData.copy.catch: Unknown error...\n\n" );
        try{
            destroy();
            CTest();
        }
        catch(SE_Exception e){
            AfxMessageBox("Init again fails" );
        }
        // throw;
    }
    return;
}


그냥 destroy()랑 기본 생성자를 한 번씩 불러주었습니다. 하지만 다시 한 번 try,catch구조 속에 넣어주었습니다. destroy()에서는 당연히 나름대로의 예외상황 처리 루틴을 가지고 있도록 코딩해 주어야 하겠습니다. 좀 모양새가 좋지 않게 되었습니다. 사실 하나의 예외 핸들러 안에 또 예외 핸들러를 넣는 것은 별로 바람직하지 않은 poor style이라고 Stroustrup은 비웃더군요... 쩝... 뭐, 별로 할 말은 없습니다. 그치만 더 나은 방법을 못 찾았어요. 나은 방법을 아시면 꼭 좀 리플 부탁...

여기서 잠깐 의문이 생기는데 생성자에서는 예외상황 루틴을 만들 필요가 없을까요? 음... 정답은 저도 모르겠습니다만 제 생각엔 만들 필요가 없다는 겁니다. 스택에 영역 할당되는 경우에는 실패하면 프로그램 종료니까 말할 필요가 없지만 힙에 new로 할당되는 경우에는 리턴 값이 NULL이냐 아니냐에 따라 체크가 가능합니다. 그러니 굳이 또 체크할 필요가 없죠. 단, 생성자 안에서 new를 사용하는 경우에 대해서는 별로 생각해 보지 않았습니다. 별로 권장하고 싶은 맘이 없네요. 꼭 사용해야 한다면 복사 생성자 같은 경우에만 사용하도록 하고 그 루틴들은 당연히 copy()에서 예외처리 루틴이랑 함께 동작되도록 만들어 주는 것이 좋겠습니다.

어쨌거나 Annotations on C++은 참으로 좋은 구조를 제공하고 있습니다. 예외처리에서조차 copy(), destroy()만 고려하면 기본 예외는 제어가 되니까요. 정말 멋지지 않습니까?


여담인데 이 글을 쓰기 전에는 this 객체가 유효한 것이냐 아니냐를 체크하기 위하여 다음 코드를 사용했었습니다.
if(this != NULL  &&  !IsBadReadPtr(this, sizeof(CMyData) ) && !IsBadWritePtr((LPVOID)this, sizeof(CMyData)))
그런데 이 코드는 디버그 모드에서만 제대로 동작하더군요. 릴리즈에서는 무조건 읽고 쓰기가 가능하게 리턴이... ㅡㅡ; 쩝...


아직 한가지 문제는 남아 있습니다. 예를 들어서 다음과 같은 코드가 있을 경우...

CMyData *a = new CMyData;
CMyData *b;
*a = *b;

b가 가리키는 영역에서 a가 가리키는 영역으로의 복사가 불가능하지만 에러는 예외처리 루틴에서 잡히며 a는 의미없는 자료들만, 즉 기본 생성자에 의한 자료들만 갖게 됩니다. 서비스 클래스 입장에서 보면 더미, 혹은 쓰레기 데이터이며 메모리를 잡아먹는 데이터가 될 가능성이 다분합니다. 에러 대신 발생하는 비용인 셈입니다. 따라서 서비스 클래스 코드에서는 이런 데이터 인스턴스가 발견되었을 때 제거해 주는 루틴이 필요하게 됩니다. 하지만 이것은 실행시에 프로그램이 죽는 것 보다야 훨 낫죠. 그리고 catch구문 안에서 에러의 종류(getSeNumber()로 얻은 값)에 따라 적절한 수행을 하는 것이 가능하므로(MSDN을 보시면 숫자가 의미하는 바를 아실 수 있을 겁니다.) 이것을 잘 활용하면 깔끔한 처리를 하는 것도 가능할 겁니다. 그건 여러분의 몫이구요.(저의 경우에는 일단 그냥 무시합니다. 저런 노드가 발견되면 그냥 삭제하죠.)


(4) 상황 3,4에 대한 첨언.


상황1,2의 방법을 응용하면 간단하게 멤버 함수의 오류를 예외처리에서 처리할 수 있게 됩니다.

그런데 멤버 함수를 작성할 때 주의할 사항이 하나 더 있습니다.
데이터 클래스의 멤버 함수는 에러가 발생할 경우 자신을 호출한 문장에게 적절히 그 사실을 알릴 방법을 마련해 두어야 합니다. Set...() 종류의 경우에는 반드시 BOOL 형을 리턴하는 것이 좋겠고 Get...()의 경우에는 의미없는 값을 정의해 두고 그것을 리턴하는 것이 좋겠죠. 그렇게 해야 그 함수를 사용하는 문장이 별 문제없이 동작하게 될 겁니다.

(GetLastError()를 하나 만들어 둬도 좋을 듯... private 멤버 변수 하나를 두고 예외 상황이 있을 때마다 정보를 기록해 두는 거죠. getSeNumber()의 결과값 따위를요. 그리고 GetLastError()가 호출되면 그 값을 리턴해 주는 겁니다.)

이렇게 할 경우 엄청난 장점이 있는데... 그것은 이 함수를 사용하는 문장들이 예외처리 루틴을 굳이 사용할 필요가 없게 된다는 것입니다!!! 단지 if()로 리턴 값의 유효성만 체크하면 됩니다. 물론 이것이 전부는 아니겠지요. C++에서 제공하는 try, throw, catch를 사용하면 if문들 중첩하여 사용할 때조차도 그 경우를 최소화 시키고 이쁘게 디자인 하는 것이 가능합니다. 어쨌거나 가장 중요한 점은 synchronous하게 프로그램을 동작시킬 수 있게 된다는 것이겠죠. 상위 레벨에서는 대부분의 문장 결과값을 확인하는 것이 가능해지고, 바라지 않는 예외상황을 최소화할 수 있게 되는 것이죠.

쩝... 그런데 이게 그렇게 말처럼 쉽게 되면 얼마나 좋겠습니까. 저도 위의 방법을 상상하고 열라 좋아하다가 좌절했었습니다. 상당히 이상한 경우가 있더군요. 아래의 예를 일단 한 번 보시고 나서 얘기를 진행하죠.(첨부파일의 예제는 아래의 오류가 정정된 버젼입니다. 컴파일해서 결과를 살펴보세요. OnInitDialog()에서 테스트를 했습니다. 좀 무식하게 메세지 박스만 잔뜩 출력합니다. ㅡㅡ;)

// 이 클래스는 테스트를 위해서 급조한 것이고 오직 테스트만을 위해서 만든 것이라 Annotations on C++의 구조를 사용하다가 말았습니다. 불완전하지만 이해해 주시길...


void SomeFunction();
OnInitDialog()
{
//...
    SomeFunction();
}


class CTest
{
public:
    CTest();
    CTest & operator=(CTest&);
    ~CTest();
    void    destroy();

    CString GetString()const;
    int GetNumber()const;
    BOOL SetNumber(int i) ;

protected:
    CString a;
    int *p;
};

BOOL CTest::SetNumber(int i) 
{
    BOOL b=FALSE;
    try{
        if(!p)
            p= new int;
        *p = i;
        AfxMessageBox("SetNumber Success" );
        b=TRUE;
    }
    catch(SE_Exception a){
        AfxMessageBox("SetNumber error" );
        b=FALSE;
    }
    return b;
}

CString CTest::GetString()const
{
    CString str;
//    _set_se_translator(SE_ExceptionOp::trans_func); // 일단 한 번 초기화 되면 다시 안 해도 되더군요.
    try{
        str=a;
    }
    catch(SE_Exception e){
        AfxMessageBox("return error of CString" );
        return str="Hehe";
    }
    return str;
}

int CTest::GetNumber()const
{
    int i;
    try{
        if(p)
            i= *p;
        else i= 4;
    }
    catch(SE_Exception e){
        AfxMessageBox("Catched int ptr reading error" );
        i=0;
    }
    return i;
}

CTest::CTest()
{
    a="Default Constructor";
    p=NULL;// memory leak?
}

CTest & CTest::operator=(CTest& other)
{
    _set_se_translator(SE_ExceptionOp::trans_func);// 일단 한 번 초기화 되면 다시 안 해도 되더군요.
    try{
        a = other.a;
        if(p)
            delete p;
        p = new int;
        *p = *(other.p);
        AfxMessageBox("No copying error" );
    }
    catch(SE_Exception e){
        AfxMessageBox("Catched exception.\nThis or source has exception." );
//        _set_se_translator(SE_ExceptionOp::trans_func);
        try{
            destroy();
            CTest();
        }
        catch(SE_Exception e){
            AfxMessageBox("Init again fails" );
        }
    }
    return *this;
}

CTest::~CTest()
{
    destroy();
}

void CTest::destroy()
{    if(p)
        delete p;
}

void SomeFunction()
{
    CTest source, dest;
    CTest *a;// = new CTest;
    *a=source;    // 에러 1 : 할당되지 않은 포인터에 값을 넣으려 합니다.
    dest = *a;    // 에러 2 : 할당되지 않은 영역에서 읽어 옵니다.

    a->SetNumber(8);    // 에러 3 : 할당되지 않은 영역에 쓰려고 합니다.
    
    CString s;
    try{
        // 에러 4. 마지막의 두 인자가 a 를 엑세스 하고 있습니다.

        s.Format("source: %s, %d, dest: %s, %d, a: %s, %d " ,source.GetString(), source.GetNumber()
            , dest.GetString(), dest.GetNumber(), a->GetString(), a->GetNumber());
        AfxMessageBox(s);

    }
    catch(SE_Exception e){
        AfxMessageBox("Wrong string." );
    }
}



조금 긴데... 바로 위에 있는 SomeFunction()이 실행되는 겁니다. 여기에 문제들이 존재합니다.(클래스 자체는 별로 문제 없습니다.) source, dest라는 변수를 사용하고 있구요, a 라는 포인터를 사용합니다.

에러 1. 이건 얄짤없이 잡힙니다. CTest::operator=()를 보시죠.(Annota... 구조를 사용하면 copy()에 해당.) 에러가 두 번 뜬다는 것을 아실 수 있습니다. 일단 "Catched exception.\nThis or source has exception." 이 뜨게 되고 a 가 유효하지 않은 에러이므로 "Init again fails" 이 녀석이 뜨게 됩니다.

에러 2. 역시 당연히 에러가 나지만 "Catched exception.\nThis or source has exception." 이 녀석만 뜹니다. dest는 문제 없으니까요. 그런데... 에러가 나는 것이 당연한 것이 아닙니다. ㅡㅡ; 이걸 알았을 때 상당히 허탈했습니다. 아래에 다시 나오니 그 때 얘기하겠습니다.

에러 3. 금지 영역에 쓰려고 하니 요것 역시 당빠 에러 잡힙니다. 에러 메세지는 "SetNumber error".

에러 4. 이 녀석이 문제입니다. 에러 메세지가 뭐가 나올까요? 디버그 모드에서는 "Catched int ptr reading error" , "return error of CString"가 차례로 나오고 스트링 s 가 출력됩니다. 아주 자연스럽죠? 그치만 릴리즈 모드에서는?? "Wrong string." 이 나옵니다. 멤버 함수 내에서 예외가 잡히는 것이 아니고 호출문에서 예외가 잡힙니다. 헐헐... 호출문의 try,catch는 이 녀석때문에 테스트를 한다고 삽입해 둔 겁니다.


문제가 되는 녀석은 GetNumber()입니다. 이 녀석이 없으면 릴리즈 모드에서도 "return error of CString"가 나오고 스트링 s 가 출력됩니다. 그렇다면 GetNumber()에는 어떤 문제가 있는 것일까요??? 저도 잘 모르겠습니다. 일단 편의를 위해 아래에 다시 한 번 이 함수를 적었보죠.

int CTest::GetNumber()const
{
    int i;
    try{
        if(p)
            i= *p;
        else i= 4;
    }
    catch(SE_Exception e){
        AfxMessageBox("Catched int ptr reading error" );
        i=0;
    }
    return i;
}

- 일단 한 가지 짚어두고 넘어가겠습니다. catch블럭 내부에서는 되도록 리턴하지 말라는 겁니다. 늘 그런 것은 아닌 것 같기도 한데(아직 확실하지 않음) 에러가 납니다. catch블럭이 파괴되면서 리턴값도 사라져 버리는 것 같더군요. 그래서 저는 리턴 타입의 변수를 처음에 선언해두고 try와 catch모두 이 변수를 수정하여 마지막에 리턴하는 방법을 사용하기로 했습니다. 더 나은 방법이 있으시면 리플 주시면 감사...

- CSting형은 읽을 때 바로 에러가 잡힙니다. 그런데 int는 잡히지 않고 호출문까지 가서야 비로소 잡힙니다. 도대체 원인을 모르겠습니다. 갖은 꽁수와 방법을 다 동원해 봤지만 실패... 하지만 한 가지 방법이 동작을 보장해 줍니다. 바로 아래와 같이 하시면 됩니다.

int CTest::GetNumber()const
{
    int i;
    __try{
        if(p)
            i= *p;
        else i= 4;

    }
    __except(EXCEPTION_EXECUTE_HANDLER){
        AfxMessageBox("Catched int ptr reading error" );
        i=0;
    }
    return i;
}

예. SEH를 사용하는 겁니다. 이 녀석을 사용하면 잡아줍니다. 문제의 핵심은 일단 "읽기"의 경우 "쓰기"를 하지 않기 때문인지는 몰라도 try,catch가 오류를 잘 잡지 않는 것 같다는 데 있습니다. "쓰기"를 시도하면 try,catch로도 잡힙니다.

제가 여러 에러 메세지를 비교 분석해 본 결과 다음과 같은 잠정적인 결론을 내렸습니다. 일단 기본 데이터 타입의 경우에는 try,catch로 바로 잡아내는 것이 쉽지 않다는 겁니다. 이런 놈의 오류를 어떻게든 잡아내려면 SEH를 사용해야 한다는 결론입니다. 하지만 SEH를 const형 멤버 함수에 쓰면 되지 않겠느냐 하면 그건 또 그렇지가 않습니다. 소멸자가 존재하는 객체의 경우에는 SEH의 __try 블럭에 사용할 수가 없습니다. 이건 에러 메세지를 보고 알게 된 겁니다.

여러가지 제약이 참으로 많습니다. 그쵸? 답답하긴 하지만 뭐, 별 수 없죠. CString의 경우에는 운 좋게도 try,catch가 잘 잡아 준다는 것이 다행입니다.

그렇다면 어떻게 하는 것이 좋을까요? 만약 const형 Get...() 함수가 데이터를 읽으려고 하는데 이 함수는 여러 항목들을 모두 검사해야 한다면??? ㅡㅡ; try 블럭 안에서 에러가 잘 잡히는 놈을 먼저 사용하는 겁니다. ㅡㅡ; 꽁수라고 밖에는 말씀드릴 수가 없겠네요. 물론 먼저 사용하지 않아도 결국 에러가 잡히지만 이왕이면 에러가 빨리 잡히는 것이 좋겠죠. 단순히 기본 타입만을 사용하는 거라면 그냥 SEH의 __try, __except 구문을 사용하면 되겠고 그 외의 경우에는 에러가 잡히는 놈을 먼저 엑세스하는 겁니다.


에러 1.을 한 번 다시 보시죠. 에러가 발생한 것이 "당연"한 것이 아니란 것을 아실 수 있을 겁니다. 운 좋게도 CString형이 operator=()의 연산 중에 포함되어 있었기 때문에 try,catch가 제대로 동작한 것 뿐입니다.


끝으로 말씀드릴 것은... 데이터 클래스의 멤버 함수들에 try, catch를 사용할 경우, 반드시 테스트를 해 보라는 겁니다. 그래서 제대로 동작하지 않는다면 대책을 세우셔야 할 것 같습니다.


이 쯤에서 줄이죠. 더 말해봐야 헷갈리게만 하지 않을까 싶습니다... ㅡㅡ;


7. 그 외의 주제들.


(1) auto_ptr

STL에서 제공하는 클래스입니다. 이 놈을 사용하여 포인터를 연결해 두면 함수 리턴 시에 연결된 포인터의 객체를 자동으로 삭제해 줍니다. 매우 유용할 것 같아서 소개해 드립니다.

예를 들어서 

func()
{
    int *i = new i;
    try{
        int j=0;
        *i/=j;
        delete i;
    }
    catch(...){
        TRACE("divide by zero" );
        delete i;
        return;
    }
    return;
}

보시다시피 별로 좋지 않은 코드가 만들어졌습니다. delete 코드를 여러 곳에 사용하게 되면 나중에 코드 관리하기가 쉽지가 않죠. 하지만 auto_ptr을 사용하면 간단히 할 수 있습니다.

#include <memory>

func()
{
    auto_ptr<int> a(new i(3));

    try{
        int j=0;
        *i/=j;
    }
    catch(...){
        TRACE("divide by zero" );
        return;
    }
    return;
}

이렇게 delete를 하지 않아도 auto_ptr이 삭제되는 때에 객체 a가 자동으로 i를 delete해줍니다. 주의 사항은 auto_ptr끼리 복사 같은 것은 하지 말라는 겁니다. 무슨 일이 벌어질 지 잘 정의되어 있지 않습니다.
auto_ptr의 원리는 아주 간단합니다. a는 포인터가 아닌 객체이기 때문에 스택에 저장됩니다. 스택의 데이터는 함수가 끝날 때 자동으로 소멸자가 불려지게 됩니다. 따라서 예외 상황의 발생 여부에 상관없이 소멸자가 불려지는 것이 보장되는 겁니다. 이 때 자신에게 연결된 포인터의 메모리를 삭제하는 겁니다. 자세한 사항은 Stroustrup 14장의 auto_ptr을 참고하시기 바랍니다.

(2) 예외 객체의 상속.


비슷한 형태의 객체는 상속을 통해 그룹화가 가능하며 당연히 virtual function을 사용하여 각각에 알맞는 함수 호출도 가능합니다. 결과적으로 catch의 개수를 줄일 수 있게 됩니다.

class Shape{... virtual draw(); ...};
class Rect:public Shape{... virtual draw(); ...};  // public으로 상속해야 하는 것을 잊지 마시길...
class Circle:public Shape{... virtual draw(); ...};

func()
{
    try{
        // throwing Rect, Circle or Shape object...
        throw Rect(); // throw Rect.
    }
    catch(Shape & s){ // 반드시 포인터 형태나 레퍼런스 형태로 선언해야 해당 가상함수가 호출됩니다.
        s.draw(); // Rect::draw()
    }
    catch(...){
        // other routines...
        throw;
    }
}

(3) catch(...) 문제.


문제랄 것은 없고... 남용하는 것이 그다지 좋은 것은 아니라는 겁니다. 자신이 원하는 예외가 아닌, 자신보다 상위의 try,catch 구문이 원하는 예외가 온 것일 수도 있기 때문입니다. 때문에 자신이 처리하는 예외 발생의 원인이 무엇인지 확신이 서지 않는다면 catch(...)를 사용하는 것에 주의를 기울여야 할 듯 합니다. 위의 예제들에서는 모두 catch(...)를 사용하였지만 이것이 일반적인 방식은 아니라는 것에 유의해 주십시오. 위의 상속 예제에서는 throw;라는 문장을 사용하였는데 이것은 re-throw라고 불리는 상황입니다. 자신보다 높은 영역으로 예외 상황을 넘기는 것이죠. 인자는 필요없습니다. 자신에게 넘어온 예외 객체를 상위 레벨로 그냥 토스합니다. 만약 토스시킨 예외객체에 대한 핸들러가 상위 레벨에 없다면... 프로그램은 종료됩니다.(terminamte가 수행되거든요.)


<끝맺는 말>

이 강좌에서는 기본적인 Access Violation 방지 방법의 예를 보여드리려고 했습니다. 기본 주제는 예외 처리구요. 예전 STL 강좌에서도 모든 영역을 다루지 않고 단지 제가 그것을 사용하는 방법에 초점을 맞춰서 보여드렸었죠.(그 방법이 그리 효율적이지 않다 하더라도...ㅡㅡ;) 이번에도 마찬가지였습니다. 고민고민 하다가 요 얼마 전에 만들어낸 따끈따끈한 방법입니다. 하지만..., 제 방법은 그리 효율적이지 않을 수도 있고 바람직한 것이 아닐 가능성이 큽니다. 더 나은 방법이 있으시면 부디 리플 부탁드리겠습니다.


며칠 전에 'Windows API 정복'을 보고 있었습니다. 여전히 머리 속에서는 Access Violation을 처리하는 방법이 없을까... 하는 것을 고민하고 있었죠. 그런데 목차를 보다가(전 지금 7장 읽는 중) 예외 처리에 관한 챕터가 마지막 부분에 있는 것을 알게 되었고 그것을 읽어보니 희망이 생기더군요. 하지만 C++의 문법이랑은 거리가 먼 것이었습니다.(여담이지만... 그 챕터에 나오는 초보자가 사용하는 예외처리 코드에 대한 비웃음의 대상 중에 저도 포함되었다는 것은 생각만 해도 창피하군요... 얼른 코드를 수정해야겠습니다. 제 딴에는 괜찮은 아이디어라 생각했었는데... ㅡㅡ;) 그래서 일단 스트라우스트럽의 책에서 예외처리(8장) 부분을 읽어봤습니다. 하지만 역시 문제가 있었습니다. C++의 것은 OS와의 인터페이스와는 거리가 먼 것이었거든요. 그래도 테스트를 해 봤습니다. 하지만 결과는 역시... "그렇게 쓰면 안 되는데요" 경고만 주루룩... ㅡㅡ;

열심히 MSDN을 찾아보다가 해결의 실마리를 발견했을 때의 기분은 아직도 잊을 수가 없네요. 하지만 여기서 제공한 클래스에 버그가 있다는 것은 약 30분 동안 저를 또 다시 좌절시켰습니다. ㅡㅡ; 어쨌거나 그건 해결되었고,, 데이터 클래스에 임베드 시키는 방법을 생각하고, 에러없이 객체를 사용하는 기본 방법을 만들고 난 후에 느낀 기분은 해방감이었습니다. ㅡㅡ; 하지만 또 다른 장벽, 즉, try,throw,catch로 잡을 수 없는 멤버들을 발견했을 때의 좌절감과 다시 그걸 잡아내는 방법을 알아냈을 때는... 음... 한심함이었습니다. 아직 멀었구나, 난... 하는 기분만 들더군요. 지쳐서일까요? 거의 30시간을 잠을 자지 않고 이거 저거 테스트해 보고 생각해보고 하다 보니... ㅡㅡ;

뭐, 어쨌거나 그냥 알고만 있기에는 아까워서리... 이렇게 강좌를 바로 작성하게 되었습니다. ^^; ... 하지만 이것은 제가 작성한 예외처리 루틴의 첫번째 버전이기 때문에 개선할 여지도 많고 문제점도 있을거라 생각합니다. 위에서 언급한 것들도 많죠. 이 길을 거쳐가신 많은 고수분들의 리플을 바라마지 않습니다.

이런 글을 올리긴 했지만 아직도 저는 릴리즈 모드로 제 프로그램을 성공적으로 런치시키지 못했습니다. 버그 투성이... 첨 프로그래밍을 시작할 때 이 방법을 먼저 알고 시작했었더라면 좀 더 나았을 것을... 하는 생각이 드네요.

이쯤에서 끝맺도록 하죠.
즐거운 연말연시가 되길 바랍니다.
제 글이 크리스마스 선물이 될 수 있다면 더 바랄 나위가 없겠구요.
그럼 이만 총총...

Happy Christmas!!!

ps. 릴리즈 모드에서 디버깅하는 방법 좀 강좌 부탁드립니다. 조금만 자세하게... 도대체가 map 파일을 해석하는 방법을 모르겠네요... ㅡ.ㅡ; 여기 팁란에 올라와 있는 계산법을 봐도 여전히 오리무중... 이해가 잘 안 갑니다...

'NIght.. > Technical Know-How' 카테고리의 다른 글

vector 사용시 추가사항  (0) 2008/10/30
VC로 ActiveX 배포하는 방법 정리 문서  (0) 2008/10/30
try catch문 사용예 3  (0) 2008/10/30
try catch문 사용예 2  (0) 2008/10/30
try catch문 사용예 1  (0) 2008/10/30
Sleep 함수구현  (0) 2008/10/30

Write your message and submit