기본 콘텐츠로 건너뛰기

마소잡지에서 MFC

출처 베르도 사는 이야기 | 베르도
원문 http://blog.naver.com/berdo_seok/70000363288
개발도구 중 가장 강력한 프로그래밍 언어로 손꼽히는 C++. 하지만 명성만큼 강력한 성능을 발휘하는 프로그래밍 기법을 익히는 것은 생각만큼 쉽지 않습니다. 이번 호에서는 C++의 내부를 들여다보고 가장 널리 이용되고 있는 C++ 프레임워크인 MFC를 탐험함으로써 새로운 시각으로 C++ 코드를 볼 수 있는 방법을 제시합니다.

Undocumented C++

1999 년은 필자에게 아주 중요한 의미로 남아 있습니다. 새 천년을 앞둔 마지막 해였으며 영화 매트릭스가 우리에게 첫선을 보였고 예언처럼 과연 지구가 멸망할 것인가가 초미의 관심사였던 때이기도 했습니다. 또한 필자가 처음으로 마소를 통해 여러분들을 만난 것도 1999년이니 기억에 남는 것은 당연할 것입니다. 그때 필자가 썼던 기사의 내용은 MFC를 이용해 MS 오피스와 같은 UI를 구현하는 방법에 대한 것이었습니다. 당시만 해도 대부분의 애플리케이션이 C/S 환경으로 제작되었으므로 가장 강력한 애플리케이션 개발도구인 C++를 사용하는 것이 당연한 것처럼 받아들여졌습니다. 통계를 보더라도 C++는 전 세계적으로 가장 애용되는 프로그래밍 언어였으니까요.
2000년 이후 개발환경이 웹 플랫폼으로 급속히 이전되면서 C++는 가장 많은 개발자가 사용하는 언어의 자리에서 물러나게 되고 자바가 그 자리를 대신하게 되었습니다. 단적으로 필자만 하더라도 2000년 이후부터는 MFC를 이용한 프로그램 개발보다는 자바를 이용한 웹 애플리케이션 개발을 더 많이 하고 있으니까요. 게임이나 PC 애플리케이션을 제외한다면 MFC의 활동 영역은 서버 COM 개발이나 클라이언트 레벨에서 스크립트나 애플릿으로는 불가능한 작업을 위한 액티브X 컨트롤의 개발로 줄어든 것이 사실입니다.

프로그래밍 세계의 The One, C++
자바는 훌륭한 OOP 언어이며 그 전략 또한 멋진 제품입니다. C++와 유사한 문법 구조를 가지고 있어 C++ 프로그래머들을 아주 빠르게 흡수했으며 그들의 만성 편두통의 원인이었던 플랫폼 이식 작업을 자바 가상머신을 이용해 단번에 해결해 주었으니까요. “Write Once Run Anywhere…” 프로그래머로서는 정말 듣기만 해도 가슴이 설레는 문장입니다. 하지만 얻는 것이 있다면 잃는 것도 있는 법. 자바는 플랫폼 독립성을 구현하기 위해 속도라는 중요한 부분을 희생할 수밖에 없었습니다. 또한 모든 플랫폼에서 제공하는 명령어 셋의 공집합만이 자바에서 사용 가능한 부분이 되므로 시스템에 종속적인 프로그래밍은 결코 자바로 구현할 수 없다는 명백한 한계를 가지게 됩니다. 이 때문에 빠른 실행 속도가 필요한 모듈이나 셸 프로그램의 개발은 여전히 C++의 사용 영역으로 남아 있고 C++가 가장 강력한 개발 도구로 불릴 수 있는 이유가 됩니다.
하지만 C++의 진정한 가치는 단순히 이러한 이유에 있지 않습니다. C++를 사용함으로써 얻는 가장 큰 장점은 프로그램의 이면을 보고 “유레카”를 외칠 수 있는 경험을 할 수 있다는 것입니다. C++가 C 언어를 계승하고 그 언어 자체의 탄생 배경이 로우레벨 프로그래밍에 있었다는 것을 생각한다면 C++를 이용해 프로그래밍의 심연을 탐험할 수 있다는 것은 지극히 당연한 결과라 할 수 있습니다. C++를 사용하고 있지만 그런 생각이 별로 안 든다고요? 그렇다면 이 기사가 여러분을 프로그래밍의 깊은 곳으로 인도하는 좋은 안내자가 되어 줄 것입니다.

“여기 두 개의 알약이 있다. 파란색을 선택하면 잠에서 깨어 아무 일도 없었다는 듯 일상으로 돌아가 믿고 싶은 것을 계속 믿으면 된다. 하지만 빨간색을 선택한다면…, 이상한 나라에 남아 끝까지 가게 된다. 명심해. 난 진실만을 제안한다는 것을….” (매트릭스중 모피어스의 대사)

Take a red pill
과연 C++가 프로그램의 이면을 볼 수 있도록 인도할 것인가? 몇 권의 C++ 교재를 탐독하고 다수의 액티브X 컨트롤을 개발해 본 MFC 프로그래머나 자바만을 사용해 온 개발자라면 C++가 그리 특별한 프로그래밍 언어가 아니라고 생각할 것입니다. 맞습니다. 사실 C++ 언어 자체는 그리 완성도가 뛰어난 OOP도 아니며 그다지 사용하기 쉬운 개발 도구도 아닙니다. 그렇다면 C++가 다른 프로그래밍 언어와 가장 큰 차이점은 무엇일까요? 포인터를 사용하고 메모리를 직접 액세스한다는 것 외엔 그리 다른 점이 느껴지지 않습니다. 하지만 이 작은 차이가 바로 모든 것의 출발점이 됩니다. <리스트 1>에 필자가 제시한 간단한 문제가 있습니다. 이 문제가 그 길로 인도하는 첫 번째 열쇠가 될 것입니다.
<리스트 1>은 하나의 멤버 변수와 두 개의 메쏘드를 가진 아주 간단한 클래스와 이를 테스트하는 함수로 구성된 예제입니다. nResult1은 CClass 클래스를 생성(new)하여 Function1을 호출하여 두 수의 합을 얻은 결과입니다. nResult2 역시 Function2를 이용하여 두 수의 합을 얻는 것은 동일하지만 nResult1 후에 생성된 CClass을 제거(delete)하고 클래스 포인터(pClass)에 NULL을 대입한 후 함수 호출을 했다는 차이점이 있습니다. 과연 OnTest() 함수가 호출되면 예제 프로그램은 어떤 결과가 나타날까요?
간단해 보이지만 선뜻 답하기가 쉽지 않을 것입니다. Function2를 호출하는 순간 프로그램은 정지할 것인가, 아니면 결과를 리턴할 것인가, 클래스가 제거된 후에도 결과를 리턴한다면 클래스는 왜 생성해야 한단 말인가…. 많은 생각이 순간적으로 머리를 뒤흔들고 지나갔을 것입니다. 답부터 말하자면 이 코드는 아무런 문제없이 결과를 리턴합니다. 이유를 설명하기 위해 OnTest() 함수의 끝에 다음과 같은 코드를 추가해서 실행시켜 보도록 하겠습니다.

int nResult3 = pClass->Function1(1, 2);

인스턴스의 실체
한 줄의 코드를 추가하고 테스트 코드를 실행하면 좀전까지만 해도 정상적으로 작동하는 프로그램이 비정상 종료되는 것을 확인할 수 있습니다. 그럼 클래스가 삭제되고 동일한 조건에서 실행시킨 Function2와 Function1이 왜 서로 다른 결과가 나타나는 것일까요? 그것은 바로 멤버 변수를 액세스하느냐 그렇지 않느냐에 그 원인이 있습니다. 즉 클래스의 멤버 변수를 액세스할 필요가 있을 때는 클래스가 생성되어 있어야 한다는 것입니다.
우리는 이렇게 생성된 클래스의 실체가 바로 인스턴스인 것을 이미 알고 있습니다. 하지만 인스턴스가 없어도 코드의 실행에는 아무런 문제가 없다는 사실이 거부감없이 받아들여지지 않는 것은 클래스의 코드가 어떻게 수행되고 인스턴스의 실체가 무엇인지에 대해 고민하지 않고 당연한듯 사용하고 있었기 때문일 것입니다.
인스턴스는 클래스 생성자를 호출한만큼 생성됩니다. 하지만 그렇다고 해서 클래스의 함수들도 중복되어 만들어지진 않습니다. C++가 그렇게 비효율적으로 작동되진 않을테니까요. 그럼 여러 개의 인스턴스가 동일한 함수 코드를 사용한다면 함수들은 어떻게 자신이 어떤 인스턴스를 다루고 있다는 것을 알 수 있을까요? 이 대목에서 바로 this 포인터의 정체가 드러납니다.
클래스의 함수가 호출될 때 C++는 그 함수를 호출하는 인스턴스의 포인터를 함수의 인자로 전달하는데 그것이 바로 this 포인터입니다. <리스트 1>에서 Function1이 호출될 때는 pClass 의 포인터가, Function2가 호출될 때는 NULL 값이 this 포인터로 들어갑니다. 기계어로 디스어셈블해서 보면 Register의 ECX를 통해 인스턴스 포인터가 전달되는 것을 확인할 수 있습니다. 따라서 this 포인터를 액세스하지 않는 한, 즉 인스턴스의 멤버 변수에 접근할 필요가 없는 코드는 클래스의 인스턴스가 없더라도 전혀 문제가 발생하지 않는다는 얘기가 됩니다.
<화면 1>은 Function1이 호출됐을 때의 디스어셈블 코드와 메모리 상태이고 <화면 2>는 Function2가 호출됐을 때의 비주얼 스튜디오 화면입니다. 그림에서 박스로 표시된 부분이 바로 this 포인터의 값입니다. this 포인터가 클래스 인스턴스를 가리킨다는 것은 이미 설명했습니다. 그렇다면 인스턴스의 내부는 어떤 모습을 하고 있는지 확인해 보도록 하겠습니다.
<화면 1>의 4분할된 화면 중 왼쪽 면이 바로 인스턴스의 실체를 파악할 수 있는 힌트가 됩니다. 왼쪽 상단 창을 보면 this 포인터가 가리키는 인스턴스의 내용이 표시되고 있고 그 아래쪽에서 실제 메모리 상의 내용을 확인할 수 있습니다. 멤버 변수인 m_nResult의 값이 있는 것은 당연한 일이고 그 위에 있는 __vfptr이 있다는 것에 주목하기 바랍니다. __vfptr은 가상함수를 위해 필요한 포인터 값인데 이에 대해서는 잠시 후 좀더 자세히 설명하겠습니다.
결국 우리는 다음과 같은 결론에 도달할 수 있습니다.

클래스 인스턴스 = 가상함수 포인터(4byte) + 멤버변수에 필요한 메모리
즉 클래스 인스턴스는 스택(stack) 영역이나 힙(heap) 영역에 생성되는 메모리 블럭에 지나지 않습니다. 따라서 CClass 인스턴스는 0x00421320부터 0x00421327까지 8byte의 메모리일 뿐입니다.

가상함수의 작동 원리
C++ 가 객체지향 언어이므로 당연히 가상함수를 지원합니다. 가상함수는 상속성을 구현하기 위한 중요한 요소 중 하나입니다. 가상함수의 개념은 쉬운 듯 하지만 그 강력한 기능을 정말 멋들어지게 프로그래밍한 코드를 쉽게 찾아 볼 수 없음을 상기할 때 그리 녹록한 분야는 아닌 듯 싶습니다. 그렇다고 독자 여러분에게 가상함수에 대한 설명을 반복할 필요까진 없을 것 같습니다. 대신 여기서는 가상함수가 메모리에 어떻게 위치하고 어떤 원리로 적합한 가상함수를 찾아가는지, 그 작동 원리에 대해 설명하겠습니다. <리스트 2>는 가상함수의 작동원리를 테스트하기 위한 예제 소스입니다.
<리스트 2>는 CParent라는 부모 클래스와 이것을 상속한 CChild 클래스를 보여주고 있습니다. 가상함수의 작동원리의 설명을 위하여 Function1은 일반함수로 Function2는 가상함수로 만들었고 함수의 내용은 비워두었습니다. OnTest()는 CChild 클래스를 두 개 생성하여 일반적인 함수 호출을 하도록 한 것과 Base 클래스로 변환하여 동일한 함수를 호출하도록 했습니다.
먼저 클래스가 생성되는 순간 가상함수 테이블 포인터인 __vfptr의 값이 어떻게 변하는지 확인하기 위해 디버깅을 시작합니다. CChild 클래스의 생성자가 호출되면 CChild 클래스의 부모 클래스인 CParent 클래스의 생성자가 다시 호출됩니다. C++ 아키텍처는 CParent의 생성자에서 앞서 설명한 것처럼 __vfptr과 멤버 변수만큼을 메모리에 할당합니다. 부모 클래스의 생성자가 호출된 다음 다시 자식 클래스인 CChild 클래스의 생성자를 실행합니다. 이러한 과정을 거쳐 objChild1과 objChild2가 생성되는데 이때 인스턴스의 메모리 변화는 <표 1>과 같습니다.
이 결과를 통해 우리는 클래스 생성의 비밀을 유추해낼 수 있습니다. 가상함수 테이블 포인터인 __vfptr의 값은 생성 과정에서 변경이 발생합니다. 하지만 여러 개의 인스턴스를 생성한다 하더라도 동일한 클래스의 인스턴스는 같은 가상함수 테이블 포인터를 가지고 있다는 점도 주목하기 바랍니다. 그리고 가상함수와는 상관없긴 하지만 objChild1과 objChild2가 앞서 배운 것처럼 정확히 12byte[가상함수 테이블 포인터(4byte) + CParent 클래스의 멤버 변수(4byte) + CChild 클래스의 멤버변수(4byte)]의 메모리 차를 보인다는 점, 먼저 생성된 objChild1이 나중에 생성된 objChild2보다 높은 메모리 주소를 가지는 것으로 보아 스택은 위에서 아래로 쌓인다는 것도 확인할 수 있습니다.
한 가지 주의할 것은 <표 1>에서 CParent 클래스 생성시 __vfptr과 CChild 클래스 생성시 __vfptr 두 개의 값을 나열하였다고 해서 하나의 인스턴스가 두 개의 __vfptr을 가진다는 것을 의미하는 것은 아니라는 것입니다. 상속된 클래스는 생성 과정에서 부모 클래스의 수만큼 __vfptr의 값이 변경되고 최종적으로 자신의 클래스에 맞는 가상함수 테이블의 값을 사용하게 됩니다.
이러한 구조로 인해 가상함수는 어떠한 베이스 클래스로 형 변환되어 호출되더라도 자신의 가상함수를 호출할 수 있습니다. 또한 서로 다른 인스턴스가 동일한 가상함수 테이블을 사용하도록 구조적으로 설계되어 있어 데이터 중복을 최소화합니다. 앞서 인스턴스가 다르더라도 동일한 함수 코드를 사용한다는 것을 확인했듯이 이제 이러한 구조는 당연하게 받아들여지리라 믿습니다.
가상함수가 호출되는 원리를 알아본 김에 가상함수 테이블의 구조도 한번 확인해 보도록 하겠습니다. <그림 3>에 objChild1에 대한 인스턴스 구조와 __vfptr의 메모리 내용이 나와 있습니다. __vfptr은 가상함수로 선언된 Function2와 클래스 파괴자, 두 개의 가상함수를 가지고 있음을 알 수 있습니다. <화면 3>의 오른쪽 창을 보면 가상 테이블의 실제 내용이 나와 있는데 Function2 함수의 시작 포인터 0x004010F0과 파괴자 코드의 시작 포인터인 0x004010D7이 순서대로 들어있음을 확인할 수 있습니다.
지금까지 확인한 내용을 정리해 보면 어떤 클래스의 일반함수가 호출되면 현재 인스턴스가 형 변환된 형태에 맞게 그 클래스에 해당하는 함수가 호출되지만 가상함수를 호출하면 인스턴스가 베이스 클래스로 형 변환되어 있는 것과 상관없이 인스턴스의 처음 4byte가 가리키는 가상함수 테이블을 찾아내서 호출된 가상함수의 실제 코드가 있는 시작번지로 이동하여 작업을 수행한다는 결론에 도달합니다.

Enter the MFC
클래스, 인스턴스, this 포인터 그리고 가상함수 등은 이미 많은 책에서 설명한 내용입니다. 하지만 대부분의 C++ 입문서가 개념적 설명에 그치고 있고 그러한 것들이 내부적으로 어떻게 사용되고 작동하는지에 대해서는 그 누구도 얘기해 주지 않습니다. 물론 이러한 것들은 그 의미만 파악하고 사용법만 익혀도 프로그래밍하는 데는 전혀 지장이 없습니다. 그러나 동일한 내용이더라도 필자가 앞서 설명한 것처럼 작은 것 하나라도 의심하고 실제 어떻게 작동되는지 직접 확인하는 습관을 들이기 바랍니다. 그러한 과정이 반복되면 C++ 코드가 내부적으로 어떻게 작동되는지 알 수 있게 되고 앞의 테스트에서와 같이 스택 메모리가 어떻게 관리되는지도 부수적으로 배울 수 있는 행운이 찾아오기도 하니까요.
지 금까지의 내용이 C++ 코드의 이면을 볼 수 있는 안내서 역할을 했다면 앞으로 설명할 내용은 MFC의 내부를 들여다보는 방법을 제시할 것입니다. C++ 탐험이 나무를 보는 것이라면 MFC 여행은 숲을 관찰하는 것에 비유할 수 있습니다.

MFC의 핵심요소, CRuntimeClass
우 리 주위에는 항상 곁에 있어 그 고마움과 심지어는 존재 자체를 잊고 살아가는 그러한 것들이 있습니다. MFC의 세계에서는 CRuntimeClass가 바로 그런 존재입니다. 만약 CRuntimeClass가 없어진다면 우리가 사용하는 대부분의 MFC 클래스들은 모두 정상적으로 작동되지 않을 것입니다. MFC 클래스 하이라키(hierarchy) 차트를 보면 90% 이상이 CObject를 최상위 부모클래스로 사용하고 있는 것을 확인할 수 있습니다. 이렇게 중요한 역할을 하는 CObject 클래스의 가장 핵심적인 요소가 바로 CRuntimeClass입니다. 심지어는 CObject에서 상속되지 않은 CArchive, CCreateContext 클래스마저도 CRuntimeClass를 멤버변수로 갖거나 함수의 파라미터로 사용하고 있으니 CRuntimeClass에 대해 알지 못하면서 MFC 구루(guru)가 된다는 것은 불가능하다고 해도 과언이 아닐 것입니다.
그 럼 이렇게 중요한 클래스를 왜 거의 사용한 적이 없는건지 의구심이 생기겠지요. 그것은 CRuntimeClass가 애플리케이션 구현을 위한 기능보다는 MFC 클래스를 관리하고 연결하기 위한 역할이 더 크기 때문입니다. 이런 이유로 우리는 CRuntimeClass를 직접적으로는 거의 사용하지 않고 있습니다. 하지만 CRuntimeClass는 우리가 의식하지 못하는 사이에 이미 여러분의 코드 깊숙한 곳에 자리잡고 있다는 것을 명심하십시오.

CMainFrame의 생성 과정
그럼 CRuntimeClass가 얼마나 MFC와 밀접한 연관이 있는지 또 MFC 내에서 어떠한 용도로 사용되고 있는지 확인해 보도록 하겠습니다. 가장 기본적인 사용방법을 알아보기 위해 먼저 실행파일 생성을 위한 MFC 프로젝트(MFC AppWizard(exe))를 하나 만들기 바랍니다. 이때 Dialog based를 제외한 Single document, Multiple documents를 선택하는 것을 잊지 말기 바랍니다. 이렇게 생성된 프로젝트 파일 중 MainFrame.h 파일을 열어 생성자를 보면 다음과 같이 되어 있는 것을 확인할 수 있습니다.
class CMainFrame : public CFrameWnd
{
protected: // create from serialization only
    CMainFrame();
    DECLARE_DYNCREATE(CMainFrame)
생 성자 바로 위를 보면 생성자가 protected 타입으로 선언되어 있는 것을 볼 수 있습니다. protected 타입의 속성상 외부에서의 함수 호출이 불가능하다는 것은 이미 알고 있는 사실입니다. 그렇다면 CMainFrame은 과연 어떻게 생성되고 우리가 사용하고 있는 것일까요? 이런 궁금증을 한번도 가져본 적이 없나요? 그렇다면 지금부터라도 이런 의문을 갖는 습관을 들이기 바랍니다. 그게 바로 MFC 탐험의 초석이 되니까요.
궁금증을 풀기 위해 CMainFrame이 생성되는 코드를 따라가 보도록 하겠습니다. CMainFrame은 애플리케이션의 메인 윈도우입니다. 따라서 CWinApp의 InitInstance() 함수를 상속한 곳에서 윈도우를 생성한다는 것을 추론할 수 있습니다. 방금 생성한 프로젝트 App 클래스의 InitInstance() 함수를 확인해 보지만 어디에도 CMainFrame을 생성하는 코드는 없습니다. 단지 DocTemplate을 하나 만들고 실행 명령문을 파싱하고(ParseCommandLine 함수) ProcessShell Command() 함수를 호출할 뿐입니다. 이중 윈도우 생성이 일어날 만한 곳은 ProcessShellCommand() 이므로 MFC 소스가 위치한 곳에서 VC++의 ‘Find in Files’를 실행하여 원본 함수가 있는 곳을 찾아보기 바랍니다. ProcessShell Command() 함수의 원본 소스를 보면 OnFileNew()를 호출하는 것을 알 수 있고 다시 원본 소스 코드를 확인하고 추적하는 작업을 반복하면 결국 CDocTemplate 클래스의 CreateNewFrame() 함수에 도달하게 됩니다. 그리고 이 함수 내의 코드 중 다음과 같은 부분을 찾아볼 수 있습니다.

CFrameWnd* pFrame = (CFrameWnd*)m_pFrameClass->CreateObject();

바 로 MFC의 이 라인에서 여러분이 만든 메인 프레임은 생성됩니다. 여기서 m_pFrameClass는 InitInstance()에서 DocTemplate를 생성할 때 세번째 인자로 넘겨준 CMainFrame 클래스의 CRuntimeClass 포인터입니다. CRuntimeClass는 RUNTIME_CLASS(CMainFrame)와 같이 MFC 매크로에 의해 얻을 수 있습니다. MFC 원본 소스를 보면 RUNTIME_CLASS() 매크로는 다음과 같이 정의되어 있습니다.

#define RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name))

따 라서 RUNTIME_CLASS(CMainFrame)는 (CRuntime Class*)&CMainFrame::classCObject로 바꿔 쓸 수 있으며 어떠한 문제도 일으키지 않습니다. 이것은 CMainFrame이 CObject에서 상속됐으며 CObject는 CRuntimeClass를 public 변수로 가지고 있기 때문입니다.
하지만 CObject에서 상속받은 클래스가 모두 CRuntimeClass의 기능을 사용할 수 있는 것은 아닙니다. <리스트 2>의 CParent 클래스를 COjbect에서 상속되도록 수정한 후 RUNTIME_CLASS() 매크로를 사용하도록 시도하면 컴파일 에러가 발생하는 것을 확인할 수 있습니다. CRuntimeClass의 기능을 사용하기 위해서는 또 다른 매크로의 도움을 필요로 합니다.

CRuntimeClass의 세가지 사용 레벨
RUNTIME_CLASS() 매크로의 선언문을 보면 CParent 클래스에 classCParent라는 static 변수가 선언되어 있어야만 한다는 것을 알 수 있습니다. 그렇다면 classCParent라는 static 변수는 언제 선언되고 언제 그 값이 정해지는 것일까요? 그 답을 찾기 위해 ‘class##class_name’이라는 문장을 다시 MFC 내부 소스를 검색할 필요가 있습니다. 그 결과, 우리는 이 변수를 사용하기 위해서는 다음과 같은 매크로를 사용해야 한다는 것을 알 수 있습니다.
DECLARE_DYNAMIC(class_name)
IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, pfnNew)
COject 에 상속받도록 수정한 CParent 클래스의 생성자 밑에 DECLARE_DYNAMIC(CParent)을, 그리고 클래스를 사용하는 적당한 곳에 IMPLEMENT_RUNTIMECLASS (CParent, CObject, 0, NULL)라는 두 개의 매크로를 삽입함으로써 드디어 CRuntimeClass를 사용할 수 있는 준비가 끝납니다. MFC에서는 IMPLEMENT_RUNTIMECLASS를 직접 사용하기보다는 선언 매크로와 쌍을 이룬 매크로를 사용할 수 있도록 준비해 놓았습니다.

DECLARE_DYNAMIC(class_name)
IMPLEMENT_DYNAMIC(class_name, base_class_name)
IMPLEMENT_DYNAMIC 은 IMPLEMENT_RUNTIME CLASS와 동일하며 이것이 CRuntimeClass의 첫 번째 사용 레벨입니다. 두 번째 사용 레벨은 앞서 CMainFrame의 동적 생성과 관련된 것입니다. DECLARE_DYNAMIC을 CParent 클래스에 추가한 상태에서 CRuntimeClass를 이용한 동적 생성을 시도해 보기 바랍니다.

CParent pParent = (CParent*)RUNTIME_CLASS(CParent)->CreateObject();

컴 파일 오류는 발생하지 않습니다. 하지만 프로그램을 디버깅해 보면 pParent에 NULL 값이 넘어온다는 것을 알 수 있습니다. 즉 동적 생성이 되지 않은 것이죠. 원인을 알기 위해 디버깅을 해 보면 CRuntimeClass의 멤버변수이며 클래스의 동적 생성 함수를 가리키는 m_pfnCreateObject 값이 NULL이기 때문임을 확인할 수 있을 것입니다. 동적 생성을 위해서는 m_pfnCreateObject 값을 셋팅해 줘야 하고 그러기 위해 또 다른 매크로의 도움을 필요로 합니다.

DECLARE_DYNCREATE(class_name)
IMPLEMENT_DYNCREATE(class_name, base_class_name)
이 매크로는 앞서 보여준 CMainFrame의 헤더 파일에서 보았던 것이며 MFC 프로젝트 위저드로 생성된 대부분의 파일에서 사용되는 매크로입니다. 이것으로 우리는 개념적으로 알고 있었던 클래스 동적 생성에 대한 내부적인 구현 방법을 알아보았습니다. 그렇다면 new라는 C++ 문법이 엄연히 존재함에도 왜 이런 동적 생성을 필요로 하는 것일까요? 그 첫 번째 이유는 클래스 설계자의 의도 때문입니다.
CMainFrame이나 CDocument, CView 클래스들은 생성자가 모두 protected로 선언되어 있으므로 new로 생성할 수 없습니다. 물론 개발자가 public으로 수정하여 사용한다면 가능은 하겠지요. 하지만 굳이 protected로 해놓은 것은 이 클래스를 사용하는 개발자가 각각의 클래스를 독자적으로 생성하여 사용하지 말라고 얘기하는 것과 같습니다. OOP에서는 문법 자체가 설계자의 의도와 주석을 대신할 수 있으니까요.
CMainFrame의 생성 과정을 쫓아가 보았다면 프레임과 도큐먼트와 뷰가 많은 부분에서 얽혀있고 서로가 서로를 필요로 한다는 것을 알 수 있습니다. 따라서 이 세 개의 클래스들은 생성 과정과 삭제 과정에서 일정한 순서를 지켜 작업이 이루어져야 하므로 이러한 생성, 삭제를 개발자들이 임의대로 사용하지 못하도록 하기 위한 것입니다.
그렇다고 꼭 MFC 프레임워크에서만 생성이 가능한 것은 아닙니다. 필자는 C/S 애플리케이션의 기능을 웹 버전에서 사용하기 위하여 프레임, 도큐먼트, 뷰 구조를 액티브X 컨트롤로 개발해야 할 필요가 있었는데, 이러한 구조가 CDocManager, CWinApp와 너무 밀접한 관계가 있어 전체를 다시 코딩해야 하는 문제가 발생한 적이 있습니다. 궁여지책 끝에 베이스가 되는 MFC 클래스를 필자가 직접 설계하여 구현하고 기존에 개발한 클래스를 직접 설계한 클래스에서 상속받도록 수정해서 수많은 클래스들을 새로 만들어야 했던 위기에서 단지 3개의 클래스만을 추가하여 문제를 해결할 수 있었습니다. MFC의 내부구조를 자주 봐두면 이러한 어려움이 있을 때 그 능력을 발휘하여 생각보다 쉽게 문제를 해결할 수도 있습니다.

DECLARE_SERIAL(class_name)
IMPLEMENT_SERIAL(class_name, base_class_name, wSchema)
CRuntimeClass 의 마지막 사용레벨은 직렬화와 관련이 있습니다. 직렬화를 사용하기 위해서는 이 두 매크로를 사용해야 합니다. 이 매크로는 직렬화에서 사용하는 오퍼레이터인 “>>”를 구현하고 있습니다. 이로 인해 우리는 문서를 저장하고 객체를 저장할 때 Serialize() 함수를 좀더 쉽고 직관적으로 구현할 수 있습니다. 물론 IMPLEMENT_SERIAL은 IMPLE MENT_DYNCREATE와 IMPLEMENT_DYNAMIC의 기능이 모두 구현되어 있습니다. 그래서 필자는 이것을 사용레벨이라고 설명하였고 상위레벨은 하위레벨의 모든 기능을 사용할 수 있습니다. 따라서 직렬화된 데이터를 읽어들일 때 저장된 데이터에 맞는 클래스가 동적 생성되는 기능을 사용할 수 있는 것입니다.

MFC 메시지 맵의 작동원리
MFC에서 기본적으로 생성하고 또 가장 많이 쓰이는 매크로가 무엇을 구현하고 있고 내부적으로 어떻게 작동되는지에 대해서 알아봤습니다. 이제 메시지 맵과 관련된 부분만 알게 된다면 MFC 프로젝트 위저드에서 생성되는 클래스들의 내용을 대부분 이해할 수 있게 됩니다. 메시지 맵도 선언을 위한 매크로와 구현을 위한 매크로의 쌍으로 사용되고 있습니다.

DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(theClass, baseClass)
END_MESSAGE_MAP()
이 매크로에 대한 내용은 앞서 설명한 방법을 사용하면 어렵지 않게 그 내용을 확인할 수 있으므로 자세한 설명은 생략하겠습니다. 여기서는 단순히 MFC가 어떻게 윈도우 메시지를 각각의 클래스에 전달하도록 구현되어 있는지 그 원리만 간략히 설명하겠습니다.
MFC 는 클래스 위저드를 이용해 추가한 메시지들을 BEGIN_MESSAGE_MAP과 END_MESSAGE_MAP 사이에 지정된(ON_WM_PAINT()와 같은) 매크로로 변환하여 삽입합니다. 물론 메시지 맵 매크로를 알고 있다면 위저드를 사용하지 않고 직접 코딩으로 추가할 수도 있습니다(메시지 맵 매크로는 AFXMSG_.H 파일에서 확인할 수 있습니다).
메시지 맵은 생각보다 아주 단순한 구조를 가지고 있습니다. 단순히 메시지 전달에 필요한 데이터들(메시지 타입, LPARAM, WPARAM으로 전달될 데이터, 호출될 함수의 포인터 등)을 _messageEntries[]라는 배열에 저장하여 메시지를 전달하는 구조로 구현되어 있습니다.
즉 윈도우 메시지가 발생하면 CWinApp는 메인 윈도우를 시작으로 모든 하위 윈도우들에게 메시지를 전달하기 시작합니다. 이때 각각의 윈도우를 구현한 클래스들은 _message Entries[]에 담겨져 있는 메시지 데이터들과 비교하여 자신이 처리할 메시지가 있는지 판단합니다. 물론 이러한 과정은 _GetBaseMessageMap()이라는 함수를 이용하여 베이스 클래스까지 전달되도록 되어 있습니다. 이러한 일련의 과정 중 메시지를 처리할 합당한 메시지 맵을 만나게 되면 메시지 처리를 하고 메시지 라우팅은 종료됩니다.
이처럼 MFC 메시지 맵이 배열을 이용한 간단한 구조로 되어 있어 MFC 내부를 들여다 본 사람들에게 공격의 대상이 되기도 합니다. 필자도 그중 하나이지만 사실 이보다 더 효율적인 구조가 떠오르진 않습니다. 클래스와 리스트를 이용해서 더 편하게 쓸 수 있도록 구현할 순 있겠지만 이보다 더 빠르고 작은 데이터로 구성할 수 있는 방법은 없을테니까요.

C++ Reloaded
이제 MFC 프로젝트 위저드가 생성한 파일들을 다시 열어보십시오. 그전까지 뭘 하는지도 모를, 단순한 매크로로 보였던 부분들의 내부가 보이기 시작합니까? 그리고 내가 추가한 메시지가 함수로 전달되는 과정도 눈에 보이기 시작할 것입니다. 영화 매트릭스를 보면 오퍼레이터들은 화면에 흐르는 녹색 글자들을 보며 매트릭스 세계에서 일어나는 일들을 알아냅니다. 반대로 네오는 매트릭스 세계를 단순한 데이터들의 집합으로 보는 능력도 가지게 됩니다. 영화에서만 일어나는 일이라고요? C++ 프로그래머들에겐 영화 내용 중 가장 공감이 가는 장면입니다.
필자는 이 글을 통해 C++의 내부를 들여다 보고, MFC의 원본 소스를 탐험하는 방법을 설명하고자 했습니다. 당장 사용 가능한 기술도 중요하겠지만 기초가 튼튼해야 그 위로 많은 지식을 쌓을 수 있는 법이니까요. 설명한 예가 너무 구시대의 유물로 받아들여질 수도 있을 것입니다. 하지만 액티브X 컨트롤 프로젝트에 사용된 클래스들도 앞서 설명한 예제의 범주에서 크게 벗어나지 않습니다. MFC를 사용하지 않는 COM으로 프로젝트를 하는 경우 이러한 MFC 내부 탐험 방법은 더욱더 그 진가를 발휘할 것입니다. COM에서는 정말 의미도 파악하기 힘든 매크로들이 너무도 많으니까요.
C++ 내부를 보기 위한 노력은 하드웨어에 대한 이해를 넓혀주고 간단한 어셈블리어를 자연스럽게 배울 수 있도록 하는 부수적인 효과도 있습니다. 또한 MFC 원본 소스 보기는 정말 C++ 프로그래머들에게 권장하고 싶은 일 중 하나입니다. MFC 소스만큼 훌륭한 C++ 교과서도 없을뿐더러 문서화되지 않은(심지어는 MSDN에도 등록되어 있지 않은) AFX_ZERO_INIT_OBJECT 매크로나 AfxFindResource Handle() 등의 유용한 함수들을 찾아낼 수 있고 사용할 수 있게 됩니다. 이렇게 쌓은 지식은 다른 언어, 다른 플랫폼에서 개발을 하더라도 아주 큰 도움이 될 것입니다. 이제 C++을 재장전 하십시오 ‘Revolution’은 여러분의 몫입니다.


<출처>
www.imaso.co.kr

댓글

이 블로그의 인기 게시물

80040154 오류로 인해 CLSID가 {xxxx-...}인 구성 요소의 COM 클래스 팩터리를 검색하지 못했습니다.

원문보기 .NET 으로 만든 응용프로그램에서 com 객체를 호출한 경우 Windows7 64bit 에서 제목과 같은 에러가 발생했다. Win32 COM 과 .NET 프로그램간의 호환성 때문에 생긴 문제였다. 원인은 .NET 실행시 JIT 컴파일러에 의해 최적화된 기계어로 변환되기 때문.. Win32 COM은 컴파일시.. Win32 COM에 맞춰 빌드 속성에서 하위버전으로 맞춰 컴파일을 다시하는 방법도 있지만 메인 프로젝트가 .NET이라면 참조되는 모든 프로젝트를 다 바꿔야할 노릇.. 또 다른 방법은 COM+를 이용하여 독립적으로 만드는 것이다. 분리시키는 방법은 아래 주소해서 확인할 수 있다. http://support.microsoft.com/kb/281335 나의 경우는 Win32 COM DLL을 64비트 .NET 프로그램에서 참조하니 COM 객체를 제대로 호출하지 못하였습니다. 그래서 .NET 프로그램의 Target Machine을 x86으로 설정하니 제대로 COM 객체를 호출하였습니다.

[Pyinstaller] 실행 파일 관리자 권한 획득하기

고객사에서 일부 사용자에게서 프로그램 오류가 발생한다며 아래와 같이 에러 캡처를 보내왔습니다. 프로그램에서 로그를 남기기 위해 로그 파일을 생성하는데 권한의 문제로 로그 파일을 생성하지 못해 프로그램 오류가 발생한 것 같습니다. 처음에는 Python 코드에서 관리자 권한을 요청하는 코드를 넣으려고 했는데, 실제로 Stackoverflow를 찾아보면 이런 내용이 나옵니다. 프로그램이 관리자 권한으로 실행되지 않았다면 관리자 권한으로 다시 프로그램을 실행시키는 코드입니다. import os import sys import win32com.shell.shell as shell ASADMIN = 'asadmin' if sys.argv[-1] != ASADMIN: script = os.path.abspath(sys.argv[0]) params = ' '.join([script] + sys.argv[1:] + [ASADMIN]) shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params) sys.exit(0) 하지만 개인적으로 이런 방식은 마음에 들지 않았고 조금 더 찾아보니 Pyinstaller로 exe 파일을 만들 때 옵션을 설정하여 관리자 권한을 요청하도록 할 수 있다고 합니다. --uac-admin을 옵션에 추가하면 프로그램 실행 시 관리자 권한을 요청할 수 있습니다. pyinstaller.exe --uac-admin sample.py 하지만 안타깝게도 이 방식은 원하는 대로 동작하지 않았습니다. 마지막으로 manifest 파일을 이용하여 시도해보았습니다. spec 파일을 이용하여 pyinstaller로 빌드하면 <실행 파일 이름>.manifest 라는 파일이 생성됩니다. 파일에서 아랫부분을 찾아볼 수 있습니다. <security> <re

초간단 프로그램 락 걸기

프로그램에 락을 걸 일이 생겨났다. 하드웨어 락을 걸면 쉬울텐데 그 정도는 아니고 프로그램의 실행 날짜를 제한 해 달라고 한다. 그래서 파일(license.lic)을 가지고 락을 걸리고 결정을 했다. 요구 사항은 아래와 같다. 1. license.lic 파일이 없으면 프로그램을 실행 할수 없게 한다. 2. 지정한 날짜를 넘어서는 프로그램을 실행 할수 없게 한다. 3. 사용자가 시스템 날짜를 되돌렸을때 인식하여 프로그램을 실행 할수 없게 한다. 음.... 1.번 문제는 사용자가 프로그램을 실행하기 위해서 license.lic 파일을 받아야만 한다. license.lic 파일에는 최근 실행 날짜/종료날짜 이런식으로 적도록 한다.(물론 내용은 암호화 한다.) 최근 실행날짜는 프로그램이 실행때마다 업데이트 하도록 하고 시스템 날짜와 비교하여 시스템 날짜가 최근 실행 날짜보다 이전의 날짜면 시스템 날짜를 되돌렸다고 인식하도록 한다.(3.번 문제 해결) 시스템 날짜와 종료 날짜를 비교하여 시스템 날짜가 종료 날짜를 넘으면 프로그램을 실행 할수 없도록 한다.(2.번 문제 해결)