COM, ATL

분산 컴포넌트 생성 및 아파트먼트, 강력한 분산 객체 프로그래밍 모델 DCOM 개념 - 그림은 없음...ㅡㅡ;;;

디버그정 2008. 7. 27. 01:38

강력한 분산 객체 프로그래밍 모델 DCOM(1)
COM과 DCOM의 기본 개념

COM이나 DCOM이 프로그래밍에 직접적인 영향을 주는 것은 아니지만 DCOM을 이용하면 분산 객체의 강력한 기능을 다양하게 이용할 수 있다. 이번 연재를 통해 분산 객체 모델인 COM과 DCOM에서 사용되는 기본 이론에 대해 알아보고, 이러한 이론을 바탕으로 실제 구현을 살펴보도록 하겠다.

글 김성기 건국대학교 컴퓨터 공학과 IS 연구실
ditoman@chollian.net

2000/10 COM과 DCOM의 기본 개념
2000/11 DCOM을 사용한 채팅
2000/12 DCOM과 코바의 비교


사실 COM에 대해 정확하게 이해하지 않아도 프로그램 작성하는 데는 아무런 이상이 없다. 대표적으로 COM으로 구현된 DirectX 같은 경우에도 이것을 사용하기 위해 COM을 완벽하게 이해하고 있을 필요는 없다고 생각된다. 하지만 분산 객체라는 강력함을 쓸 수 있는 DCOM에 와서는 실용성이 증가하였다. 이러한 DCOM을 자유 자재로 사용하기 위해서는 COM에 대한 이해가 수반되어야 할 것이다.

1 COM이란
COM이란 Component Object Model의 약자로 단순한 모델만을 제공하는 이론을 제공하는 것이다. 이 모델은 OLE, ActiveX 등을 통해 실제로 사용되며 현재 제공되는 많은 API들이 이러한 COM을 사용한 모델로 대체될 예정이다. 일례를 들어보면 현재 DirectX에서 사용되는 많은 인터페이스들과 Active Directory들이 내부적으로는 COM을 사용한 인터페이스에 대한 호출로 구성되며, 앞으로 이런 것들은 점점 더 늘어날 것이라 생각된다.
이런 COM은 또한 3-tier 환경과 분산 컴퓨팅(Distributed Computing) 환경에서 코바와 마찬가지로 사용될 수 있다. 물론 코바와 비교했을 때 어떤 것이 더 낫다고 잘라서 말할 수는 없고 각기 장단점이 있으며, 어떤 분산 객체 기술을 사용할 것인가 하는 문제는 개발자에게 익숙한 언어나 서버의 플랫폼, 각 기술에서 제공해 주는 것들과 목적을 분석해서 판단을 내리는 것이 현명하다. COM이나 DCOM을 사용한 호출은 실제 오브젝트(Object)의 서버 컴포넌트를 호출하게 되는데 , 이를 그림으로 나타내면 그림 1과 같이 된다.





{{
}}
그림 1 : 다른 프로세스 내에서의 COM 컴포넌트

그림 1은 한 컴퓨터 내에서 서로 다른 프로세스간에 COM이 호출되는 순서를 보여주고 있다. 먼저 클라이언트가 어떤 서버 객체를 요구할 경우 클라이언트는 하나의 메소드를 호출하고 이를 COM run-time이 가로채서 Security Provider에게 해당 클라이언트가 이 서버의 오브젝트를 생성할 권한을 갖고 있는지 체크한다. 만약 적당한 권한이 있다면 이를 마치 하나의 프로세스 내에서 호출하는 것처럼 이것을 호출하게 된다.
물론 이 그림에서 보이는 COM run-time이 이것을 찾기 위해서는 서버 안의 오브젝트 데이터베이스를 참조하게 되는데, 이 데이터베이스는 실제적으로 레지스트리를 사용해서 구현되며 프로세스간의 통신으로는 Windows Message나 LRPC 등이 사용된다.
그림 1에서 COM run-time의 부분은 실제적으로 프록시(Proxy)와 스텁(Stub)이라고 불리는 코드에 의해 구현된다. 이를 나타내는 것이 그림 2이다.
{{
}}
그림 2 : 프록시와 스텁

클라이언트가 서버의 오브젝트를 호출할 때 프록시 코드 함수의 파라미터를 마샤링(Marshaling)하는 작업을 거치게 되며, 다시 이를 받은 서버 측에서는 스텁 코드를 사용하여 이를 언마샤링(Unmarshaling)하여 실제 객체를 호출한 후 다시 리턴 값을 마샤링한 후 이를 클라이언트가 받아서 언마샤링하는 작업을 거치게 된다.
이 때 마샤링, 언마샤링이란 서버의 메소드 호출로 가는 파라미터나 서버에게서 돌아오는 리턴 값을 네트워크를 통해서 전송될 수 있게 전환해 주는 것을 말하며 언마샤링이란 네트워크를 통해서 전송된 값을 다시 원래의 타입으로 바꿔주는 것을 말한다. 이러한 과정을 그림 3과 같이 나타낼 수 있다.


{{
}}
그림 3 : 다른 기계에서의 COM 컴포넌트

그림 3은 서로 다른 컴퓨터 상에 있는 프로세스간의 통신을 보여주고 있는데,  한 컴퓨터에 있을 경우와 다른 점은 하부에서 통신하기 위한 메커니즘으로 여러 가지 서로 다른 방법이 사용될 수 있다는 점이다. 여기에서 사용될 수 있는 프로토콜에는 다음의 레지스트리에 언급되어 있다.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Rpc\ClientProtocols

이것은 RPC 프로토콜로 사용될 수 있는 여러 가지 프로토콜들을 나열한 것으로 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Rpc에 DCOM Protocols 밑에는 DCOM Protocol로써 사용될 순서가 정해져 있다. 필자가 사용하는 윈도우 2000 시스템에서는 다음과 같은 값들이 나열되어 있었다.

ncacn_ip_tcp, ncacn_spx, ncacn_nb_nb, ncacn_nb_ipx

이 경우 tcp/ip, spx, netbeui, ipx 순서로 RPC 프로토콜로 사용할 순서를 정한다는 의미이며, dcomcnfg.exe라는 유틸리티를 사용하여 순서의 수정이 가능하다.

박스 기사------------------------------------------------------------
COM에서 사용되는 여러 아이디(ID)

COM에서 사용되고 있는 여러 종류의 아이디에 관해 살펴보도록 하자. GUID(Globally Unique Identifier)란 전역적으로 오직 하나의 128비트 숫자를 말하는데, 이것은 기계의 MAC Address와 현재의 시간을 사용해 만들어지는 것으로 전 세계에서 오직 하나의 고유한 숫자로 보장된다. CLSID란 어떤 클래스의 고유한 ID를 말한다(Class ID). IID란 어떤 인터페이스의 고유한 ID를 말한다(Interface ID).
여기서 CLSID와 IID의 차이란 클래스와 인터페이스의 차이를 말하는데 결국 하나의 클래스는 여러 개의 인터페이스를 구현할 수도 있다는 점에서 CLSID는 IID가 표시하는 인터페이스를 구현하는 클래스의 ID를 말한다고 볼 수 있다.
ProgID란 기억하기 어려운 CLSID를 매핑시켜 놓은 것으로 CLSIDFromProgID, ProgIDFromCLSID로 상호 변환이 가능하다. 이러한 모든 GUID들과 ProgID들은 모두 시스템 레지스트리 안에 들어가 있다. 참고로 GUID는 다음과 같이 정의되어 있다.

typedef struct _GUID
{
   unsigned long Data1;
   unsigned short Data2;
   unsigned short Data3;
   unsigned char Data4[8];
} GUID;

이러한 IID, CLSID등은 모두 위와 같은 GUID에 typedef로 정의되어 있다. 예를 들면 {AA000926-FFBE-11CF-8800-00A0C903B83C} 같은 형식으로 쓰이며 이를 정의하기 위해서는 C에서는 다음과 같이 하면 된다.

const GUID IID_IChattingServer2= {0x400C5DEF,0x18C8,0x11D3,{0xA3,0xE8,0x00,0xAA,0x00,0x6E,0x4A,0x82};

위와 같은 형식으로 하거나 DEFINE_GUID 같은 매크로를 사용하면 된다. 이러한 IID나 CLSID 등은 잘 알려진 인터페이스의 경우는 미리 공포되어 있으며 그 예는 다음과 같다. {00000000-0000-0000-C000-000000000046} 경우는 IUnknown의 IID이며, {00020400-0000-0000-C000-000000000046} 경우는 IDispatch의 IID이다. IUnknown과 IDispatch에 대한 자세한 설명은 잠시 뒤에 하겠다. 또한 이러한 GUID를 생성하기 위해서는 UuidCreate나 CoCreateGUID 함수를 호출해서 사용할 수도 있고 Guidgen.exe란 유틸리티를 사용해서도 생성이 가능하나 개발자가 직접 이러한 API나 유틸리티를 직접 사용할 일은 별로 없다.
------------------------------------------------------------------

1-1 각 서버의 타입
각 오브젝트의 서버가 될 수 있는 타입에는 세 가지 종류가 있는데 이는 Inproc 서버, 로컬 서버, 서비스 타입이 있다. 각각에 대해 살펴보면 다음과 같다.

① Inproc 서버
Inproc 서버란 DLL의 형태를 띠고 있는 서버를 말하는데 이 타입의 서버는 직접 클라이언트 안으로 로딩된다. 즉 클라이언트가 여러 개일 경우는 각 클라이언트마다 구별된 DLL이 메모리 속으로 로딩된다. 이러한 DLL들은 다음과 같은 방출 함수를 가져야 하는데, 이는 다음과 같다.

DllGetClassObject
DllCanUnloadNow
DllRegisterServer
DllUnregisterServer

DllGetClassObject는 클라이언트가 객체를 생성하기 위해 사용되는데 CoGetClassObject 안에서 실제로 해당 객체가 Inproc 서버 타입인 경우 불리게 되며 다시 CoGetClassObject 안에서는 해당 라이브러리가 메모리 속으로 로딩이 안된 경우는 CoLoadLibrary를 사용하여 메모리로 로드된다. DllCanUnloadNow는 CoFreeUnusedLibrary 안에서 호출되며 해당 객체를 해제해도 될지 결정하기 위해 불려진다. 만약 해제해도 된다고 DLL이 말한다면 해당 DLL을 해제시킨다. DllRegisterServer는 서버 인터페이스를 등록시키기 위해 Regsvr32.exe에 의해 불려진다. 여기서 보통 레지스트리를 등록한다거나 하는 작업이 이루어지게 된다. DllUnregisterServer 역시 서버 인터페이스를 등록해제 시키기 위해 Regsvr32.exe에 의해 불려진다.
필자는 이러한 Inproc 서버가 왜 필요한 것인지 이해가 안 될 떄가 많다. DLL일 경우 그냥 로딩해서 쓰면 되는데 굳이 COM을 통해 Inproc 서버로 만들 필요가 있는지 약간의 의문이 남는다. 하지만 다른 한편으로는 소켓을 통해서 함수 호출과 비슷한 작업을 일일이 코딩을 통해 만들려면 양이 엄청날 것이라는 생각이 든다. Inproc 서버 타입만이 MTS에서 지원되어 강력한 트랜잭션이나 오브젝트 풀링(Object Pooling)과 같은 기능을 제공해 줄 수 있다.
Inproc 서버는 꼭 클라이언트의 영역 안에서 로딩될 수 있는 것이 아니라 독립된 프로세스가 이를 로딩하여 마치 로컬 서버처럼 행동할 수도 있는데 이러한 프로세스를 Surrogate 프로세스라고 하며 이러한 Surrogate 프로세스의 예가 MTS이다. DCOM에서 Inproc 서버는 이러한 Surrogate 프로세스를 사용하여 서버에서 객체가 생성되는데, 이는 리모트(Remote) 환경에서 DLL이 클라이언트 속으로 들어갈 수 없기 때문이다.
이러한 Surrogate 프로세스는 대략 다음과 같은 과정을 거쳐 생성되는데, 이는 해당 CLSID 밑에 AppID란 값을 가진 Surrogate 프로세스를 등록한다. 이 AppID를 가진 응용 프로그램이 Surrogate 프로세스가 되며 이 AppID 밑에는 DllSurrogate란 값을 가지고 있어야만 한다. 만약 이 값이 특정한 프로세스의 경로를 지정하면 COM은 이 프로세스에게 생성하려고 하는 오브젝트의 CLSID 값을 넘겨주어 제어를 넘긴다. 따라서 독자들이 Surrogate 프로세스를 만들고자 한다면 해당 CLSID 값을 받아서 이를 처리하는 방식으로 제공하면 된다. 만약 아무런 값도 설정되어 있지 않다면 디폴트 Surrogate에게 제어를 넘기게 된다.

② 로컬 서버
로컬 서버란 Exe 형태로 구현되는 서버를 말한다. 이 경우는 소스 안에서 CoRegisterClassObject라는 API를 호출해야 하는데,  이는 윈도우 시스템이 유지하는 ROT(Running Object Table)에 해당 IClassFactory를 등록시킨다. 이 때 클라이언트가 CoGetClassObject를 호출할 때는 ROT 테이블에서 해당 ClassFactory를 찾고, 다시 주어진 IID로 해당 인터페이스를 활성화시키게 된다. 해당 ClassFactory를 메모리에서 해제시키기 위해서는 CoRevokeClassObject란 API를 사용하면 된다. ROT의 내용을 확인하기 위해서는 ROT Viewer라는 유틸리티를 사용하면 된다.

③ 서비스(Service)
이러한 타입의 객체들은 서비스 타입으로 일반 로컬 서버와 똑같은 방식으로 활성화 되지만 로컬 서버와 달리 시스템이 시작될 때 로딩되어 계속 메모리에 남아 있게 되며 서비스를 종료시킬 때 종료된다. 이 타입을 쓰는 이유는 한 가지 뿐인데, 이는 항상 메모리에 상주하여 보다 빠르게 반응할 수 있다는 장점이 있기 때문이다.

박스 기사------------------------------------------------------------
COM 객체를 만들기 위한 레지스트리의 사용 과정

COM이 실제로 호출되는 것은 레지스트리와 밀접한 관련이 있다. 만약 클라이언트가 CoCreateInstance 등을 사용해서 서버 객체를 만들려고 시도할 경우 COM은 다음과 같은 작업을 수행한다. CoCreateInstance는 내부적으로 다음과 같은 작업을 수행한다. CoGetClassObject를 호출해서 IClassFactory 인터페이스를 획득하고, 획득한 IClassFactory 인터페이스의 CreateInstance 인자로 해당 IID를 가지는 객체를 생성한다. CoGetClassObject는 내부에서 다음과 같은 작업을 수행한다. CoGetClassObject의 인자 중에 CLSID와 IID를 가지고 해당 오브젝트를 찾게 되는데, 이 때 CLSID와 IID에 관한 모든 정보는 레지스트리에 다음의 키 밑에 들어 있게 된다. 즉 HKEY_LOCAL_MACHINE\SOFTWARE\Classes\ 밑에 해당 CLSID가 들어 있으며 HKEY_CLASSES_ROOT\Interface\ 밑에는 IID의 값들이 들어 있다. 이 때 CoGetClassObject가 해당 CLSID를 가진 클래스를 찾는다.
로딩하는 타입에는 대략 세 가지가 있는데 이는 해당 CLSID의 키 밑에 보면 LocalServer32나 InprocServer32가 있는데, LocalServer32 타입의 경우는 EXE 타입으로 독립된 프로세스를 서버상에 띄우게 되며, InprocServer32 같은 경우는 Surrogate 프로세스가 해당 DLL을 로딩하여 띄우게 되거나 해당 클라이언트의 메모리 속으로 직접 로딩된다. 참고로 InprocHandler32는 사용자 스스로 마샤링을 구현하기 위해 사용된다. 또한 서버의 타입이 서비스 타입인 경우는 해당 CLSID 밑에 AppID의 값이 나와 있는데, 이 값을 가지고 HKEY_CLASSES_ROOT\AppID\ 키 값을 뒤져서 해당 Application의 위치를 찾아낸 후 해당 서비스를 기동하게 된다. 이 과정을 그림으로 나타내면 다음과 같다.

{{
}}
그림 4 : CoCreateInstance의 호출 순서

물론 그림 4에서 정확히 말하면 서버 오브젝트 포인터(Server Object Pointer)가 넘어오는 것은 Inproc 서버로 한정이 되며, 기타 다른 타입의 서버인 경우에는 오브젝트 포인터는 클라이언트에서 서버 객체를 표현하는 프록시 포인터가 된다. 이 포인터를 사용하여 이 때부터 서버 객체의 메소드를 사용할 수 있게 된다.

1-2 스레딩 모델
먼저 이 부분을 설명하기 위해서는 아파트먼트(Apartment)라는 것에 대해 설명이 필요하다. 아파트먼트는 하나의 프로세스 내에서 여러 개의 스레드를 가지고 각각의 스레드 동기화 문제를 해결하기 위해 나온 것으로 각 오브젝트를 가지고 있는 하나의 방이라는 개념으로 이해하면 된다. 각 아파트먼트당 하나의 스레드가 존재하며 오브젝트는 적당한 아파트먼트 안으로 들어가게 된다.
이러한 스레딩 모델(Threading Model)은 레지스트리에서 CLSID의 스레딩 모델의 값을 보면 된다. 가능한 경우는 스레딩 모델이 아예 없거나 아파트먼트, 프리(Free), 보스(Both)가 되는 것이다. 아파트먼트는 STA 모델을 가리키며 프리는 MTA 모델을, 보스는 STA, MTA 둘 다 지원한다는 것을 말한다. 이 값이 아예 없다는 것은 main STA에서 동작한다는 것을 말하고 이 main STA는 프로세스가 초기에 주 스레드에서 호출한 CoInitializeEx 값에 의해 결정된다.

① STA(Single Threaded Apartment)
STA 내에서 서버는 각각의 아파트먼트 내에 각 오브젝트가 존재하며 하나의 프로세스 내에서 각 아파트먼트 내에 각 오브젝트가 들어가 있게 된다. 이 경우는 CoInitializeEx(NULL,COINIT_APARTMENTTHREADED)를 각각의 스레드당 호출하여 초기화 된다. 이러한 각각의 오브젝트 호출은 메시지에 의해 동기화 된다. 즉 이러한 메시지를 통해 동기화 된다는 것은 메시지 루프를 가져야 한다는 얘기이다.
하나의 아파트먼트 내에서는 프록시의 개입 없이 직접적으로 호출할 수 있으나 다른 아파트먼트 내에서는 이렇게 될 수는 없다. 즉 하나의 메소드 호출은 클래스 네임이 OleMainThreadWndClass를 가지는 윈도우 클래스가 등록한 WndProc로 가서 적당한 Interface의 메소드를 호출하게 된다. 이러한 모델 내에서의 호출은 그림 5와 같다.

{{
}}
그림 5 : STA에서의 메시지 큐
 
그림 5에서 보는 것처럼 서버로의 모든 호출은 큐(Queue) 속에서 들어가며 GetMessage,DispatchMessage에 의해 해당 오브젝트로 전달된다.

② .MTA(Multi Threaded Apartment)
MTA 내에서 하나의 아파트먼트 내에 여러 개의 스레드와 오브젝트가 들어가게 되며 따라서 오직 한 번만 CoInitializeEx(NULL, COINIT_MULTITHREADED)를 호출하면 된다. 이 경우에는 여러 개의 스레드가 동시에 오브젝트의 메소드로 접근할 수 있기 때문에 Reentrant하도록 소스가 작성되거나 동기화 문제를 코드 안에서 해결해야 한다. 하지만 서버 안에서의 스레드간 인터페이스 전달에 프록시가 개입할 필요가 없다.
또한 STA로 작성하는 것보다 빠른 속도를 보이는 멀티 스레드의 장점을 충분히 활용할 수
가 있으며 하나의 프로세스 내에 오직 하나의 MTA만 만들어질 수 있다.

③ 혼합 모델
이 모델은 프로세스가 한 개의 MTA와 한 개 이상의 STA를 가진다는 것을 의미한다.

④ STA와 MTA와 혼합 모델에서의 선택
어떤 오브젝트의 모델을 선택하는 것은 각기 장단점이 존재하기 때문에 상황에 맞게 선택해야 하는데, STA에서는 프로그래머가 스레드간의 동기화를 신경쓸 필요가 없다. 그러나 여기서 다른 아파트먼트로 인터페이스를 전달하기 위해서는 프록시의 개입을 필요로 하므로 느려질 수 있으며, 또한 객체로의 메소드 호출이 메시지 루프(Loop)를 통해 한번에 하나씩 만 처리되기 때문에 멀티 스레드의 장점을 충분히 활용할 수가 없게 된다.
그러나 MTA를 선택한 경우 서버 객체의 구현자는 여러 개의 스레드가 동시에 메소드 호출을 수행할 수가 있으므로 코드는 Reentrant하게 작성되거나 Mutex, Critical Section, Event를 통해 동기화 코드를 작성해야 한다. 따라서 작성이 어려워지게 되지만 이 경우라고 해도 수많은 사용자가 사용할 수 있는 오브젝트라면 한번 고려해볼 만하다.

박스 기사------------------------------------------------------------
Reentrant란?

사전적으로는 "재진입이 가능하다"라는 뜻인데, 일반적으로 하나의 코드에 여러 개의 스레드가 동시에 접근할 수도 있기 때문에 각  스레드간에 영향을 받지 않는 방식으로 코드를 작성하는 것을 말한다. 이러한 소스를 작성하기 위해서는 Thread Local Storage(TLS)를 사용하고, 전역 변수나 멤버 변수를 사용하지 않고 C처럼 호출하는 함수에 인자를 넘겨주는 방법으로 어느 정도 가능하다.
이렇게 Reentrant하게 작성되어야 하는 것으로는 ISAPI를 사용해서 IIS가 여러 개의 스레드로 동시에 하나의 코드로 접근하는 경우를 들 수가 있다. 이러한 경우에 필자는 멤버 변수를 사용하지 않고 모든 코드를 마치 C에서처럼 작성한다. 기존의 C++에 익숙한 개발자라면 이런 식으로 코드를 작성하는 것이 짜증나는 일일 수도 있을 것이다.
------------------------------------------------------------------

1-3 인터페이스
인터페이스란 이러한 COM 모델 속에서 외부로 노출되는 메소드와 프로퍼티들의 목록을 외부로 노출하기 위한 일종의 접속 지점(Connection Point)을 말하는데, 이러한 인터페이스를 기술하기 위해 IDL이 사용된다. 즉 이러한 인터페이스들이 외부로 노출되기 위해서는 외부로 노출될 메소드들의 목록이나 프로퍼티들의 목록을 만들기 위해 이것을 기술하는 언어로 IDL을 쓰며, 이렇게 만들어진 메소드나 프로퍼티들을 실제적으로 구현하기 위한 언어로는 어느 언어나 사용할 수 있다.
이렇게 만든 IDL은 MIDL 컴파일러에 의해 프록시, 스텁 코드를 생성하는데 사용되며, 생성된 프록시,  스텁 코드는 직접 소스와 함께 컴파일 되어 실행 파일이나 DLL 파일 등으로 바뀌어서 사용된다. 이 때 MIDL에 의해 컴파일된 IDL은 name_p.c, name_i.c, name.h, dlldata.c 같은 파일들을 생성하는데(Visual Studio에서 ATL 위저드를 사용해서 생성한 경우) 각각에 대해 간단히 설명을 하면 다음과 같다.

* name_p.c : 실제 프록시와 스텁 코드의 구현이 들어가게 된다.
* name_i.c : 인터페이스 정의 파일이다.
* name.h : 인터페이스의 C/C++ 정의를 갖고 있는 헤더로 두 가지의 버전이 있는 것을 볼 수가 있다. 하나는 C 버전의 struct type으로 C++의 가상 함수 테이블(virtual function table)과 각종 멤버들이 정의되어 있음을 볼 수 있다. 또 다른 버전은 C++용으로 C++의 추상적 가상 함수(abstract virtual function)로 선언되어 있음을 볼 수가 있다. 여기에서 name은 Idl 파일의 파일명을 가리킨다.

① IDL
IDL은 Interface Description Language의 약자인데 COM이나 DCOM에서 사용되는 IDL은 MS의 MIDL 컴파일러로 컴파일 되며 이것은 코바 IDL과 다르고 DCE IDL에서 파생되어 어느 정도 유사점을 가지고 있다. 이러한 IDL파일들은 다음과 같은 기본 구조를 가지며 또한 IDL 파일의 구조는 전체적으로 대부분 다음과 같은 구조를 가진다.

[ /*attributes*/ ]
interface,dispinterface,module,library,coclass name
{ /*body*/  }

위에서 [] 속에 들어갈 수 있는 애트리뷰트(Attribute)는 다음과 같은 것들이 있는데, 일반적으로 많이 사용되는 것들만 적어 보았다.

표 1 : 애트리뷰트의 사용
----------------------------------------------------------------------
Attribute Usage
-----------------------------------------------------------------------
async_uuid MIDL 컴파일러에게 COM 인터페이스의 Synchronous된 버전과                            Asynchronous된 버전을 정의한다고 알려준다.
uuid  특정한 인터페이스를 다른 인터페이스들과 구별하기 위한 128비트 숫자가                  들어가는데 실제 값은 GUID,CLSID,IID를 표현한다.
local  MIDL 컴파일러에게 헤더 파일만 생성하도록 지시한다. 인터페이스는 uuid                  나 local attribute를 가져야 한다.
object  COM 인터페이스로써 인터페이스를 알리고 MIDL 컴파일러에게 RPC                     클라이언트와 서버측의 RPC 스텁 대신 DCOM을 위한 프록시와 스텁을                   생성하도록 알린다.
version  인터페이스의 다중 버전이 존재하는 경우 버전을 표시하기 위해 쓰인다.                   오브젝트와는 함께 쓰일 수 없는데 이유는 하나의 인터페이스를 가지는 여                  러 개의 오브젝트가 사용될 소지가 있기 때문이다.
pointer_default 파라미터로 보낼 포인터의 디폴트 타입을 기술한다. unique,ref,ptr와 같은                   것들이 될 수 있는데 이들의 차이점에 대해서는 MSDN이나 Microsoft사                  이트를 참조하기 바란다.
dual  IDispatch로부터 파생되며 VTBL을 통한 호출이나 IDispatch를 통한 호출                 모두를 지원하는데 각각의 차이점에 대해서는 잠시 뒤에 설명하도록 하겠다
endpoint  RPC가 기다리고 있을 포트 번호나 호출 순서 등을 기술한다.
oleautomation 인터페이스가 automation과 호환된다는 사실을 지시한다.
--------------------------------------------------------------------

또한 스테이트먼트(statement)로 사용될 수 있는 interface,dispinterface,module,library,coclass들에 대한 간단한 설명은 다음과 같다.

* interface : 해당 인터페이스가 IUnknown에서 파생 받았음을 나타낸다.
* dispinterface : 해당 인터페이스가 IDispatch에서 파생 받았음을 나타낸다.
* module : 방출된 함수의 DLL의 entrypoint의 집합을 정의한다.
* library : 타입 라이브러리를 생성하기 위해 사용하는 모든 정보를 포함하고 있다.
* coclass : 지원되는 인터페이스들의 리스트를 제공한다.

다음은 MIDL에서 사용되는 기본 타입들을 나타낸다.

표 2 : MIDL에서 사용되는 기본 타입들
-----------------------------------------------------------------
Data type Description    Default sign
----------------------------------------------------------------
boolean  8 bits     Unsigned
byte  8 bits     -
char  8 bits     Unsigned
double  64-bit floating point number  -
float  32-bit floating point number  -
hyper  64-bit integer    Signed
int  32-bit integer.플랫폼에 의존적  Signed
long  32-bit integer    Signed
short  16-bt integer    Signed
small  8-bit integer    Signed
void *  32-bit pointer for context handles only -
wchar_t  16-bit Wide character type  Unsigned
----------------------------------------------------------------

oleautomation으로 지정된 인터페이스들은 위의 기본 타입들과 약간 다른 타입을 갖는데 이는 다음과 같다.




표 3 : oleautomation으로 지정된 인터페이스의 타입
--------------------------------------------------------
Type   Description
-------------------------------------------------------
boolean   8 bits
unsigned char  8-bit unsigned
Double   64-bit floating-point number
Float   32-bit floating-point number
Int   32-bit integer, 플랫폼에 의존적
Long   32-bit signed integer
Short   16-bit signed integer
BSTR   고정 길이 문자열
CY   8-byte fixed-point number
DATE   1899년 12월 30일 이후의 64비트 값
SCODE, HRESULT 내장된 에러 타입
Enum   Signed integer, 사이즈가 플랫폼에 따라 다르다. 리모트 수행에서                          16비트의 unsigned 개체로 취급된다. 이 값은 typedef enum에 의                          해 정의된 값을 파라미터로 주고받기 위해서 정의된 Type을 말한                          다.
IDispatch *  IDispatch interface 의 포인터
IUnknown *  IDispatch로부터 파생받지 않은 모든 interface의 포인터
--------------------------------------------------------------------

또한 각 기본 타입은 C와 유사한 struct,union,,enum 등을 사용하여 기본 타입을 확장할 수 있다. 다음은 기본 타입 앞에 [] 안에 들어가서 각 타입을 설명한다. 이를 애트리뷰트라고 하는데, 이는 다음과 같은 것들이 있는데 여기서는 지면 관계상 많이 사용되는 것들만을 다루었다. Array and Sized-Pointer Attributes는 배열이나 다중 레벨의 포인터를 기술하는 데
사용되는 Attribute를 말한다.

----------------------------------------------
Attribute Usage
-----------------------------------------------
size_is  고정된 포인터나 배열을 위해서 할당된 메모리의 양을 보내기 위한 값
max_is  배열의 최대값을 보낸다.
length_is 전송될 배열의 길이, 전송될 배열의 길이가 일부인 경우 이 값을 변경함으                  로써 전송량을 최소화 시킬 수가 있게 된다.
first_is  전송될 최초의 배열 요소의 인덱스
last_is  전송될 마지막의 배열 요소의 인덱스
string  char, wchar_t, byte의 배열임을 지시하고 배열을 문자열로 취급하기 위해   사용
range  값들이 수행 중에 허락되는 범위를 지시
---------------------------------------------------------

Directional Attributes는 파라미터가 건네질 직접적인 방법을 묘사하는 애트리뷰트이다.

-------------------------------------------------
Attribute Usage
-------------------------------------------------
in  클라이언트가 서버로 전달하는 값임을 나타냄
out  서버가 클라이언트로 전달하는 값임을 나타냄
-------------------------------------------------

② 기본 인터페이스들
다음으로 기본적인 인터페이스들에 관해 살펴보자.

* IUnknown : 모든 인터페이스의 기본 인터페이스로 인터페이스 객체의 수명을 관리한다. 여기에 해당하는 메소드들은 다음과 같다.  HRESULT QueryInterface(REFIID iid,void ** ppvObject) 메소드는 하나의 인터페이스에서 인터페이스가 지원하는 또 다른 인터페이스를 요청한다. ULONG AddRef(void) 메소드는 참조 변수를 증가시킨다. 하나의 인터페이스에 대한 포인터를 구했을 때 반드시 이 함수를 호출해서 해당 인터페이스에 대한 참조가 증가했음을 인터페이스에게 알려주어야 한다. ULONG Release(void)는 참조 변수를 감소시킨다. 클라이언트가 해당 인터페이스를 다 사용했음을 나타낸다. 해당 참조 변수가 0이 되었을 때 해당 객체는 메모리에서 지워져야 한다.

* IDispatch : VB나 스크립트 같은 vtable을 지원하지 않는 언어에서 메소드 호출을 지원하기 위해 사용되는 인터페이스로 이 인터페이스 안의 메소드는 다음과 같다. HRESULT GetTypeInfoCount( unsigned int FAR* pctInfo) 메소드는 오브젝트가 타입 정보를 제공하는지 확인한다. HRESULT GetTypeInfo( unsigned int iInfo,LCID lcid, ITypeInfo FAR* FAR*  ppTInfo); 메소드는 ItypeInfo 인터페이스를 획득한다. HRESULT GetIDsOfNames( REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int oNames, LCID lcid, DISPID FAR* rgDispId); 메소드는 메소드의 아이디 값을 구한다. 이것은 해당 메소드를 실제 호출하기 위해서 필요하다. HRESULT Invoke( DISPID dispIdMember, REFIID riid, LCID lcid,WORD wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult,EXCEPINFO FAR* pExcepInfo, unsigned int FAR*  puArgErr); 메소드는 주어진 아이디를 가지고 메소드를 호출한다.

* IClassFactory : 주어진 IID로 인터페이스를 구현한다. 오브젝트를 실제로 만드는 인터페이스로 메them는 다음과 같다. HRESULT CreateInstance( IUnknown *pUnkOuter,REFIID riid, void **ppvObject); 메소드는 IID를 가진 실제 인터페이스를 생성한다. HRESULT LockServer(BOOL flock ); 메소드는 서버에 락(Lock)을 건다. 즉 메모리에 고정시킨다. 더욱더 빠르게 서버에 엑세스를 하기 위해서 이것을 호출한다.

위에서 살펴본 3개의 인터페이스는 모든 인터페이스에 비하면 빙산의 일각에 불과하다는 것을 말해둔다. OLE,ActiveX에서 사용되는 인터페이스들은 엄청나게 많으며 위에서 보여진 인터페이스들은 정말 기본이 되는 것이므로 잘 숙지하기 바란다.
 
③ 인터페이스의 구현 방법
하나의 인터페이스가 또 다른 인터페이스를 이용하는 경우에 사용하는 방법으로 이러한 코드 재사용을 사용해서 구현하는 방법에는 대략 다음과 같은 두 가지의 방법이 있다. Containment는 하나의 인터페이스가 또 다른 인터페이스의 구현을 포함하고 있다. 즉 어떤 하나의 인터페이스가 다른 인터페이스를 외부로 노출시키지 않는다. QueryInterface를 사용해서 내부의 인터페이스를 얻는 것이 불가능하다. 반면 Aggregation은 어떤 하나의 인터페이스가 다른 인터페이스를 외부로 노출시킨다. QueryInterface를 사용해서 내부의 인터페이스를 얻는 것이 가능하다.

2 COM과 DCOM
지금까지 COM에 관한 일반적인 내용들을 살펴보았다. COM과 DCOM에는 본질적으로 차이가 많으나 클라이언트의 입장에서 본다면 차이는 크지 않다. 즉 메소드를 호출하고 받는 것의 차이는 내부에서 RPC를 통해서 일어난다는 점과 클라이언트가 서버의 오브젝트를 만들기 위해 서버의 주소를 알아야 한다는 점 외에 메소드 호출 방식에는 차이가 없다.
간단하게 COM과 DCOM의 차이점을 살펴보면 다음과 같다. 즉 COM에서는 객체를 CoInitialize를 사용하여 초기화하나 DCOM에서는 CoInitializeEx를 사용하여 초기화 한다. 또한 객체를 실제로 만들 때 CoCreateInstanceEx에서 CLSCTX_REMOTE_SERVER 서버가 세팅되어 있어야 한다는 점이 다르다. CoCreateInstanceEx의 파라미터 중에서 COSERVERINFO에서 서버의 기계 이름을 제공해 주어야 한다는 점을 제외하면 나머지 COM과 동일하다. 이것은 기본적으로 DCOM이 로케이션 트랜스페어런시(Location Transparency)를 제공해 주기 때문으로 내부적으로는 DCOM은 RPC를 사용한다. 이러한 RPC는 다시 클라이언트와 서버의 세팅에 따라 여러 가지의 프로토콜들을 사용할 수 있다.
지금까지 간단하게 COM에서 사용되는 용어들과 기본 개념들에 대해서 알아보았다. 다음 호에서는 DCOM을 사용한 채팅을 직접 구현해 보도록 하겠다. 참고로 COM 컴포넌트를 구현하기 위해 위에서 언급한 모든 것을 다 알 필요는 없지만 작업상 일어날 수 있는 여러 가지 문제들을 해결하기 위해 위에서 정리한 내용 정도는 알 필요가 있다고 생각한다. COM에 대해 더 자세히 알고 싶은 독자는 다음에 나오는 책들과 필자의 홈페이지(http://www.ditoman.pe.kr)를 참조하기 바란다.


참고 서적
1. 프로페셔널 DCOM 프로그래밍, Dr.Richard Grimes, 백승관 역, 1997, 대림
2. Inside COM+ Base Services, Guy Eddon, Microsoft Press
3. Inside ATL, George Shepherd,Brad King, Microsoft Express
4. Julian Templeman's Beginning MFC COM Programming, Julian Templeman, Wrox Press
5. Visual Studio 97로 배우는 ActiveX 프로그래밍 가이드
6. MSDN, Microsoft 웹 사이트

정리 김상연 기자 sykim@pserang.co.kr

[출처] 14. DCOM|작성자 다다닥