. 컨트롤의 일생

오브젝트화가 성공적으로 완료되기는 했는데 그다지 쉬운 실습은 아니었다. 설명을 읽고 다 이해했다면 좋겠지만 아무래도 좀 복잡하므로 실행 순서를 점검해보고 객체가 어떻게 만들어지고 사라지는지 보면서 코드를 다시 한 번 살펴 보도록 하자. 먼저 정적으로 선언되는 msg 객체의 일생을 보자.

CShowMsg msg; 선언문에 의해 객체가 생성되고 생성자는 아무것도 하지 않는다. 메인 윈도우의 WM_CREATE에서 객체의 Create 멤버함수를 호출하면 이 함수에서 CreateWindow 함수를 호출하여 윈도우를 만드는데 마지막 인수로 this 포인터를 전달했다. 이때 WM_NCCREATE 메시지가 발생하며 이 메시지는 ShowMsgProc으로 전달된다. 메시지 프로시저는 이 윈도우가 맵에 등록되어 있는지 조사하는데 등록되어 있지 않으면 AddObject 함수를 호출하여 새로 등록하며 이때 객체의 hWnd 멤버변수에 윈도우 핸들이 대입된다. 곧이어 WM_CREATE 메시지가 발생하며 이 메시지를 처리할 때는 객체가 맵에 등록되어 있으므로 객체 포인터를 찾을 수 있고 객체의 OnMessage 함수로 처리가 넘겨진다.

OnMessage에서는 WM_CREATE 메시지를 받았을 때 멤버변수 x, y를 초기화하고 str 버퍼의 메모리를 할당한다. 이후 WM_KEYDOWN, WM_PAINT 등의 메시지도 같은 방법으로 OnMessage로 전달될 것이며 이 과정은 윈도우가 파괴되기 전까지 계속 반복된다. msg2 객체도 물론 완전히 동일한 순서대로 생성되는데 msg 객체와는 다른 맵에 등록될 것이며 고유의 멤버변수값을 유지한다.

다음은 동적으로 생성되는 msg3 객체가 어떻게 만들어지는지 보자. 포인터 변수를 선언할 때는 아무 일도 일어나지 않으며 스택에 4바이트만큼 공간이 할당될 뿐이다. new 연산자로 객체를 생성할 때 생성자가 호출되며 역시 아무 일도 일어나지 않는다. Create 함수가 호출될 때는 msg 객체와 마찬가지 순서대로 객체 맵에 등록되고 이후 실행되는 과정은 동일하다.

정적으로 선언되는 객체나 동적으로 만들어지는 객체나 생성과정은 완전히 동일하다. 그러나 파괴되는 과정은 상당한 차이가 있어 코드 작성에 주의해야 할 점이 많다. 정적으로 생성되는 msg 객체는 메인 윈도우와 일생을 같이 하며 일부러 파괴시키는 코드가 없다. 그래서 프로그램이 종료될 때 컨트롤 윈도우도 같이 파괴된다. 윈도우가 파괴될 때는 WM_DESTROY 메시지가 발생하는데 이 메시지를 받았을 때 str 버퍼를 해제하는 내부적인 종료 처리를 한다. 그리고 윈도우는 파괴된다.

프로그램이 종료될 때는 윈도우뿐만 아니라 모든 전역변수들도 파괴되므로 msg 객체도 파괴된다. msg 객체의 파괴자에서는 맵에서 자신을 제거해야 하는데 이 경우는 그럴 필요가 없다. 왜냐하면 프로그램이 종료되는 특수한 상황이기 때문에 더 이상 맵을 관리할 필요가 없기 때문이다. 맵에서 자신을 제거하고 싶어도 제거할 맵이 아직 있다고 보장할 수도 없다. msg객체나 도우미 객체나 둘 다 전역변수이므로 어떤 변수가 먼저 파괴될 지 알 수가 없는 것이다. 실제로 테스트해보면 msg 객체보다 도우미 객체가 먼저 파괴되는데 정확한 파괴 순서는 컴파일러에 따라 조금씩 달라질 수 있다. 이 상황에서 맵을 액세스하려고 하면 에러만 발생할 것이다. 그래서 도우미는 죽기 전에 파괴자에서 arObj NULL을 명시적으로 대입하여 나는 이미 죽었음. 건드리지 말 것을 표시해놓는 것이다. 그래서 객체의 파괴자는 도우미 클래스의 arObj를 확인하여 도우미가 아직 살아 있는지 확인한 후에야 RemoveObject 함수를 호출한다.

실행중에 동적으로 만들어졌다가 파괴되는 msg3 객체는 파괴 순서가 좀 다르다. 정적으로 선언된 msg는 윈도우가 먼저 파괴되고 객체가 파괴되지만 msg3는 반대로 객체가 먼저 파괴되면서 자신의 윈도우를 파괴해야 한다. delete 연산자는 객체를 파괴하는 것이지 그 객체가 관리하는 윈도우 따위는 아예 존재 자체를 모른다.

delete 연산자에 의해 객체가 파괴될 때 파괴자가 호출되며 파괴자는 RemoveObject 함수를 호출하여 맵에서 객체를 제거한다. 이때 객체를 제거하기 전에 윈도우를 먼저 파괴해야 한다. 왜냐하면 윈도우 파괴시 WM_DESTROY 메시지가 전달되고 이 메시지에서 컨트롤의 종료 처리를 해야 하는데 컨트롤의 OnMessage가 이 메시지를 제대로 받으려면 객체 맵에 아직 객체가 남아 있어야 하기 때문이다. 그래서 윈도우를 먼저 파괴한 후 객체를 맵에서 제거하도록 하였다. 순서가 바뀌면 치명적인 에러 원인이 된다.

객체가 파괴되는 세 번째 유형은 객체의 윈도우만 DestroyWindow 함수로 파괴할 때이다. 이 경우는 앞의 두 경우보다 더 골치가 아픈데 왜냐하면 객체는 그대로 있고 객체가 관리하는 윈도우만 파괴되기 때문이다. DestroyWindow 함수로 윈도우를 파괴하면 WM_DESTROY 메시지가 전달되고 이 메시지에서 str 버퍼를 정리한다. 그리고 끝이다. 객체는 그대로 남으며 파괴자가 호출되지 않는다. 이 상황이 과연 바람직할까?

객체가 삭제되면 이 객체가 관리하는 윈도우는 당연히 파괴되어야 한다. 그렇다면 같은 논리로 윈도우가 파괴되면 객체도 같이 삭제되어야 하는가 하면 그렇지는 않다. 윈도우가 없더라도 객체는 그대로 존재할 수도 있다. 왜냐하면 객체는 다시 사용할 수 있는 변수이므로 윈도우가 사라져도 다시 Create 함수를 호출하여 윈도우를 또 만들 수 있기 때문이다. 그래서 윈도우가 파괴될 때 굳이 delete this를 할 필요가 없다.

윈도우없이 객체만 존재하는 상황은 적어도 문법적으로는 합법적이다. 그러나 이런 상황을 그냥 내버려 두면 여러 가지 부작용이 생길 수 있으므로 예외 처리가 필요하다. 윈도우가 없는 객체가 있는 상태에서 프로그램을 종료하면 이 객체의 파괴자에서 DestroyWindow 함수로 윈도우를 파괴하려고 할 것이고 윈도우 핸들이 무효한 상태에서 파괴할 수 없으므로 에러가 발생한다. 이 상황을 방지하기 위해서 RemoveObject 함수는 DestroyWindow를 호출하기 전에 IsWindow 함수로 윈도우 핸들이 유효한지 체크를 해보고 유효한 윈도우 핸들에 대해서만 DestroyWindow 함수를 호출하도록 되어 있다.

이보다 더 큰 부작용은 윈도우가 없는 객체에 대해서도 멤버함수를 호출하거나 SendMessage로 메시지를 강제로 보낼 수 있다는 점이다. 그러면 멤버함수는 무효한 핸들에 대해 InvalidateRect 따위의 함수를 호출해 댈 것이며 이것도 에러 원인이 된다. 그래서 이런 상태를 방지하기 위해 모든 멤버함수는 윈도우가 유효한지 체크해 봐야 할 의무가 있다. 그렇다면 속도 감소의 불이익이 있더라도 일일이 IsWindow를 불러 봐야 하는가? 이것은 개발자가 선택할 문제인데 나는 그렇게 하지 않았다.

사용자가 DestroyWindow로 윈도우를 파괴했으면 이 윈도우를 쓰지 않겠다는 의사 표현을 한 것인데 다시 멤버함수를 호출한 것은 명백한 유저 불량이므로 개발자가 이런 문제까지 신경써 줘야 할 의무는 없다고 생각한다. 컨트롤의 사용자는 최종 사용자가 아니라 개발자이며 이런 실수를 했다면 개발중에 알 수 있으므로 실행속도의 저하를 감수해 가며 굳이 윈도우의 유효성을 점검해 볼 필요가 없다.

MFC의 윈도우 객체들도 똑같은 문제가 있는데 MFC는 윈도우의 유효성을 일일이 점검하고 있다. , IsWindow 함수를 호출하기는 하되 이 호출 문이 ASSERT매크로 안에 있어 디버깅중에만 핸들을 점검하도록 함으로써 실행시간의 낭비는 없도록 하였다. MFC 예제에서 다음과 같은 코드를 작성해보자

 

     CButton Btn;

     Btn.SetWindowText("방금 만든 버튼");

 

CButton 객체만 만들고 윈도우는 만들지 않았다. 이 상태에서 SetWindowText 함수로 윈도우의 캡션을 바꾸려고 하면 당장 에러가 난다. 왜냐하면 윈도우 핸들이 무효하기 때문이다. MFC의 프레임 워크는 이 상황을 다음과 같이 처리하고 있다.

 

void CWnd::SetWindowText(LPCTSTR lpszString)

{

     ASSERT(::IsWindow(m_hWnd));

     ....

 

CWnd로부터 파생되는 클래스의 모든 멤버함수들은 선두에서 윈도우 핸들이 유효한지 일일이 점검한다. 상용 라이브러리이기 때문에 만약에 있을지 모르는 개발자의 실수까지도 이런 식으로 알려주는 배려를 하는 것이다. 하지만 어디까지나 에러가 있다는 것을 알려만 줄 뿐이지 에러 상황을 처리하지는 않는다. 위와 같은 코드를 그대로 릴리즈하면 즉시 사망하는 프로그램이 만들어진다.