VS.NET 의 새로 추가된 특징 - C++ Attributes : 한결 더 쉬워진 COM 개체
개발

원저자 : Richard Grimes ( [email-masked]
)

원본 기사 URL : http://msdn.microsoft.com/msdnmag/issues/01/04/attributes/print.asp
기사와
관련된 코드 : http://user.chollian.net/~kevin13/srclist/article/cppattr.zip

다음의 번역이 옳다면 정성태([email-masked]: http://user.chollian.net/~kevin13)
가 한 것이고, 그렇지 않다면 그가 한 것이 아닙니다. 단지, 틀린 점이 있다면, 메일부탁드립니다. 조언을
기다립니다.

[역자 서문]
이 글은, 제가 임의로 번역한 것입니다. 원문 번역을 충실히 하지
않았으며, 개인적인 의견으로 필요없겠다 싶은 말들은 과감히 삭제를 했습니다. 지금까지 공부해 오면서, MSDN
Magazine 에 MS 기술의 많은 부분들이 공개되어 있는 것을 경험했습니다. 영문이기 때문에, 쉽게 다가서지
못하는 분들께 조금이라도 그 기술을 알려드리고자 하는 뜻에서 번역을 한 것입니다. 저는 영어를 그리 잘 하는
편이 아니므로, 다소 제가 이해를 잘못한 부분이 있을 수 있으므로, 그에 대한 책임은 질 수 없습니다. 그리고,
저도 현업에서 일하고 있기에 쓸데 없는 문장까지 일일이 번역을 하는 수고는 할 수 없습니다. 기사의 원
저자에게는 정말 죄송합니다. 참고로, 여기서 제가 쓰는 모든 글은 어쨌든, "원 저자" 가 쓴 글을 읽고 제가
나름대로 이해해서 쓴 것입니다. 따라서, 여기 있는 글에 대한 모든 저작권은 제가 아니라, 바로 그 "원 저자"
에게 있습니다. 혹시나, 여기 있는 번역글을 다른 곳에 쓰신다면, 반드시 그 출처에 "원 저자" 의 이름을
밝혀주시기 바랍니다. 저는... ^^ 넣지 않으셔도 좋습니다. 아울러, 이 글을 시작으로 해서 많은 분들이,
틈나는 데로 이러한 번역을 해서 "웹" 상에 공개를 하신다면, 우리나라의 S/W 발전에 많은 도움이 되리라
생각해 봅니다. 서문... 이만 줄입니다.

* 이 문서에서 링크되어져 있는 표는 이
문서의 맨아래에 나열되어져 있습니다.

[번역 문서
Version 1.0: 처음 작성일 2002.07.07]
수정 내역 : 2002.07.08 - ATL 7.0 에서의
함수 export 문제점 1 문서 수정

난이도     1   2   3

요약 VS.NET 에 새롭게 추가된 "C++ 속성" 은 "속성제공자" 를 통해서 C++ 코드로
대체됩니다. "속성제공자" 는 COM 클래스를 위한 코드를 생성하고 그 코드는 컴파일시에 C++ 컴파일러에 의해 프로젝트
코드에 추가되어집니다. 많은 코드를 몇줄의 속성으로 표시를 해둠으로써 결과적으로 코드의 양이 대폭 줄어든 효과를
얻게 됩니다. 더군다나, C++ 속성은 IDL 과 RGS 파일의 기능도 가지고 있습니다. 여러분들이 VS.NET 에서
ATL 프로젝트를 생성하는 경우, 소스 코드안에 포함되어진  IDL 비슷한 구문과 축소된 RGS 파일을 볼 수
있는 것은 이 때문입니다. 이번 기사에서는 C++ 속성과 "속성제공자" 에 의한 처리절차를 설명합니다. 또한,
C++ 속성들이 어떻게 등록과정에 영향을 미치는지와, 어떤 속성들이 모듈 진입코드를 위해
사용되는지를 설명할 것입니다. 맨 나중으로, 그러한 특징들이 어떻게 .NET 플랫폼을 위한 통합된 이벤트
체계에 발맞추는 지를 알아볼 것입니다.

거의 모든 기술들이 "속성" 이라는 단어를 사용하는 요즘, 지나친
"속성" 용어의 사용으로 혼란이 올 것입니다. 간단히 말해서, 속성은 어떤 항목의 "특성" 을 말합니다.
"property" 는 항목에 대한 것들 - 행동양식, 요구사항 - 을 설명해 줍니다. VS.NET 에서의 "속성" 은
OLE DB 소비자 코드, 성능 카운터 코드, 웹서비스와 ATL Server 코드, 그리고 COM 개체와
관계된 이 4가지 종류의 용도를 위해서 사용되어집니다. 다양하게 쓰이는 속성의 용도중에서 이번 기사에서는
Visual C++ COM 에 관계된 속성만을 다룰 것입니다. VC++ 컴파일러는 타입정보와 "unmanaged" C++
코드를 생성하는데에 "속성" 을 사용합니다.
우선, 기존 IDL 에서의 속성의 사용법을 알아볼 것이며, 그런 다음 어떻게 C++ 속성들이 IDL 과
등록파일들을 대체하는지를 설명할 것입니다. 그 후 마지막으로, COM 개체를 개발하는 데에 실제로 어떻게 C++ 속성들이
씌여지는지와 ATL 7.0 과 어떻게 관계되는지를 설명할 것 입니다. 이 기사에서 소개되는 코드는 Visual Studio
.NET Beta 1 에서 테스트되어졌으며, 정식버전에서는 변경이 되었을 수도 있습니다. ( 역주: 제가 VS.NET
정식버전으로 이 기사의 내용을 테스트 해보면서, 달라진 부분에 대해서는 그에 맞게 수정을 했습니다.
)

IDL 과 Attributes
COM 의 초창기 시절에, 그 기술은 두가지로 뚜렷이 나뉘어 졌습니다. 하나는 Interface
프로그래밍이고, 또 하나는 Automation(자동화) 프로그래밍입니다. 인터페이스 방식은, "이른 바인딩(
early-bound )" 과  "확장된 MS RPC IDL" 을
사용하는 것을 말합니다. 인터페이스 방식은 클라이언트와 서버간의 일종의 정해진
"계약" 을 요구합니다. 그리고, 그런 한번 정해진 계약은 서버와 클라이언트 컴퓨터 모두에
proxy-stub DLL 의 설치를 필요로 하면서 변경자체가 거의 불가능하게 되어졌습니다. 한번 정해진
인터페이스 명세서는 변하지 않는 다는 "암시적인" 약속을 해야 하기 때문에, 해당 COM 개체가 개선된다 해도 옛날에
정해진 인터페이스를 그대로 수용하고 있으며, 새로운 요구사항에 대해서는 또 다른 인터페이스를 정의하도록 했기 때문에, 결국
필요가 증대되면서 COM 개체는 많은 인터페이스를 담도록 설계되어졌습니다. - 가장 많은 변화를 한 것이 바로
"Internet Explorer" 인데, IHTMLDocument 인터페이스가 IHTMLDocument2,
IHTMLDocument3, IHTMLDocument4 까지 나온 것이 그 좋은 예일 것입니다. 아마도 IE 7.0 이
나오면 IHTMLDocument5 가 나오게 될 것입니다. 인터페이스 프로그래밍은 구현된 기능들이 인터페이스의
메소드로 나타나게 됩니다. Component 의 기능은 실행시에 특정 인터페이스에게 요청을 보냄으로써 이루어지게
됩니다.
VB 에서는, 개체들이 언어의 한 부분으로 통합되었습니다. VB 를 "객체지향적" 인
언어라고 말할 수는 없겠지만, "객체기반" 적인 언어임에는 틀림없습니다. "자동화" 는 VB 의 형식체계를 모델로
탄생하게 되었고, 그것은 곧 "개체" 자체로 받아들여지는 성질을 물려받게 됩니다. 인터페이스 방식과 다소 틀린 체계를 갖기
때문에 그것을 기술하기 위해서는 인터페이스방식의 "IDL 구문" 을 사용하지 않고 "Type정보" 를 생성해내는 "ODL(
Object Definition Language ) 구문" 을 사용해서 기술되어집니다. "자동화" 는 "늦은 바인딩" 으로
호출이 되어지며, "자동화 개체" 가 구현한 기능을 호출하기 위해서는 "타입정보" 를 이용한 "IDispatch" 를
호출함으로써 이루어집니다.
전형적으로, "자동화" 클라이언트는 특정 메소드를 수행하기
위해서는 "질의" 를 해야 합니다. 비록, "자동화" 를 통해서 구현된 메소드들이 "Dispinterface" 로
그룹화되어 있기는 하지만, 사용에는 다소 제약을 받게 됩니다. 왜냐면, 초기VB 버전에서는 인터페이스에 대해서 직접 다룰
수 없기 때문입니다. 초기 VB 버전에서는 그러한 개체를 전혀 별개의 방식을 써서 사용을 했습니다. 현재의 VB
버전에서는 인터페이스를 직접 다룰 수 있습니다. 그렇긴 해도, 예전의 개체를 다루는 방식도 여전히 지원되고
있습니다.
인터페이스와 자동화를 구분짓기 위해서는, IDL 과 ODL 이 가지는 방대한 정보를 명확하게
이해해야 합니다. 다음은 C++ 메소드에 대한 간단한 메소드 선언입니다.

HRESULT foo(long* p);

C++ 의 측면에서 볼때, 이 메소드는 명백히 호출자 측에서 "long" 형 포인터를 넘겨주는 것으로 이해될 수
있습니다.
해당 함수안에서는 그 데이터를 읽기 위해서 포인터 p 의 주소를 참조할 것입니다. 하지만,
이것은 순전히 가정에 불과합니다. 왜냐면, 포인터 p 는 "long" 형 배열에 대한 첫번째 항목을
가르킬 수도 있지만, 초기화되지 않은 "long" 형 포인터 자체일 수도 있기 때문입니다. "long" 형 배열일
경우에는, 메소드 안에서 해당 영역을 읽어낼 것이고, 초기화 되지 않은 "long" 형 포인터라면 메소드 안에서 해당
내용을 채우는 데에 사용될 것입니다. C++ 에서는, 이 2가지 서로 다른 사용법 - 하지만, 같은 문법을 사용하는 것에
대해서 구분지을 수 있는 방법이 없습니다. 즉, 데이터가 전달되어지는 방향과 얼마나 많은 항목들이 전달되어질 것인지를
결정할 수 있는 문법 체계가 없는 것입니다. 바로, 그러한 결함을 IDL 과 ODL 에서 수정/보완을 해줍니다. 바로, 그
구문을 나타내주는 것이 "속성" 들입니다.
VC++ 에서 COM 을 만들어 보신 분이라면 이미 아시겠지만,
[in] 과 [out] 과 같은 속성이 그러한 역할을 명시해주는 데에 사용되어집니다. 만약 한 항목에 대해서 여러개의
속성이 사용될 필요가 있다면, [in,out] 처럼 하나의 "꺽쇠괄호" 안에서 컴마를 구분자로 해서 사용하면 됩니다. 어떤
속성들은 인자를 요구하기도 하는데, 그 인자는 해당 속성의 뒤에 "()" 괄호안에 놓여지게
됩니다. 종합해서, 위의 HRESULT foo( long *p ) 를 IDL 구문으로 다음과 같이 정의
되어지면,

[id(1)] HRESULT Method([out, retval] long* p);

명백히, 데이터가 메소드안에서부터 호출자에게 이동된다는 것을 나타내게 되는 것입니다.

IDL 과 ODL
얼마동안은, ODL 과 IDL 은 서로 별개의 기술언어로써 공존해왔습니다. 하지만, COM 기술의
발전으로 인해, 한가지 언어로 통합되어졌습니다.  이름하여 "IDL" ( IDL 의 "I" 자를 따고,
"OLD" 의 "DL" 을 따서. ) 이라 불리게 되었는데... 어허... 달라진듯하면서도 달라지지 않은
이름이군요.
IDL 컴파일러 ( MIDL.exe ) 와 ODL 컴파일러 ( MkTypeLib.exe ) 의
통합으로 인해 개발자들은 다소 편해지게 되었습니다. - 역주: 사실 저도 통합된 IDL 세대입니다. - 현재는,
MIDL.EXE 하나로 컴파일를 합니다. 비록 이 시점에 이루어진 통합과 Dual Interface 의
출현이 비슷하긴 해도 동시에 이루어진 것은 아닙니다. Dual Interface 프로그래밍 방식은, 인터페이스
방식을 좀더 보강한 것이라고 볼 수 있습니다. 왜냐면, 그것은 서로 연관된 기능의 메소드를 그룹화시키지만,
"IDispatch" 를 통한 "늦은바인딩" 도 지원하기 때문입니다. 새로운 형식의 IDL 은 "IDispatch" 사용의
지원과 타입정보에 기반한 마샬링을 위한 "형식 라이브러리 정렬" 을 허용하기 위한  "타입정보" 를
필요로 하게 되었습니다. 이러한 모든 것은 IDL 의 Library 구문을 사용함으로써
해결되어졌습니다.
"타입정보" 는 보통 "TypeLibrary" 라고 불리는 이진데이터로
컴파일되어집니다. 그 이진 데이터는 DLL 이나 EXE 파일안에 "RESOURCE" 파일의 형태로
포함되어질 수 있습니다. "Type Library" 는 시스템에 등록되어지게 됩니다. VB 6.0
IDE 와 같은 툴들은 "Type Library" 에 있는 타입정보를 이용해서 해당
COM 개체에 대한 "IntelliSense®" 기능을 제공해 줍니다.
그러나, "Type
Library" 를 등록한다는 것은 그 이상의 의미를 가지고 있습니다. 여러분이 "Type Library" 를
시스템에 등록하게 되면, COM 은 모든 인터페이스에 대해 레지스트리에 항목을 추가하게 됩니다. 이렇게 되면,
COM 은 인터페이스를 마샬링하기 위해서 "Universal Marshaler" 를 이용하게 됩니다. 인터페이스는
"TypeLibrary" 를 이용해서 정렬이 이루어지게 되는 것입니다.
IDL 과 ODL
은 근본적인 차이로 인해 결코 "하나" 로 모아질 수 없습니다. 아마도 VB 와 RPC 의
개념을 통합하기가 어렵기 때문일 것입니다. 한쪽진영에서의 중요한 것이 다른쪽에서는 전혀 관련이
없는 것들이 있습니다. 예를 들어, Component 의 "쓰레드 모델" 이 그러한 단적인 예입니다.
VB 6.0 에서 제작되어지는 Component 는 VB 의 "시각화" 특성으로 인해 "Apartment" 모델로만 생성할
수 있습니다. 그리고 UI 를 가진 모든 COM 개체들은 반드시 "STA( Single-Threaded
Apartment )" 하에서만 실행될 수 있습니다. 새로 통합된 IDL 의 타입기술구문들은 단일 쓰레드의 VB
를 위한 성향을 지니게 되었고, 그것은 곧 "coclass" 의 쓰레딩 모델을 지정할 수 있는
방법을 제공하지 않게 되는 결과를 가져온 것입니다.
그런 이유로 인해, 개체는 2가지 영역에서의
정보를 종합해서 기술되어지게 되어졌습니다. "Type Library" 와 시스템 레지스트리에 등록된 CLSID 항목이
그것입니다. "인터페이스 정보와 타입라이브러리 세부 정보 자체는 타입라이브러리가 등록될 때 시스템 레지스트리에
위치하게 되고, 그외의 정보들 - 개체와 ProgID 등의 세부 사항 - 은 Component 자체가 노출시키는
"DllRegisterServer" 함수에 의해서 레지스트리에 추가되어집니다. 대다수의 COM 서버들은 내부에
"Resource" 형식으로 된 TypeLibrary 를 가지고 있고, 그 상태로 "DllRegisterServer"
함수안에서 레지스트리에 등록되어지게 됩니다.
IDL 은 [custom] 속성을 가지고 있는데, 그것으로 IDL 의 확장없이 Type
Library 에 특정한 정보를 추가할 수가 있습니다. 대표적으로 COM+ 에서 트랜잭션 요구사항을 기술하기
위해서 "mtxattr.h" 에 선언되어 있는 "TRANSACTION_XXX" IDL 속성을 들 수
있습니다. 또 다른 예로, MIDL 5.0 컴파일러는 항상 "빌드시간" 과 "MIDL" 의 버전을 TypeLibrary 에
[custom] 속성을 사용해서 추가를 해둡니다.
비록, COM+ 개발팀이 COM+ 구성요소 서비스
스냅인에서 TRANSACTION_XXX [custom] 속성을 이용하도록 만들기는 했지만, 이러한 [custom] 속성을
이용한 구성요소 설정을 더 이상 확장하지는 않기로 결정을 했습니다. 여러분이 구성요소 서비스 스냅인에서
구성요소를 COM+ Application 에 추가할 때, 스냅인은 등록될 DLL 의 "쓰레딩 모델" 과
"ProgID" 를 얻기 위해서 "DllRegisterServer" 함수를 호출합니다. 그외의
COM+ Catalog 에 담겨질 모든 정보들 ( 예를 들어, 구성요소가 지원하는 인터페이스와 그것들의 메소드 )
을 구성요소와 관계된 타입라이브러리를 로드한 후, 질의를 통해서 얻게
됩니다.
COM+ 구성요소를 설치할 때, 타입정보는 상당히 중요한 요소입니다.
왜냐면, COM+ 는 실행시에 올바른 Context 를 생성하기 위해서 구성요소의 완벽한 설명서를 필요로
합니다. "타입정보" 는 구현에 관해서 세부사항들을 포함하고 있습니다. 실제로, 이 점이 바로 IDL 과
ODL 이 더 불일치하게 되는 면입니다. coclass ODL 구문은 타입라이브러리와 연계된 서버가 특정한
인터페이스를 구현한 클래스를 가지고 있는 것을 선언합니다. 이것은 구성요소가 인터페이스를 구현했다면 반드시
인터페이스의 IID 를 인자로 QueryInterface 를 호출해야만 하는 COM 의 규칙과
상반됩니다. 달리 말하면, 여러분은 실행시에, 구성요소가 지원하는 동작에 관해서 요청을 해야 하는 것입니다.
반대로, IDL 은 좀더 추상적입니다. 왜냐면, 그것은 인터페이스( 연관된 메소드의 집합 )만을 정의할 뿐입니다. IDL
은 언제, 어떻게 인터페이스가 구현되는지에 대한 어떠한 정보도 포함하고 있지 않습니다.
다음의 2 가지
정보 - 쓰레딩 모델과 ProgID - 는 구성요소의 구현과 밀접하게 맺어져 있습니다. ProgID 는 구성요소의
CLSID 와 연관되어져 있는 또 다른 이름입니다. 그렇다면 타입정보가 CLSID 를 가지고 있다면 왜 ProgID 는
가지고 있지 않는 걸까요? 마찬가지로, 여러분이 구성요소를 만들때, 특정 쓰레딩 모델을 사용하게
됩니다.  일단 한번 구성요소가 작성되면, 여러분은 쓰레딩 모델을 변경할 수 없습니다. 왜냐면, 구성요소가
Message Pump, 쓰레드 친화성, 그리고 쓰레드에 민감한 코드와 데이터를 동기적으로 액세스 해야 하는 등의 사항들을
"쓰레딩 모델" 이 나타내주기 때문입니다. 그러므로, 구성요소를 설명하는 타입정보는 구성요소의 쓰레딩 모델이
없이는 불완전할 수 밖에 없습니다.

C++
Attributes
프로그래머에게 있어서, IDL 과 TypeLibrary 의 이러한 불일치는 고통일 수 밖에 없습니다.
왜냐면, 구성요소에 대해서 기술되어지는 정보가 몇개의 파일에 걸쳐서 존재한 다는 것이고, 그것의 관리는 프로그래머의 몫이기
때문입니다.
여러분은 이러한 예를, 구성요소를 기술하기 위해 3개의 파일을 사용하는 ATL 3.0
프로젝트에서 충분히 경험했을 것입니다. 그 3개의 파일 중 가장 대표적인 IDL 파일은 "인터페이스 기술부분" 과
구성요소에 의해서 지원되는 인터페이스의 리스트, CLSID 를 가지고 있는 coclass 항목을 포함한 "Library"
블록을 포함하고 있습니다. 다음으로 RGS 파일은 IDL 에서 빠져 있으면서도 타입라이브러리 등록시에 제외된 정보들을 담고
있습니다. 구성요소에 대한 정보를 담고 있는 마지막 파일은 바로 ATL class 자체입니다. 이 파일은
인터페이스 맵을 통해서 구성요소가 지원하는 인터페이스 목록을 나타냅니다. COM 의 세계에서는, 구성요소의 인터페이스를
구할 수 있는 유일한 방법이 QueryInterface 메소드를 통해서 이루어지고, ATL 의 경우
"interface map" 매크로를 사용해서 구현됩니다. 부가설명을 드리면, ATL 클래스는 구성요소가 사용할 쓰레딩
모델을 클래스 헤더 파일안에 상속되어지는 클래스로 정해지게 됩니다. 따라서, ATL 에서는, 클래스 헤더파일
자체도 구성요소를 기술하는 역할을 하게 됩니다.
구성요소의 쓰레딩 모델을 변경하고자 한다면, RGS 파일뿐만 아니라 그 쓰레딩 모델을 반영할 ATL
클래스의 헤더파일에서도 변경을 해주어야 합니다. 마찬가지로, 구성요소의 CLSID 값을 변경하려고 한다면, IDL 파일 (
coclass 구문 ) 과 함께, RGS 파일에서도 변경을 해주어야 합니다. 나아가서, 구성요소에 또다른 인터페이스를
추가하려고 하면, "인터페이스 맵" 의 C++ 매크로 부분과 함께, IDL 파일의 coclass
구문을 변경시켜 줘야 합니다. ( 역주: 물론, 이런 일의 대부분의 코드는 ATL 마법사가 알아서 해주는 부분도 있기는
하지만, 간혹 수작업으로 해야 할 때도 있는 데, 그런 경우 다소 불편하게 됩니다 ) 몇몇개의 파일에 걸쳐서 이러한
정보들이 산재해 있다보니 개발로서는 혼란스러울 뿐만 아니라, 에러까지도 쉽게 유발하게 됩니다.
바로 이 시점에서, C++ 속성은 구성요소 기술을 위한 좀 더 완벽한 해결책을 제시해줍니다. C++ 속성을
사용함으로써, 형식 라이브러리( type library ) 를 변경할 필요도 없고, 어떻게 등록될지에 대한 고려도 하지
않아도 됩니다. C++ 속성은 형식 라이브러리안에 있는 타입정보에 대해서 추상화된 기능을 제공해줍니다. 즉, 복잡한 C++
구문과 서술 파일들을 고치지 않고 간단한 C++ 속성을 알기만 하면 제대로 동작하기 때문에, 이후의 개발자들은 해당 C++
코드를 모르게 될 뿐더러 오히려 C++ 속성이 그러한 역할을 자연스럽게 해주는 것처럼 알게 될
것입니다.

표 1  C++ 속성의 사용을 지정

ATL 의 사용에 관계없이 이러한 "속성" 들을 코드에 적용할 수 있습니다. 만약, ATL 프로젝트가
아닌 코드에서 그러한 속성들을 사용한다면고 해도 IDL 과 형식라이브러리를  사용한 것과 똑같은 효과를 얻을
것입니다. 물론 그렇긴 해도, 대개의 경우 여러분들은 C++ 속성을 ATL 프로젝트안에서만 사용하게 될
것입니다.  표 1 에서는 VS.NET 에서의 ATL
COM 프로젝트 마법사가 사용되는 모습을 보이고 있습니다. "속성"에 대해서 체크가 되는 경우, 프로젝트에 추가되는 모든
클래스들에서 "C++ 속성" 이 사용되어지게 됩니다. (역주: 한글 VS.NET 에서는 '특성사용" 이라는 체크박스가
있습니다. ) "속성 사용" 으로 얻게 되는 효과는 크게 2가지입니다. 하나는, 지금까지 언급한 데로 여러 파일에
걸친 구성요소 기술 정보를 통합한 다는 것이고, 또 하나는 전체적으로 소스가 간결해 진다는
것입니다.

C++ 속성은?
C++ 속성의 개념은 상당히 간단하다고 볼 수 있습니다. 원하는 속성을 적용하고 싶은 클래스나
함수의 코드이전에 속성을 선언해 두기만 하면 됩니다. 구문은 IDL 에서와 같이 "[]" 괄호가 사용되어집니다.
표 2 에서는 ATL 7.0 프로젝트에서 속성이 사용되어지는 예를 보여줍니다.
똑같은 이름의 IDL 구문을 대체하는 속성을 보여주기 위해 일부러 그 부분의 코드를 선택한 것입니다. 이전 버전의
ATL 에서 CManager coclass 의 구현이 IDL 파일에서 제거되고 C++ 헤더파일이나 CPP 파일에 구현과
함께 직접 속성으로 정의되어지는 것을 보여주는 좋은 예입니다. 이전의 IDL 파일의 typelib 구문에나 있었던
coclass 가  직접 구현코드와 함께 있기 때문에 훨씬 더 직관적임을 알 수 있습니다.
속성과 함께 쓰인 이 간단한 코드는 실제로는 많은 "C++ 코드" 를 생성해 냅니다. 표2 에서,
CManager 라고 불리는 COM coclass 를 선언해 두고 있으며 DoWork 메소드 하나만을 가진
IEmployee 인터페이스를 구현한 것을 볼 수 있습니다. CManager 라는 C++ 클래스는 속성의
영향으로 인해 coclass 까지 구현하는 역할을 해냅니다. 물론, 추상적으로 "속성" 이 쓰인 것을
뿐, 실제 IDL 텍스트는 생성되어져야 합니다. IDL 파일은 프로젝트와는 투명하게 외부적으로
생성되어지고 지며, 해당 IDL 파일을 개발자는 직접 편집할 필요없이 속성을 사용함으로써
컴파일러에 의해서 자동적으로 다루어지게 되는 것입니다. ( 역주: 실제로 ATL 7.0 프로젝트의
Workspace 에는 IDL 파일이 없지만, 프로젝트가 저장되는 폴더에는 "_" 로 시작한 프로젝트의 IDL 파일이
생성되어지는 것을 볼 수 있습니다. )
속성과 함께 쓰인 이러한 기능을 이전의 ATL 3.0 으로 똑같이 구현하려면  표 3  과 같은 코드가 필요하게 됩니다. 표3에서 보는 것처럼 ATL
3.0 에서의 COM 클래스 코드는 쓰레딩 모델을 지정하게
되는 CComObjectRootEx<> 에서 상속받게 됩니다. 또한, 클래스 팩토리와 에러
처리를 위한  CComCoClass<> 를 상속받습니다. 마지막으로 Dual 인터페이스 지원을
위한 IDispatch 관련 메소드를 제공하는 IDispatchImpl<> 클래스를 상속받게
됩니다. ATL 3.0 COM 개체는 BEGIN_COM_MAP 과 END_COM_MAP 매크로 사용으로
인한 "인터페이스 맵" 을 통해서 제공되어집니다.
ATL 3.0 구성요소를 외부에서 생성하려면 CoCreateInstance 함수를 통해서 가능합니다.
물론, 해당 구성요소는 미리 System 레지스트리에 등록되어 있어야 합니다. 이때의 레지시트리 셋팅은 ATL
에서 RGS 스크립트 파일로 구현되어지고 DECLARE_REGISTRY_RESOURCEID 매크로의 인자에
주어진 리소스 ID 를 통해서 ATL 3.0 registrar 가 사용하게 됩니다.
마지막으로, 구현된 클래스에 대한 항목은 ATL 프로젝트 파일의 주 .CPP 파일에 OBJECT MAP
에 추가됩니다. 해당 항목은 coclass 를 위한 클래스 팩토리 함수와 해당 클래스를 위한 등록함수 및 콤포넌트
카테고리를 등록하는 함수등의 포인터들로 이루어져 있습니다.
The ATL 7.0 attributed code in 표 2 에서 보여지는 ATL 7.0 코드에서는 그러한 모든 구현에 대해서 단지,
[coclass] 속성을 지정함으로써 이루어지게 되는 것입니다. 거기에는, 그 어떠한 인터페이스 맵도 찾아볼 수 없고,
등록 스크립트를 비롯해서, OBJECT MAP 까지 모든 코드가 추상화되어 감추어지게 되는 것입니다. (역주: 개발자에게
감추어질 뿐, 실제적으로 모든 코드가 생성되어집니다. )

Attribute 제공자
[coclass] 속성은 CManager 클래스를 COM coclass 로 만들기 위한 모든 C++ 코드를
생성해 냅니다. 이런 부가적인 코드는 컴파일시에 컴파일러에 의해서 생성되어져서 컴파일되는 데이터에 삽입되어지게 됩니다.
컴파일이 되는 동안, C++ 컴파일러는 소스 코드를 해석해서 OBJ 파일을 생성하는 데에 사용되어지도록 하는 테이블을
구축합니다. 그 과정속에서 "C++ 속성" 이 발견되면 컴파일러는 "속성 제공자" 라고 불리는 구성요소를 호출하고 해당
속성을 넘겨주게 됩니다. 이어서 "속성제공자" 는 "속성" 에 해당하는 실제 코드를 생성해 내고, 빌드 과정중에서도 그
속성과 관련된 여타의 중요한 역할을 수행하게 됩니다. 이러한 과정을 도해한 것이 아래의 표 4
입니다.

표 4 속성 제공자 작동구조

만약  표 2  에서 [coclass] 속성을 제거하면, CManager 클래스는
여전히 정상적으로 컴파일 될 테지만, 단지 평범한 C++ 클래스의 하나로 여겨지기 때문에 COM 개체로의 등록은
이루어지지 않을 것입니다. 실제로, [coclass] 속성은 CManager 클래스가 COM coclass 임을 지시하는
것이고, "속성제공자" 구성요소는 해당 클래스가 COM 개체로 되기 위한 모든 필요한 코드를 생성해 줍니다. 이러한
과정은, 속성으로 실제 코드를 생성하기 위한 과정중에 컴파일도중 파싱된 심볼테이블의 상태를 직접
사용하기 때문에 "C++ 전처리기" 과정으로는 불가능합니다. ( 역주: 그런 이유로, VS.NET 에서는 해당
기능의 구현을 위해서 새로이 "C++ 속성" 이란 구문을 추가하게 된 것입니다. 이 과정에 대한 자세한 설명은 아래에
"속성제공자" 에서 나옵니다. )
현재, 2 가지의 속성 제공자가 있습니다. C++ 컴파일러 ( clxx.dll ) 과 ATL 속성 제공자
( atlprov.dll ) 가 그것입니다. clxx.dll 은 기본적인 타입 생성과
정렬을 담당하는 속성을 제공해 주는 반면, ATL 속성 제공자는 COM 개체 구현을 위해서 ATL 프로젝트 안에
ATL 7.0 클래스를 이용해서 코드를 생성해 줍니다. 생성된 ATL 코드는 오직 컴파일시에만
존재하게 되며, 디버깅 모드로 컴파일하는 경우에는 ATL 코드에 대한 정보를 프로젝트의 PDB 파일에 보관하게
됩니다. 예를 들어서, 다음의 클라이언트 코드는 CManager 의 in-context 클래스 팩토리를
접근하는 것입니다.

IClassFactory* pCF;
CoGetClassObject(__uuidof(CManager),
CLSCTX_INPROC_SERVER, NULL, __uuidof(pCF),
(void**)&pCF);
IEmployee* pE;
pCF->CreateInstance(NULL, __uuidof(pE), (void**)&pE);
pCF->Release();
// use pE...
pE->Release();

위의 CManager 가 C++ 속성을 이용해서 구현된 경우, 디버거로 CreateInstance 함수
안으로 추적해 들어가간 이후,  디버거에 보여지는 코드는
CComClassFactory<>::CreateInstance 입니다. 표2 와 같이 코딩을 한 경우
분명 DECLARE_CLASSFACTORY 매크로를 사용하지 않은 데다가 CComCoClass<> 를
쓴 적이 없는 데도 실제 디버거에서는 쓴 것으로 나오는 것입니다. 즉, ATL 속성 제공자가 필요한 ATL 코드를
컴파일시에 적절히 생성해 주기 때문에 그러한 결과가 나오는 것입니다. ATL 속성을 쓰는 것과, 이전의 ATL 3.0
코드로 작성하는 것은 동일한 효과를 얻어내는 것입니다.
하지만, 엄밀히 말해서 속성 제공자가 ATL 3.0 형식의 코드를 모든 경우에 대해서 생성해 내는 것은
압니다. 실제로, 그 코드들은 ATL 관련 클래스들을 사용하지는 않습니다. 속성 제공자는 그 자신만의 방식으로
코드를 생성합니다. 비록, 삽입된 코드들이 혼합된 어셈블리 디버깅을 위한 PDB 파일에는 존재하기는 하지만, 그
소스코드 자체는 컴파일시에만 존재하게 됩니다. 예를 들어서, IEmployee 는 듀얼 인터페이스이기
때문에  표 5 에서 사용된 코드를 이용해서 개체를 호출할 수 있습니다.
만약 ATL 3.0 을 사용해서 클래스를 작성했다면,
CComTypeInfoHolder 을 내부적으로
이용하는 IDispatch::Invoke 함수를 포함하는 IDispatchImp<>
클래스를 상속받을 것입니다. 반면, ATL 속성 제공자가 생성해낸 코드라면 그러한 식으로
구현되어 있지 않습니다. 대신에, 속성제공자는 좀더 효율적인 IDispatch 코드를 나름대로의 방식으로 생성해냅니다.
필자가 직접 Invoke 함수 안으로 추적해 들어가 본 경우, 실제로 CManager::DoWork 호출을 이끌어내는
기계어 코드를 볼 수 있었습니다. 그걸로 판단해 보건데, 분명 속성이 지정된 클래스는
IDispatchImpl<> 클래스를 사용하지 않을 뿐만 아니라, ITypeInfo 의 형식
라이브러리를 이용한 메소드 호출을 이끌어 내지 않는 다는 것입니다. Invoke 메소드의 실제 소스 코드가
컴파일시에만 ( 디버그를 위한 PDB 파일 ) 존재하기 때문에, 혼합된 어셈블리 모드로의 진입으로 그
사실을 알아낼 수 있습니다.
컴파일러가 생성한 코드를 보는 방법이 컴파일러의 /Fx 스위치를 활용하는 것입니다. 그렇게 되면
속성으로 인해 삽입된 코드가 담겨진 파일이 생성되어집니다. /Fx 스위치를 적용해서 속성이 담긴 .cpp
파일을 컴파일 해보면, 컴파일러가 속성을 포함한 모든 파일에 대해서 부가적으로 또 다른 파일을 생성하는 것을 볼 수
있습니다. 실제로 그 파일들에는 속성으로 인해서 추가된 코드들이 담겨 있습니다. 이렇게 부가적으로 생성된 파일들은
실제 속성이 담긴 원본 파일과 파일이름이 같지만 확장자는 .mrg.cpp 또는 .mrg.h 를
씁니다.
표 6 은 표 2 에 나온 코드를 /Fx 스위치로 컴파일해서 나온 코드입니다.
예상한 데로, 속성 제공자는 ATL 기반 클래스인 CComCoClass 와 CComObjectRootEx 를 추가한 것을
볼 수 있고, 또한 IProvideClassInfo 지원을 위한 IProvideClassInfoImpl<> 도
추가되었음을 알 수 있습니다. 하지만, IDispatchImpl<> 로부터는 상속받지 않고 직접 해당
클래스안에서 IDispatch 인터페이스 메소드를 구현한 것을 볼 수 있습니다.   표 6 에서의 Invoke 메소드안의 코드는 ATL 속성 제공자가 인터페이스에
구현된 메소드를 호출하기 위해 switch 구문을 쓴 것을 보여줍니다. 그 방식은
CComTypeInfoHolder 에 의해서 사용되어지는 Invoke 메소드의 ITypeInfo 구현을 사용한
방식보다 향상된 성능을 제공합니다. 만약 2개의 듀얼 인터페이스를 구현했다면, 각각의 인터페이스에
대해서 IDispatch::Invoke 와 IDispatch::GetIDsOfNames
가 구현되어집니다.
아마도, 속성제공자가 기존의 IDispatchImpl<> 이 가진 유연성을 배제했다고 보시는
분도 계실 것입니다. 그런 분들에게는 ATL 3.0 에서 하던데로, 아래와 같이 IDispatchImpl<>
로부터 상속받도록 명시를 하시면 됩니다.

[coclass]
class ATL_NO_VTABLE CManager :
public IDispatchImpl<IEmployee, &__uuidof(IEmployee)>
{
public:
STDMETHODIMP DoWork(BSTR bstrTask);
};

소스코드를 간략하게 보여주기 위해서 위의 코드에서는 형식 라이브러리의 버전과 LIBID 를 위한
Template 인자를 기본인자를 사용했지만, 여러분들은 실제로 위와 같이 코드를 작성하시면 안됩니다. 그런 경우,
형식라이브러리 버전 1.0 을 사용하도록 제한이 되기 때문입니다.
위의 코드에서, 듀얼 인터페이스 호출을 위해서 IDispatchImpl<> 클래스가
사용되어집니다. 이제는 위의 코드가 컴파일되어지면, [coclass] 를 처리하려는 속성제공자는 이미 IDispatch 를
위한 구현이 된 것을 알게 되고 그와 관계된 코드는 제외를 시키게 됩니다. 이러한 규칙은 속성제공자의 기본
행동 원칙입니다. 즉, 속성제공자가 해당 속성에 대해서 코드를 추가하려고 할 때, 이미 속성으로 인한
코드와 같은 역할을 하는 코드가 존재하는 경우는 속성에 대한 코드를 삽입하지 않는
것입니다. 이것을 C++ 전처리기 과정으로 하려고 했다고 상상해 보세요. 분명 불가능한 일일
것입니다.
모든 메소드에 대해서 dispid 를 제공하지 않거나, 클래스에 대해서 [restricted()] 속성을
제공하지 않는 다면, 속성 제공자가 IDispatchImpl 을 이용해서 인터페이스를 구현한다는 것은 알아둘만
합니다.

IDL 은 그럼 어떻게
되었을까요?
C++ 속성도입의 주된 목적은 별도의 IDL 파일을 유지하지 않겠다는 것입니다. 대신에 타입에 대한
기술을 C++ 파일안에 직접 해놓습니다. 개발자는 그렇게만 해두고, 컴파일시에 컴파일러가 속성에 따른 임시 IDL
파일을 자동으로 구축하면 되는 것입니다. 그때부터의 모든 과정은 예전 ATL 3.0 시절과 다를 게
없습니다. 해당 IDL 파일은 MIDL 툴을 이용해서 컴파일될 것이고 C++ 에서 컴파일될 수 있는
헤더파일과 형식 라이브러리, C 형식의 Proxy-Stub DLL 파일 들이 생성될 것입니다. 실제로, C++ COM 관련
속성을 프로젝트에 적용시켰을 때 혼란스러운 것중의 하나가 C++ 파일들이 컴파일 되고 난 이후에 임시 IDL 파일이
컴파일되는 것입니다. MIDL 툴은 별도의 빌드과정을 요구하지 않고 C++ 컴파일러에 의해서 호출이
되어집니다. 이러한 파일들은 C++ 파일의 컴파일을 위해서 필요하지는 않습니다. 반면에 ATL 3.0
프로젝트에서는 C++ 파일들이 MIDL 툴의 결과물에 대해서 의존했었습니다.
COM C++ 속성은 대부분 기존 IDL 속성의 구문을 그대로 따왔습니다. 거의 똑같은 역할을 해내는
속성도 있습니다.  coclass IDL 구문은 [coclass] C++ COM 속성에 의해서 대체되었다고 볼 수
있습니다. C++ 속성이 클래스와 함수에 직접 적용이 되기 때문에 IDL 에서의 "library" 구문과 직접적으로 똑같은
역할을 하는 것은 찾아볼 수 없습니다. 이러한 속성들이 프로젝트내의 몇개의 파일에 존재하기 때문에, 그러한 모든
정보를 다시 하나의 "library" 구문의 블록에 모은 다는 것이 불가능한 것입니다. 실제로, 속성제공자는
이렇게 클래스나 함수마다 적용된 속성들의 정보를 모아서 하나의 IDL 파일에 모아서 생성해 놓습니다. 표 7 은 형식 라이브러리로부터 항목을 추가하거나 제외시키기 위해 쓸 수 있는
속성들을 보여주고 있습니다. 또한, 다른 IDL 파일이나 형식라이브러리를 포함할 수 있는 속성도 있습니다.

Microsoft 는 C++ 언어를 약간 변경시켰는데, 인터페이스를 C++ 파일에서도 정의할 수
있도록  __interface 예약어를 추가한 것입니다. VS.NET 의 이전버전에서, interface 기호 (
예약어가 아님. ) 는 objbase.h 파일에 구조체로 정의되어 있습니다. 이것은 다음의 코드가 VC++ 6.0
에서 컴파일될 수 있음을 의미합니다. 그렇지만, interface 기호를 사용했다고 해서 해당 C++ 클래스가
COM 인터페이스를 기술할 수 있는 것은 아니었습니다. ( 왜인지는 아래 소스의 주석을 참조하십시오. )

class CBase {int j;};

interface IMyInterface : CBase
// 인터페이스는 클래스로부터 상속받을 수 없습니다. {
IMyInterface()
: i(0) {} //
인터페이스는 생성자를 정의할 수 없습니다. void
Method1(){} //
인터페이스가 가지는 메소드는 반드시 순가상함수여야 합니다. int
i; //
인터페이스는 데이터 멤버를 가질 수 없습니다.
};

VS.NET 에서의 VC++ 은 interface 구조체와 비슷한 역할을 수행하는
__interface 라는 새로운 키워드를 도입했습니다. 하지만 그 역할은 분명하게 COM 인터페이스 규격을 정의하도록
해놓았습니다. 즉, 인터페이스는 다른 인터페이스로부터 상속받을 수는 있지만 클래스로부터의 상속은 불가능한
것입니다. 인터페이스는 오직 순가상 함수만을 포함하며, 어떠한 데이터 멤버, 생성자, 소멸자를 가져서는
안됩니다. 기존 interface 기호가 사용되어진 위의 코드와 같은 단순히 __interface 키워드로 교체한
것만으로는 VS.NET 환경에서는 정상적으로 컴파일되지 않습니다. 새로운 __interface 키워드는 오직, 듀얼
인터페이스, 디스핀터페이스, 커스텀 인터페이스의 정의를 위해서만 사용되어져야 합니다. 즉, 기존의
dispinterface 를 위한 ODL 구문은 더 이상 지원하지 않는 다는 것을 의미합니다. 대신에
dispinterface 를 명시하기 위해서 [dispinterface] 속성을 사용해야 합니다.
마찬가지로 Dual interface 를 지정하기 위해서 [dual] 속성을 사용해야 하고, 커스텀
인터페이스를 위해서는 [object] 속성을 사용해야 합니다. 유추해보면, __interface 키워드의 사용은 곧 해당
클래스가 COM 인터페이스가 된다는 것을 의미하며, 그 클래스안의 모든 메소드는 암시적으로 __stdcall 함수호출
형식을 따르게 되고 명시적으로 지정되는 않는 다면 해당 속성에 따라서 암시적으로 IUnknown 또는 IDispatch 를
기반 클래스로 상속받게 됩니다. 만약, 인터페이스 속성 ( [dispinterface], [dual] 또는 [object]
) 을 빠뜨리고 정의를 하게 되면 __interface 키워드는 순가상 함수만을 포함한 구조체를 정의하는 구문이라고
여겨집니다.
아래의 코드는 인터페이스를 정의하고 있으며 C++ 컴파일러인 cl.exe 로 /LD 스위치를 적용시켜서
명령행 라인에서 컴파일되어질 수 있습니다.

// employee.cpp
// compile with cl /LD employee.cpp
#define COM_NO_WINDOWS_H // compile quicker
#include <oaidl.h>
[module(name="Employees")];
[dual]
__interface IEmployee : IDispatch
{
[id(1)] HRESULT DoWork(BSTR bstrTask);
};

반복해서 말하면, 이것은 C++ 파일에 선언되어져 있기 때문에 분명히 IDL 구문이 아니라는 것입니다.
__interface 키워드가 오직 public 상속만을 지원하기 때문에 위의 구문에서 어떠한 "상속" 접근 지정자를 볼
수 없음에 주목할 필요가 있습니다. COM 인터페이스에서는 protected 또는 private 상속이라는 개념
자체가 존재가 않는 것입니다. 위의 파일내용으로 컴파일을 시키게 되면, 표 8 에서 보여지는 목록의 파일을 얻게 됩니다. ( 역주: C# 에서는
class, struct, interface 예약어가 있습니다. VC++ 6.0 에서는 class 와 struct 만
있다가, VC++ 7.0 에 와서야 interface 예약어를 추가 지원하게 되었다고 보시면 됩니다. )
소스 파일이 어떠한 C++ 코드도 포함하고 있지 않기 때문에 OBJ 나 DLL 파일에서는 별다른 실제
기계어 코드를 포함하고 있지 않습니다. 그렇지만 IDL 파일을 컴파일하고 생성하는 컴파일러의 입장에서는 그러한
파일을 링크시켜야만 합니다. 바로 그것 때문에 /LD 스위치 ( DLL 에 링크시키도록 하는 ) 를 사용한
것입니다.  컴파일러가 [module()] 속성을 만나게 되면, 곧장 속성 제공자를 호출하게 됩니다. 결과적으로
볼때, CPP 파일로부터 IDL 파일을 생성하게 되는 것입니다. 그 후, 컴파일러는 MIDL 툴을 호출해서 IDL
파일을 컴파일하게 되는 것입니다. 그렇게 해서, 마지막으로 Proxy-Stub 파일들이
생성되집니다.
MIDL 툴이 IDL 파일을 컴파일할때, 생성해야 할 파일들의 이름을 IDL 파일명을 기준으로 해서
매기게 됩니다. IDL 컴파일로 생성되는 파일과 기존 파일이 이름에서 중복되는 것을 막기 위해서 VS.NET
에서는 COM 서버 프로젝트의 경우, IDL 컴파일로 생성되는 결과물로 "_" 과 함께 파일명을 생성하게 되고,
클라이언트 프로젝트의 경우 "vc70" 파일명으로 시작하는 파일명이 생성되게 됩니다. 다른 이름으로 하기를 원하다면
링커에게 /IDLOUT 스위치와 함께 파일명을 정해서 지시해 주어야 합니다. 예를 들어서 다음과 같이 하면 cl.exe
는 컴파일을 마친 후, 호출하게 되는 링커의 스위치로 /IDLOUT:"myidl.idl" 을 해주게
됩니다.

cl employee.cpp /LD /link /IDLOUT:"myidl.idl"

표 6 에 생성되어진 코드는 [coclass] 속성이 컴파일러로 하여금 해당
클래스를 CComObjectRootEx<> 에서 상속받고, 인터페이스 맵을 통해서 QueryInterface 를
할 수 있도록 하는 것을 보여줍니다. 컴파일러는 인터페이스 맵을 생성하 기위해서, 클래스가 상속받는 인터페이스를
확인합니다. 만약 인터페이스 맵에 특정한 엔트리를 추가하고 싶다면 [com_interface_entry()]
속성을 사용하면 됩니다. 이 속성에 대한 파라미터는 맵의 상단에 추가되기를
원하는 COM_INTERFACE_ENTRY 매크로입니다. 예를 들어서,

[coclass,
com_interface_entry("COM_INTERFACE_ENTRY_NOINTERFACE(IMarshal)")]
class CManager : public IEmployee {/* other code... */};

위의 속성으로 인해서 인터페이스 맵의 상단에 QueryInterface 를 통해서 해당 인터페이스에 대해서
IMarshal 을 요구하게 되는 경우 E_NOINTERFACE 를 리턴하도록 하는 매크로를 추가할
것입니다.

등록
지금까지는, 여러분이 사용하게 될 C++ 속성에 대해서, 그것들을 사용하게 되는 경우 요구되어지는
최소한의 인자와 함께 예를 들어서 상세하게 설명을 하였습니다. 물론, 인터페이스와 coclass 들은 자신들을
구별하기 위해서 UUID 를 사용합니다. 그래서 __interface 와 클래스들은 [uuid()] 속성으로 UUID 를
지정할 수 있습니다. 이 속성은 VC++ 6.0 에서의 __declspec(uuid()) 와 동일한 역할을
수행합니다. __declspec(uuid()) 와 함께 사용되었던 __uuidof() 는 [uuid()]
를 쓴 경우에도 동일하게 사용할 수 있습니다. 또한, [module] 속성은 생성될 형식
라이브러리를 구분짓는 LIBID 를 지정하기 위해서 uuid 인자를 가질 수 있습니다. 만약 그러한 것들을 빼먹는 다면,
컴파일러는 적당한 UUID 를 생성해서 적용을 시킵니다.
다음은 uuid 의 사용법을 보여줍니다.

[module(dll,
name="MyLib", uuid="2DBAA5A5-85C6-44f7-85A3-207FD21EF0F5")] ;
[coclass, uuid("53A583E1-18B7-4194-A969-8C1BFA7A3F03")]
class CCounted : public ICount {/* code... */};

[module()] 속성은 uuid 를 인자로서 인정하기 때문에 '= ' 기호를 이용해서 값을
지정합니다. 반면에, [uuid()] 속성은 클래스나 꺽쇠괄호안에서의 인자로 적용이 되어집니다. [uuid()]
속성은 이름이 지정되지 않은 인자 하나를 요구할 뿐입니다.
COM 서버가 등록되는 시점에 해당 COM 서버안에 구현되어진 모든 COM 개체의 CLSID 가
레지스트리에 추가되어져야 합니다. 또한, coclass 는 관련 ProgID 를 가지게 되고, 그것을 위해서 버전 독립적인
ProgID 속성으로 [vi_progid()] 와 버전관리가 되는 ProgID 속성으로 [progid()] 가 마련되어져
있습니다. 거기다가 해당 구성요소는 콤포넌트 카테고리를 구현할 수 있는 데, 이것 또한 등록시에 레지스트리에 추가되어져야
합니다. 이러한 속성들은 어떻게 동작이 되어질 까요?.
[coclass] 속성은 해당 클래스에 대해서 "UpdateRegistry" 메소드를 추가하게 됩니다.
( 역주: 이 함수에 대해서는 이전 ATL 3.0 을 잘 공부하신 분은 친숙하게 느껴지실 것입니다. ) 하지만, ATL
3.0 에서는 등록코드에 대한 많은 부분이 "RGS" 스크립트 파일에 의존하지만, ATL 7.0
에서는 이진 데이터를 "CRegistryVirtualMachine" 이라 불리는 클래스에 넘겨주어
등록과정이 이루어 지게 됩니다. [coclass] 속성은 해당 클래스에 대해서 2 개의 배열로 된 정보를 추가합니다.
첫번째 배열은 레지스트리에 가해져야 할 변경 목록과 변경되어야 할 값들의 이진 데이터를 담고
있습니다. 두번째 배열은 이러한 동작에 의해서 사용되어지게 될 문자열 값을 키이름 또는 값자체로 가지고 있습니다.
2개의 배열이 담고 있는 데이터의 형식은 ATL 3.0 의 RGS 파일보다 더 간략화되어 있습니다. 이진데이터 형식이기
때문에 그 값을 확인하기에는 다소 어려움이 있을 테지만.
만약 해당 구성요소를 class factory 에 의해서 생성되지 않게 하려면, ( 예를 들어, COM
서버안의 다른 객체에 의해서만 생성되어지는 개체 ) 그 클래스를 "noncreatable" 로 표시를 해주어야 합니다.
그러한 클래스의 경우에는 CLSID 와 ProgID 를 가져서는 안되는 것이죠. 기본적인 [coclass()] 의 등록
과정에서 외부에서 생성되지 못하도록 재정의하려면, [noncreatable] 속성을 해당 클래스에 추가해 주어야 합니다.
이 속성은 3가지 사항을 지시합니다. 첫번째는, 코드를 포함하지 않는 빈 "UpdateRegistry" 함수를 해당
클래스에 포함시킵니다. 물론, 그렇게 되면 해당 함수가 호출되어도 등록과정은 이루어지지 않을 것입니다.
두번째로, 해당 클래스를 CComCoClass<> 로부터 상속받지 않게 합니다. 외부에서 생성할 수
없도록 했는 데, 그것을 위한 클래스 팩토리기능을 구현할 필요는 없기 때문입니다. 마지막
세번째는, 해당 모듈의 형식 라이브러리 안에 [noncreatable] 속성을 coclass 구문에
포함시켜 줍니다. 그럼으로써, Visual Basci 6.0 IDE 와 같은 응용프로그램에서 해당 COM 클래스가 생성될
수 없다는 것을 알 수 있도록 해줍니다.
현재 있는 모든 속성들이 다양한 환경을 완전히 만족해주는 형식 정보를 생성해 줄 수는 없습니다. 그런
경우를 위해서 "속성제공자" 가 CRegistryVirtualMachine 이 사용할 배열에 직접 값을 넣도록 해줄 수가
있습니다. 그렇게 레지스트리에 개발자가 원하는 특정값을 넣고 싶은 경우를 위해서 [rdx()],
[registration_script()] 속성이 제공됩니다. ATL 3.0 의 RGS 등록파일을 활용하고 싶다면
[registration_script()] 속성을 사용해서 사용자가 만들어 둔 "RGS" 스크립트 파일을 포함하게 할 수
있습니다. 이 속성으로 추가되는 레지스트리 값들은 [coclass] 로 인해 등록되는 레지스트리 값을 덮어쓰게 됩니다.
그렇기 때문에 원하는 레지스트리 등록값을 사용할 수 있는 것입니다. [resgistration_script()] 속성의
사용예가 아래에 나와 있습니다.

[coclass, registration_script(script="Counted.rgs")]
class CCounted : public ICount
{ /* other code */};

컴파일 시에, 속성 제공자는 명시된 Counted.RGS 파일을 불러 들여서 해석한 후,
CRegistryVirtualMachine 에 의해서 사용하게 될 배열에 값을 재정의하거나 추가하게 됩니다. 이
과정은 즉, 여러분이 제공하게될 RGS 파일역시 CRegistryVirtualMachine 클래스를 통해서 등록되어짐을
의미합니다.
또 다른 [rdx] 속성은 개체의 등록과정의 하나로 사용하기 위한 속성은 아닙니다. 왜냐면
그것은 언제라도 변경될 수 있는 레지스트리 값을 담고 있기 때문입니다. [rdx] 속성은 실행시에 레지스트리에 쓰고,
읽고, 지울 수 있도록 하는 하려는 목적으로 사용할 수 있습니다. 따라서, 개발자 입장에서는 개체마다의
목적에 맞는 레지스트리 값을 저장하려는 데에 사용할 수 있는 것입니다. 이 속성은 클래스의 데이터 멤버에
적용될 수 있습니다. ( 역주: 즉, 상태정보를 레지스트리에 기록할 수 도 있는 것입니다. 이는 매우 유용한 기능일 수
있습니다. ) 그에 대한 사용예가  표 9 에 있습니다.
표 9 에서는 해당 클래스의 ProgID 레지스트리 키 아래에 usagecount 라는 이름의 값으로
클래스의 m_dwCount 변수 값을 저장하도록 하고 있습니다. 컴파일 시에 이 속성을 만나게 되면, 역시 속성 제공자가
호출되어지고 RDX 라는 맵을 생성하고 [rdx] 속성으로 명시된 클래스 내의 모든 데이터 멤버를 그 맵에 추가해
놓습니다. 그 후, RegistryDataExchange 함수가 호출될 때마다 해당 맵에 등록된 데이터가 레지스트리로부터
읽혀지거나, 써지게 됩니다.
표 9  에서는, FinalStruct 메소드와 FinalRelease
메소드 2가지 경우에서 RegistryDataExchange 를 호출하게 해놓았는 데, 이는 결국, COM 구성요소가
생성되면서 레지스트리로부터  값을 읽어서 m_dwCount 변수에 채우게 되고 반대로 COM 구성요소가 제거될
때는 m_dwCount 변수가 가진 값을 레지스트리에 쓰게 되는 것입니다.

Component 카테고리
등록부분에서 살펴볼 마지막 단계는 Component 범주입니다. 범주는 2가지 측면으로
사용되어집니다. 하나는, 구성요소가 어떤 범주에 해당하는 기능을 구현했음을 알리도록 등록하는 것입니다. 예를
들어서, 어떤 구성요소가 ActiveX® Control 을 구현했다면, CATID_Control 범주를 등록해
두는 것입니다. 그렇게 되면, 현재 시스템에 등록된 ActiveX® Control 을 찾기 위해서 Category
Manager 에게 CATID_Control 을 구현한 콤포넌트들을 요청하기만 하면 되는 것입니다. 반대로 보면,
구성요소가 자신이 활성화될 컨테이너의 자격요건을 알아보는 데에 쓰일 수가 있습니다. 예를 들어, 컨테이너가 특정한
범주의 기능을 갖추어야만이 해당 Component 를 포함할 수 있는 경우가 있을
것입니다.
ATL 3.0 에서는, 해당 클래스가 카테고리 맵을 가지고 있는 경우에 등록되어집니다. ATL
에서 제공되는 BEGIN_CATEGORY_MAP 과 END_CATEGORY_MAP 매크로
사이에 IMPLEMENTED_CATEGORY 매크로를 통해서 구현되어집니다. ATL 7.0 에서는 물론, 이것이
속성으로 대체되었습니다. [implements_category()] 와 [requires_category()] 가
바로 그것입니다. 예를 들어보면,

[coclass, implements_category("CATID_Insertable")]
class CCounted : public ICount
{ /* other code */};

위의 코드는, CCounted 구성요소가 컨테이너에 삽입되어 질 수 있는 범주에 속함을
알려줍니다.

Code Modules
새로 나온 "속성" 은 단순히 COM 관련 코드만을 생성하는 것은 아닙니다. 속성들로 인해서 DLL 모듈
진입코드도 생성되어집니다. ATL 3.0 에 젖어있던 사고방식으로는  표 10 의 코드만이 실제 COM 서버를 제작하는 전부라고
하면 믿기지 않을 것입니다. 눈치가 빠른 독자라면, 표 10의 코드에서 [module()] 속성에 dll
인자를 준 것을 알아채셨을 것입니다. 이 인자는 컴파일러와 링커로 하여금 Win32 DLL
관련된 코드를 생성하도록 해줍니다. 만약 이 DLL COM 서버를 EXE 서버로 만들고 싶다면 단순히 /LD
스위치를 제거하는 것만으로 가능합니다. 기존 ATL 3.0 에서의 DLL 과 EXE 간의 COM 서버 코드와
비교해 본다면 새로운 문법의 "속성" 을 쓰는 것이 얼마나 쉬운 것임을 알게 됩니다. 물론, 프로그램을
".NET" 에서부터 시작하는 사람은 초심자들은 이해못하겠지만, 저처럼 ATL 3.0 에서 구르던 사람은
"속성" 이 제공해주는 편리함은 이루 말할 수 없습니다. 간단히 속성의 인자를 바꾸는 것으로, 프로젝트
전체에 영향을 주는 작업이 손쉽게 이루어지는 것입니다.  표 11 에서는 [module()] 속성이 모듈의 Target 에 따라서 어떤
코드를 생성하는지를 보여줍니다.
한가지 더 언급하자면, ATL 7.0 DLL 에서는 [module()] 속성으로 인해 .DEF 파일로
함수를 export 시키는 작업이 필요가 없습니다. 그렇다면, COM DLL 이 노출시키는 표준 함수 (
DllRegisterServer, DllUnRegisterServer, DllMain, DllCanUnloadNow,
DllGetClassObject ) 이외에 또 다른 함수를 export 시키고 싶다면 어떻게 해야 할까요? 자동화된
속성으로 인해, DLL 함수 export 는 더더욱 쉬워졌습니다. 단지, 함수 정의를 할 때 _declspec 만 추가시켜
주면 됩니다. 다음은, 그 예입니다.

extern
"C" _declspec( dllexport )
void
MyBeep()
{

MessageBeep(MB_OK); }[module(dll,
name="Test"), idl_module(name="TestModule",
dllname=
"test.dll")];

이제 해당 DLL 의 Import Library 를 클라이언트 프로그램에 추가한 뒤, 컴파일을 하게 되면
MyBeep 함수는 호출되어질 것입니다.

////////////////////////// 문제점 1
/////////////////////////////////////////
원래 이 글의 원문에는 함수를 노출시키기
위해 [idl_module()] 속성과 다음의 라인을 추가해야 된다고 되어 있었습니다.

#pragma comment(linker, "/EXPORT:MyBeep=_MyBeep")

하지만, 그렇게 하면, "Depends.exe" 로 MyBeep 함수가 정상적으로 export 된 것을 확인할
수 있었지만 클라이언트에서 .LIB 파일과 함께 링크시킬 때 해당 함수를 찾을 수 없다는 에러를 만날 수 있었습니다. 다른
식으로 고민을 하다가 예전처럼 _declspec 예약어를 써봤는데 정상적으로 컴파일이 되는 것이었습니다. 그 때에는
[idl_module()] 속성도 필요치 않았습니다.

제가 [idl_module()] 속성을 제대로 사용하지
못했을 수도 있는 데 혹시, 그 구문에 대해서 함수 export 관련한 방법을 아시는 분은
조언부탁드립니다.
//////////////////////////////////////////////////////////////////////////

Class 구현
위의 글중에서, 속성이 클래스의 직접적인 구현에 영향을 미친다고 이미 얘기를 했습니다. 바로, 그렇게
클래스의 구현과 관계된 속성에 대해서 알아보겠습니다. 그러고 보니, 이미 2개의 속성은 소개를 드린 것 같습니다.
바로 [coclass] 와 [noncreatable] 이죠. 2개의 속성 모두 적용된 클래스에 대해서 기반 클래스를
조작함으로써 생성될 코드에 영향을 주게 됩니다. [coclass] 속성은 기반 클래스를 추가하는 반면에
[noncreatable] 속성은 해당되는 기반 클래스를 제거하는 역할을 해줍니다. 이러한 속성들은 코드를
추가하기도 하지만, 맵을 추가하는 경우도 있습니다. 어떤 속성들은 해당 클래스에 적용시키므로 인해서 coclass
구현 자체를 바꿀 수도 있습니다.
모든 coclass 는 반드시 쓰레드 모델링에 대해서 명시를 해야합니다. 즉, 오직 하나의 쓰레드만이
해당 구성요소를 접근할 것인지, 또는 여러개의 쓰레드에서 동시에 접근할 수도 있는지에 대해서 지정해야 되는 것입니다.
전자의 경우, 구성요소는 동기화 관련 코드를 제공할 필요가 없습니다. 반대로, 후자의 경우에는 반드시 데이터 멤버와
쓰레드에 민감한 코드에 대해서 동기화 관련 코드로 보호를 해주어야 합니다. 이러한 동기화 정책은 component
가 작동하게 될 apartment 를 결정하는 중요한 요소입니다. 동기화 코드 를 제공하지
않는 구성요소는 STA 에서 작동되어야 합니다. 반면, 스스로 동기화 코드를 구현한 구성요소는 MTA
에서 작동될 수 있습니다. 덧붙여서, 만약 구성요소가 윈도우 메시지 큐를 요구하거나 ( 예를 들어서, 사용자 인터페이스를
지닌 경우 ) 쓰레드 친화성과 관련되어 있다면 ( 예를 들어서, TLS 를 쓰고 있는 경우 ) , 해당 구성요소는 반드시
STA 에서 실행되어져야 합니다.
구성요소의 쓰레딩 모델은 [threading()] 속성으로 나타내어집니다. 이 속성은 쓰레딩 모델을
나타내는 단 하나의 인자를 가집니다. 인자의 값으로는 apartment, single, free,
neutral, both 마지막으로 rental 이 될 수 있습니다. 만약, COM 구성요소가 DLL
방식으로 제공되어진다면, 이 속성은 해당 클래스가 상속받게 될  CComObjectRootEx<> 의
템플릿 인자값을 결정하게 되고, coclass 와 관계된 레지스트리의 ThreadingModel 값에 영향을 주게
됩니다. "rental" 모델일 경우에는, 해당 속성으로 인해 구성요소의 InprocServer32 레지스트리
키값에 동기화 값을 추가하게 됩니다. (역주: 저 자신은 아직까지 단 한번도 rental 모델을 명시적으로 써 본 적이
없어서 이 부분에 대한 명확한 설명을 제시해 드리지 못하겠습니다. )
만약 구성요소가 MTA 에서 동작시키겠다면, 개발자 자신이 직접 쓰레드에 민감한 부분에 대해서 동기화
객체를 써서 공유 데이터 등을 보호해야 합니다. C++ 속성에서는 [synchronize] 라는 속성을 제공함으로써 그러한
기능을 대신 수행해 줍니다. 다음의 예제를 보십시오.

[coclass, threading("free")]
class CMTAObject : public IWork
{
CComBSTR m_bstr;
public:
[synchronize]
HRESULT DoSomething(BSTR bstrParam)
{
m_bstr = bstrParam;
return S_OK;
}
};

DoSomething 메소드는 m_bstr 데이터 멤버에 값을 기록합니다. 만약 2개의 쓰레드가 동시에 그
함수를 부르게 된다면, 결과는 예측할 수 없습니다. 하지만, 위의 코드에서 DoSomething 메소드에
[synchronize] 속성이 추가된 것을 볼 수 있습니다. 그 속성은 스택상에
CComObjectRootEx<> 기반 클래스에 정의된 ObjectLock 클래스의 인스턴스를 생성해
줍니다. ObjectLock 클래스 생성자는 해당 클래스의 쓰레딩 모델에 적합한 Lock 메소드를
호출하게 되고, 해당 메소드가 수행을 마치고 스택이 제거되는 시점에서 ObjectLock 의 소멸자에서
Unlock 메소드가 호출이 되어집니다. 결과적으로, 해당 메소드내의 전체 코드 영역이, 지정된 시간에 단 한개의
쓰레드만이 실행될 수 있도록 해주는 것입니다. 물론, [synchronize] 속성을 제거하면 ObjectLock
클래스와 관계된 코드역시 제공되지 않습니다. 그런 경우, CComObjectRootEx<>::Lock 과
CComObjectRootEx<>::Unlock 함수를 필요한 부분에 명시적으로 호출해 주는 방식을 써도 같은
효과를 얻게 됩니다.
COM 구성요소와 관계된 것 중에 빠질 수 없는 것이 Aggregation ( 통합 )
입니다. 예를 들어, A 라는 구성요소를 B 라는 구성요소가 통합을 하게 되면, 외부에서는 A 가 있다는 사실을
모른 체, B 구성요소의 인터페이스를 다루게 되는 것입니다. 하지만, 실제 B 구성요소가 수행하는 역할의 대부분은 A
구성요소의 것을 활용하는 것입니다. C++ 속성에서는 통합을 위해서
[aggregatable()] 과 [aggregates()] 속성을 제공해 주고 있습니다.
[aggregatable()] 속성은 해당 구성요소가 외부의 COM 구성요소에 의해서 통합되어질 수 있음을 나타내
주고 또한, 그것으로 인해 구성요소의 class factory 의 구현에 영향을 주게 됩니다. 속성이 가질 수 있는
인자로는 "never", "allowed", "always" 가
있습니다. 만약 IClassFactory::CreateInstance 의 첫번째 파라미터 IUnknown
값이 NULL 이 아니고 해당 구성요소에 지정된 속성이 [aggregatable("never")] 일 경우, class
factory 는 CLASS_E_NOAGGREGATION 이라는 에러를 리턴하게 됩니다. 반대로, 첫번째 파라미터에
"NULL" 이 쓰이고, 해당 구성요소의 속성이 [aggregatable("always")] 라면, class
factory 는 E_FAIL 이라는 에러를 반환해서 구성요소 생성에 실패하게
됩니다.
이름에서 알 수 있듯이, [aggregates()] 속성은 해당 구성요소가 생성될 때 다른 구성요소를
내부에 통합하고 있음을 말해 줍니다. 이 속성은 생성될 인터페이스 맵에
"COM_INTERFACE_AUTOAGGREGATE_BLIND" 라는 매크로를 추가하게 됩니다. 이
매크로는 내부에 통합할 개체의 CLSID 를 인자로 갖게 되는 데, 바로 그 인자를
[aggregates()] 의 인자로 넣어주면 됩니다. 하지만, 이 방식이 그다지 좋은 것은 아닙니다. 왜냐면 그로 인해서
통합할 개체에 대한 모든 인터페이스를 노출시켜야 되기 때문입니다. 만약 통합할 개체에 대해서 단일 인터페이스만을 노출시키고
싶다면, 그런 경우에는 [com_interface_entry()] 매크로를 쓰는 것이 바람직합니다. 그 매크로는 인터페이스
맵에 COM_INTERFACE_ENTRY_AUTOAGGREGATE 매크로를 추가하게 됩니다.
클래스 구현의 마지막 주제로 예외를 살펴보겠습니다. COM 구성요소는 절대로 예외를 발생시켜서는
안됩니다. 그 대신, 에러 개체를 이용해서 에러에 관한 정보는 넘겨 줄 수 있습니다. 바로
ISupportErrorInfo 인터페이스를 구현해야 그러한 작업이 가능해줍니다. C++ 속성에서는 그러한 경우를
위해서 [support_error_info()] 가 마련되어져 있습니다. 이 속성은 단 하나의 인자를 가지게 되는 데,
에러 개체를 생성하는 구성요소에 대한 인터페이스의 이름을 넘겨주어야 합니다. 하나의 구성요소에서 여러개의
인터페이스를 구현하는 경우, 마찬가지로 각각에 대해서 [support_error_info()] 속성을 기술해 주면
됩니다.

통합된 이벤트 모델
COM Connection Points 는 콜백 인터페이스를 통한 방식으로 그
기능이 구현되어져 있습니다. 하나의 구성요소가 다중 인터페이스에 대한 이벤트를 제공할 수 있고, 다중 클라이언트가 하나의
구성요소로부터도 이벤트를 받을 수 있는 상당히 현실적인 구조를 가지고 있습니다.에 대해서 는 유연성있는
구조입니다. 하지만, 그러한 유연성이 아무런 댓가도 없이 주어지지는 않습니다. 연결자체를 인터페이스
기준으로 이루어지기 때문에 inproc 구성요소에 대해서 적합합니다. 사실, 인터페이스 마샬링의 부하를 무시할 수 없을
정도이기 때문에, 원격 구성요소에서의 콜백 인터페이스 구현은 다소 무리가 있어 보입니다.
역시, 이벤트 또한 C++ 속성에서 지원이 되어집니다. 이벤트를 발생시킬 클래스는
[event_source(com)] 속성으로 표시해 두면 됩니다. 이 속성에 쓰이는 인자는, 생성될 이벤트의 형에
대해서 명시를 해야 합니다. 그에 대해서는 3가지 값이 있습니다. "native" 인자는 C++ 함수
콜백 방식을 이용해서 이벤트를 생성하게 되고,
"com" 인자는 말그대로 connection point 이벤트를 생성하게 되고,
"managed" 인자는 Microsoft.NET 형식의 이벤트 구조를 제공하게 됩니다. 물론, "managed"
인자는 관리된 클래스에 대해서만 사용이 가능합니다. 이 기사에서는, 오직 Connection Point 이벤트 방식에
대해서만 다룰 것입니다. 그렇긴 해도 나머지 2방식도 비슷한 형식의 구문을 사용하게 됩니다. 사실, 그 비슷함으로 인해
MS 는 "Unified Event Model" 이라고 명명한 것입니다.
COM 이벤트를지원하기 위한 가장 간단한 방법은,  VS.NET 에서 ATL 마법사를 이용하는
것입니다. 그럼, 지정된 클래스에 [event_source(com)] 이라는 속성을 추가하게 됩니다. 그러나,
여러분이 알다시피, Connection Points 를 제공하기 위해서는 Event Interface 가 필요하고 그
이벤트를 받기 위해서는 이벤트 핸들러 구성요소가 그 Event Interface 를 구현해야 하는 것입니다.
VS.NET 의 ATL 마법사는 해당 프로젝트에 dispinterface 를 추가시키고, 이벤트 소스 클래스에
__event 키워드로 그 dispinterface 를 선언해 둠으로써 이벤트 인터페이스로 사용하게 되는
것입니다. 이에 대한 예를  표 12 에서 볼 수 있습니다.
__event 예약어는 이벤트를 위해서 추가된 C++ 예약어들중의 하나입니다. 표 12 의 예제에서,
이 예약어는 해당 클래스가 _IEventSink 인터페이스에 대한 이벤트를 생성할 수 있 있음을 명시해
주는 역할을 합니다. COM event source 이기 때문에, __event 예약어는 지정된 인터페이스(
여기서는 _IEventSink )에 대한 Connection Point 를 추가하게 되는 것입니다.
추가적으로, 속성 제공자" 는 그 이벤트를 호출할 수 있는 메소드를 생성하게 됩니다. ATL 3.0 시절에는,
"Fire_" 로 시작되는 이벤트 이름을 가진 메소드가 "CS" 접미사로 끝나는 헤더 파일에
제공되었습니다. 그와는 대조적으로, C++ 속성에서는, 새로운 이벤트 메소드는 메소드 명과 그 메소드를 담고 있는
인터페이스 명의 조합으로 이루어집니다. 그렇기 때문에, 예제에서의 SomethingHappened 이벤트를 발생시키기
위해서는 _IEventSink_SomethingHappened 라는 이름으로 메소드 호출을 해주어야 합니다. 아래는 그
예제입니다.

STDMETHODIMP CMyObject::DoSomething()
{
_IEventSink_SomethingHappened(CComBSTR(L"DoSomething called"));
return S_OK;
}

이벤트에 관계되어 추가된  또다른 C++ 예약어 2개는 다행히도 별로 어렵지 않게 기억할 수
있는 __hook 과 __unhook 입니다. 이 속성들은 [event_receiver()] 속성을 가진
클래스에서 사용됩니다. [event_receiver()] 속성은 그 클래스가 이벤트를
위한 처리기임을 명시해줍니다. 이벤트 수신을 위한 예제 코드가  표 13 에 나와 있습니다.
그 예제에서는 다소 중요한 사항이 포함되어 있습니다. 먼저, #define _ATL_ATTRIBUTES
를 했고, COM 과 관계된 속성을 사용한 것입니다. [module()] 속성을 쓰긴 했지만, 해당
실행파일이 COM 서버로 사용되어지지는 않습니다. 왜냐면, 여타의 class factory 도 가지고 있지 않기
때문입니다. 단지, 그 실행파일이 이벤트 싱크 개체를 가지고 있다는 것을 의미할 뿐입니다.
두번째로, CMyHandler 이벤트 핸들러 클래스가 Connection Point 이벤트를 처리할
것이라는 것을 지정하기 위해 [event_receiver(com)] 속성을  사용한
것입니다. 물론, 그것과 함께 이벤트 싱크 인터페이스에 관계된 메소드를 구현해야 합니다. 하지만, 자세히 보시면
그 클래스가 _IEventSink 로부터 상속받지 않았음을 알 수 있습니다. 상속없이, _IEventSink 가 가지고
있는 메소드에 대응하는 핸들러 메소드를 가지고 있을 뿐입니다.
Connection Point 가 구현된 어떤 구성요소를 사용하려 하면, 반드시 Connection
Point 를 통해서 이벤트를 받기를 원한다고 지정하기 위해서 해당 구성요소에 대해 advise 를 해주어야 합니다.
통합된 이벤트 모델을 사용해서 이것을 구현하기 위한 표준 방법은 __hook 키워드를 사용하는 것입니다.
__hook 은 3개의 인자를 갖고 있는 데, 첫번째는 받고 싶은 이벤트 메소드의 이름이고, 두번째는 이벤트 소스에
대한 인터페이스 포인터, 세번째는 해당 이벤트에 대해서 실제 처리하려는 코드를 가진
메소드의 포인터입니다. 속성을 가진 이벤트 인터페이스는 __hook 예약어가 제대로 동작하기
위해서 해당 코드에서 접근가능해야 합니다. 바로 그 이유 때문에, 표 13에서 보듯이 _IEventSink 인터페이스
정의를 중복 코딩을 하게 된 것입니다. ( 역주: 그래도 이런 경우, import 등의 방법을 통해서 원본 개체
DLL/EXE 로부터 컴파일시에 자동으로 추출해 주는 것이 있었으면... 하는 아쉬움이 있네요.
)
예제의 main() 함수를 보면, 처음에 이벤트 소스 구성요소가 CoCreateInstance 함수를
이용해서 생성되어집니다.  그런 후, CMyHandler 클래스의 인스턴스가 스택상에 생성되고, 이벤트 소스
클래스에 대해서 advise 를 하게 됩니다. 다음 라인에서 "Svr.MyObject.1" 구성요소의
DoSomething 메소드를 호출하는데, 그 메소드안에서는 이벤트를 발생시키는 코드 (
_IEventSink_SomethingHappened )가 담겨져 있습니다. 클라이언트 코드가 MTA 에서
실행되고 있기 때문에, 해당 구성요소는 inproc MTA 개체입니다. 그래서 주 쓰레드가 아닌 MTA 쓰레드
상에서 이벤트가 발생하게 되고, __hook 코드로 인해서 해당 이벤트 처리기 함수 ( 예제에서는,
Handler ) 의 실행으로 이어지게 되는 것입니다. Sleep 함수는 예제를 간단히 하기 위해서 쓴 것
뿐입니다. 단지, 주 쓰레드가 Handler 함수의 처리가 끝나기 이전에 종료되는 것을 막아주는 역활 이외에는
아무런 의미도 없습니다. CMyHandler 인스턴스가 소멸되기 전에, Unadvise 를 호출한 것을 볼 수 있습니다.
예상하시겠지만, 그 안에서 __unhook 코드로 인해 더 이상의 이벤트를 받지 않게 되는 것입니다.
위에서 본 것처럼, 새로운 [event_source()], [event_receiver()]
속성으로 인해서 ATL 3.0 시절에 비해 이벤트 발생 및 처리를 위한 코드를 후러씬 더 간단하게 작성할 수 있다는 것을
아시게 되었을 것입니다. 그렇긴 하지만, 그 2가지 속성의 진가는 이벤트에 대해서 C++ 함수 콜백 이나 .NET 이벤트
방식을 사용할 때 더욱 발휘됩니다.

////////////////////////// 문제점 2
/////////////////////////////////////////
직접 해보지 않고는 자신의 것으로 만들 수
없습니다.

서버 : [event_source("com")] 적용된 Free 쓰레딩 모델 COM
구성요소
STDMETHOD(
DoWork )( [in] BSTR text
)
{

_bstr_t bstrText =
text;
::MessageBox(
NULL, LPCSTR( bstrText ), NULL, MB_OK
);
}
클라이언트
: [event_receiver("com")] 를 사용, STA 로 CoInitialize.

한번
해보세요.
클라이언트에서 STA 로 초기화 한 후, "Free" Server 개체를 생성하시면 정상적으로 생성이
됩니다. 하지만, pObj->DoWork( "TEST" ); 라는 코드를 실행하면,
"TEST" 라는 문자열이
전달되지 않습니다. 하지만, 클라이언트를 CoInitializeEx 에
COINIT_MULTITHREADED 전달해서
MTA 상에서 서버를 생성해서 DoWork 을
호출하면 정상적으로 문자열이 전달되어집니다. 상황이 바뀌어도
마찬가지입니다.
서버를 "apartment" 로 하고 클라이언트를 "MTA" 로 초기화하면
DoWork("TEST");
호출에서 "TEST" 문자열이 전달되지 않습니다. 물론, "STA"
클라이언트가 호출하면
정상동작이 됩니다.

서버: MTA, 클라이언트: STA 인 상황에서도 문자열이 정상
전달되는 경우가 있습니다. 클라이언트에 구현된 [event_receiver("com")] 관련된 속성을
제거하면,
정상적으로 "TEST" 문자열을 받게 됩니다.

이에 대한 예제 테스트 코드는 http://user.chollian.net/~kevin13/srclist/article/cppattr.zip
에 있습니다.

이 문제를 해결하시는 분이 계시면, 꼭 메일 부탁드립니다. 가르침을
기다리겠습니다.
////////////////////////////////////////////////////////////////////////////

결론
C++ 속성을 사용할만한 가치가 있다는 2가지 명백한 이유가 있습니다. 하나는, 해당 클래스의 구현에
속성이 적용된다는 것입니다. 이로 인해 해당 클래스에 대한 정보가 기존에는 산재해 있던 것이 하나의 파일로 모아지게 되는
효과가 있습니다.  그렇지 않아도 방대한 프로젝트 파일들속에서 헤매고 있는 개발자에게는 해당 COM 클래스에
대해서 알아보기 위해 하나의 파일만을 참조하게 되는 것은 편리한 일이 아닐 수 없습니다. 두번째 이유는, C++
속성이 코드의 양도 줄인다는 것입니다. ( 역주: 사실 처음 VS.NET 에서 ATL 프로젝트를 만들고
나서는, ATL 3.0 에 비해 상대적으로 아주 빈약한 코드의 양 때문에 한동안 허전해 하기도 했습니다.
)
여러분은 ATL 프로젝트에서 C++ 속성을 원하는 만큼 취사선택해서 사용하실 수
있습니다. 속성제공자는 사용된 속성에 준하는 코드가 없는 경우에만 ATL 코드를 생성해낼 것입니다. 이것은 무엇을
말해주는 것일까요? 즉, 기존의 ATL 프로젝트를 VS.NET 에서 열어서 속성을 사용해 나가는 방식으로 수정을
해도 된다는 것입니다. 분명한 것은 새로 도입된 속성으로 인해 생성되는 코드는 이전의 ATL 버전의 것보다 더욱
효율적이라는 것입니다.

For related articles see:
http://msdn.microsoft.com/msdnmag/issues/0900/vsnet/vsnet.asp
http://msdn.microsoft.com/msdnmag/issues/1000/atlserv/atlserv.asp

Richard Grimes is the author of several
books on COM and ATL for Wrox Press, including Professional ATL
COM Programming and Professional Visual C++ 6 MTS
Programming. Richard can be contacted at [email-masked].

Figure 2 C++ Attributed Code

// in manager.h
[coclass]
class ATL_NO_VTABLE CManager :
public IEmployee
{
public:
STDMETHODIMP DoWork(BSTR bstrTask);
};

Figure 3 CManager Class in ATL 3.0

class ATL_NO_VTABLE CManager :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CManager, &CLSID_Manager>,
public IDispatchImpl<IEmployee, &IID_IEmployee>
{
public:
DECLARE_REGISTRY_RESOURCEID(IDR_MANAGER)

BEGIN_COM_MAP(CManager)
COM_INTERFACE_ENTRY(IEmployee)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

STDMETHOD(DoWork)(BSTR bstrTask);
};

Figure 5 Calling the CManager Object

IDispatch* pDisp;
CoCreateInstance(__uuidof(CManager), NULL,
CLSCTX_INPROC_SERVER, __uuidof(IDispatch),
(void**)&pDisp);

BSTR bstr = SysAllocString(L"call meeting");
VARIANT var;
VariantInit(&var);
V_BSTR(&var) = bstr;
V_VT(&var) = VT_BSTR;
DISPPARAMS params = {&var, NULL, 1, 0};
pDisp->Invoke(1, IID_NULL, 0, DISPATCH_METHOD,
&params, NULL, NULL, NULL);
VariantClear(&var);
pDisp->Release();

Figure 6 Injected Code Created for IDispatch::Invoke

[coclass]
class CManager : public IEmployee,
/*+++ Added Baseclass */
public CComCoClass<CManager, &__uuidof(CManager)>,
/*+++ Added Baseclass */ public
CComObjectRootEx<CComSingleThreadModel>,
/*+++ Added Baseclass */
public IProvideClassInfoImpl<&__uuidof(CManager)>
{
public:
STDMETHODIMP DoWork(BSTR bstrTask);
//+++ Start Injected Code
#injected_line 17 "d:\\development\\AttributeTest\\manager.h"
virtual HRESULT STDMETHODCALLTYPE IEmployee::Invoke(
/* [in] */ DISPID dispIdMember,
/* [in] */ REFIID riid,
/* [in] */ LCID lcid,
/* [in] */ WORD wFlags,
/* [out][in] */ DISPPARAMS *pDispParams,
/* [out] */ VARIANT *pVarResult,
/* [out] */ EXCEPINFO *pExcepInfo,
/* [out] */ UINT *puArgErr)
{
(void) riid;
(void) dispIdMember;
(void) lcid;
(void) wFlags;
(void) pExcepInfo;
(void) puArgErr;
HRESULT hr = S_OK;
if (pDispParams == 0) {
return DISP_E_BADVARTYPE;
}
if (pVarResult != 0) {
VariantInit(pVarResult);
}
switch (dispIdMember) {
case 1:
{
BSTR i1 = (BSTR) V_BSTR(&pDispParams->rgvarg[0]);
hr = ((IEmployee*)this)->DoWork(i1);
if (pVarResult != 0) {
V_VT(pVarResult) = VT_ERROR;
V_ERROR(pVarResult) = hr;
}
break;
}
default:
return DISP_E_MEMBERNOTFOUND;
}
return hr;
}
virtual HRESULT STDMETHODCALLTYPE IEmployee::GetIDsOfNames(
/* [in] */ REFIID riid,
/* [size_is][in] */ LPOLESTR *rgszNames,
/* [in] */ UINT cNames,
/* [in] */ LCID lcid,
/* [size_is][out] */ DISPID *rgDispId)
{
(void) riid;
(void) rgszNames;
(void) cNames;
(void) lcid;
(void) rgDispId;
static LPOLESTR names[] = { L"DoWork" };
static DISPID dids[] = { 1 };
for (unsigned int i = 0; i < cNames; ++i) {
int fFoundIt = 0;
for (unsigned int j = 0;
j < sizeof(names)/sizeof(LPOLESTR); ++j) {
if (lstrcmpW(rgszNames[i], names[j]) == 0) {
fFoundIt = 1;
rgDispId[i] = dids[j];
}
}
if (fFoundIt == 0) {
return DISP_E_UNKNOWNNAME;
}
}
return S_OK;
}
HRESULT TypeInfoHelper(REFIID iidDisp, LCID /*lcid*/,
ITypeInfo** ppTypeInfo)
{
if (ppTypeInfo == NULL) {
return E_POINTER;
}
*ppTypeInfo = NULL;
TCHAR szModule1[_MAX_PATH];
::GetModuleFileName(_AtlBaseModule.GetModuleInstance(),
szModule1, _MAX_PATH);
USES_CONVERSION;
CComPtr<ITypeLib> spTypeLib;
HRESULT hr = LoadTypeLib(T2OLE(szModule1), &spTypeLib);
if (SUCCEEDED(hr)) {
CComPtr<ITypeInfo> spTypeInfo;
hr = spTypeLib->GetTypeInfoOfGuid(iidDisp, &spTypeInfo);
if (SUCCEEDED(hr)) {
*ppTypeInfo = spTypeInfo.Detach();
}
}
return hr;
}
virtual HRESULT STDMETHODCALLTYPE IEmployee::GetTypeInfoCount(
unsigned int* pctinfo)
{
if (pctinfo == NULL) {
return E_POINTER;
}
CComPtr<ITypeInfo> spTypeInfo;
*pctinfo =
(SUCCEEDED(TypeInfoHelper(
__uuidof(IEmployee), 0, &spTypeInfo))) ? 1 : 0;
return S_OK;
}
virtual HRESULT STDMETHODCALLTYPE IEmployee::GetTypeInfo(
unsigned int iTInfo, LCID lcid, ITypeInfo** ppTInfo)
{
if (iTInfo != 0) {
return DISP_E_BADINDEX;
}
return TypeInfoHelper(__uuidof(IEmployee), lcid, ppTInfo);
}
BEGIN_COM_MAP(CManager)
COM_INTERFACE_ENTRY(IEmployee)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IProvideClassInfo)
END_COM_MAP()
//? End Injected Code
// other code omitted
};

Figure 7 General C++ Attributes for Generating Type
Information

Attribute

IDL
Equivalent

Description

cpp_quote

cpp_quote
statement

Adds a
cpp-quote statement to the generated IDL file. The quoted
string will be added to the header file generated from the
IDL

emitidl

Used to
turn on and off IDL generation

export

Used to
add a union, typedef, enumeration, or struct to the generated
IDL

idl_quote

Adds an
IDL statement to the generated IDL file

import

import
directive

Inserts an
IDL file into the generated IDL file outside of the generated
library block

importidl

Merges an
IDL file into the generated file so that the statements in the
imported file's library block are placed in the resultant
file's library block, but the statements outside the imported
file's library block placed outside of the resultant file's
library block

importlib

importlib
directive

Gives
access to types defined in a compiled type
library

include

include
directive

Inserts a
header file into the generated IDL file outside the library
block

includelib

Inserts an
IDL or header file into the generated IDL file after the
importlib statement inside the library block

library_block

Places the
item inside the library block of the generated IDL
file

Figure 8 Compiler-generated Files

File

Description

서버:
_employee.idl
클라이언트: vc70.idl

The
generated IDL file

서버: _employee.h
클라이언트:
vc70.h

C/C++
bindings header file

서버: _employee_i.c
클라이언트:
vc70_i.c

Constants
declaration file

서버: _employee_p.c
클라이언트:
vc70_p.c

Proxy-stub
implementation file

dlldata.c

Entry
points for the proxy-stub file

employee.dll, employee.obj

Empty DLL
code generated for employee.cpp

Figure 9 [rdx] Attribute Applied to m_dwCount Data

[coclass, progid("MySvr.Counted.1")]
class CCounted : public ICount
{
public:
[rdx(key="HKCR\\MySvr.Counted.1",
valuename="usagecount", regtype="dword")] DWORD m_dwCount;
CCounted() : m_dwCount(0){}
HRESULT FinalConstruct()
{
RegistryDataExchange(eReadFromReg);
m_dwCount++;
return S_OK;
}
void FinalRelease()
{
RegistryDataExchange(eWriteToReg);
}
/* other code */
};

Figure 10 A One File Implementation of a COM Server

// manager.cpp
// compile with cl /LD manager.cpp
#define _ATL_ATTRIBUTES
#include <atlbase.h>
#include <atlcom.h>
using namespace ATL;

[module(dll, name="OneFileServer")];

[dual]
__interface IEmployee : IDispatch
{
[id(1)] HRESULT DoWork(BSTR bstrTask);
};

[coclass]
class CManager : public IEmployee
{
public:
HRESULT DoWork(BSTR bstrTask)
{
ATLTRACE(_T("I am about to %S\n"), bstrTask);
return S_OK;
}
};

Figure 11 Code Generated for the [module()] Parameter

dll

exe

service
and service_name

Entry
point

DllMain

_tWinMain

_tWinMain

_Module

CAtlDllModuleT<>

CAtlExeModule<>

CAtlServiceModuleT<>

Other
exported functions

DllGetClassObject, DllRegisterServer,
DllUnregisterServer, DllCanUnloadNow

Figure 12 Event Generation Code

[dispinterface, hidden, uuid("72AB1227-E965-4D66-B09D-CB9C5F282A0F")]
__interface _IEventSink : IDispatch
{
[id(0)] void SomethingHappened([in]BSTR strDesc);
}

[object, dual, uuid("F65FFE64-8B16-4018-A6C0-2CBE633364E4")]
__interface IEventSource : IDispatch
{
[id(0)] HRESULT DoSomething();
}

[coclass, event_source("com") , threading("free"), progid("Svr.MyObject.1")]
class CMyObject : public IEventSource
{
public:
__event __interface _IEventSink;
HRESULT DoSomething();
/* other code */
};

Figure 13 Event Receiver Code

#define _ATL_ATTRIBUTES
#define _WIN32_DCOM
#include <atlbase.h>
#include <atlcom.h>
#include <stdio.h>
using namespace ATL;

[module(name="MyHandler")];

[dispinterface, hidden, uuid("72AB1227-E965-4D66-B09D-CB9C5F282A0F")]
__interface _IEventSink : IDispatch
{
[id(0)] void SomethingHappened([in]BSTR strDesc);
}

[object, dual, uuid("F65FFE64-8B16-4018-A6C0-2CBE633364E4")]
__interface IEventSource : IDispatch
{
[id(0)] void DoSomething();
}

[event_receiver(com)]
class CMyHandler
{
public:
void Advise(IEventSource* pSrc)
{
__hook(IEventSink::SomethingHappened, pSrc, Handler);
}
void Unadvise(IEventSource* pSrc)
{
__unhook(IEventSink::SomethingHappened, pSrc, Handler);
}
void __stdcall Handler(BSTR strDesc)
{
_putws(L"event handled");
_putws(strDesc);
}
};

void main()
{
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
{
CComPtr<IEventSource> pSrc;
pSrc.CoCreateInstance(L" Svr.MyObject.1");

CMyHandler handler;
handler.Advise(pSrc);
pSrc->DoSomething();
Sleep(5000); // kludge to allow the event to be handled
handler.Unadvise(pSrc);
}
}

From the April
2001 issue of MSDN
Magazine.
Get it at your local newsstand, or better yet, subscribe.

ⓒ 2001 Microsoft Corporation. All rights reserved. Terms
of Use. Privacy
Policy.