COM, ATL

COM 객체 생성과정 설명 및 도식화(깔끔한 설명)

디버그정 2008. 7. 27. 00:47

COM객체, 그 탄생의 비밀


이재규 (영산정보통신)

대부분의 경우 프로그래머는 COM객체를 사용하는 클라이언트의 입장에서 코딩한다. COM객체를 사용하기 위해서는 먼저 COM객체를 생성해야 하는데, 이는 CoCreateInstance라는 함수를 이용해서 한줄로 끝낼 수 있다. 그러나 CoCreateInstance를 통해 COM객체가 생성되는 과정을 깊이 살펴본다면 그리 간단치만은 않다. 이 복잡한 과정들을 풀어헤쳐 보고자하는 것이 이글이 주제이다.

COM객체 생성방법

COM객체는 COM규약을 준수하는 컴포넌트를 의미한다. COM은 많은 것을 규정하고 있지만, 그 중에서 중요한 것 중의 하나가 COM객체를 생성하는 메커니즘이다.

따라서 COM객체들은 그 객체가 단순하든, 복잡하든 동일한 방법으로 생성할 수 있다. 뿐만 아니라 COM객체가 DLL의 형태이든, EXE의 형태이든, 심지어 다른 컴퓨터에 있는 객체일지라도 동일한 방법으로 생성한다. 그 동일한 방법은 CoCreateInstance를 사용하는 것이다. CoCreateInstance 함수에 대해서 먼저 살펴보기로 하자.

STDAPI CoCreateInstance(
	REFCLSID rclsid,	// 생성하고자 하는 객체의 CLSID
	LPUNKNOWN pUnkOuter,	// Aggregate하는 객체의 IUnknown포인터
	DWORD dwClsContext,	// 생성 컨텍스트
	REFIID riid,	// 얻고자 하는 객체의 인터페이스
	LPVOID *ppv	// 객체 인터페이스의 포인터를 받을 곳
);

모든 COM클래스는 유일한 CLSID를 갖는다. 따라서 생성할 COM객체를 지정하는데 CLSID를 사용하는 것은 당연한 일이다. 또 다른 중요한 인자인 dwClsContext는 어떤 형태의 객체 서버를 활성화할 것인지를 결정한다. 예를 들어 CLSCTX_INPROC_SERVER를 지정하면 인프로세스 COM객체를 지정하게 된다.

COM객체는 C++객체와는 달리 포인터를 직접 얻어와 조작할 수 없다. COM객체는 오직 인터페이스를 통해서만 조작할 수 있기 때문이다. 그래서 CoCreateInstance를 이용하여 객체를 얻어 ppv인자로 포인터를 얻는 것은 인터페이스의 포인터이다. riid인자에 바로 어떤 인터페이스를 원하는지를 지정하면 된다.

예를 들어 Microsoft의 웹도큐먼트 객체를 사용하고 싶다면 다음과 같이 코딩하면 된다.

HRESULT hr;
LPUNKNOWN pUnknown = NULL;

hr = CoCreateInstance(CLSID_HTMLDocument, 
	NULL,
	CLSCTX_INPROC_SERVER,
	IID_IUnknown,
	(LPVOID*)&pUnknown);
<리스트 1> 인프로세스 COM객체를 생성하는 예

클래스 팩토리

클래스 팩토리는 자신이 맡고 있는 COM객체의 인스턴스를 만들어주는 간단한 COM객체이다. 클래스 팩토리는 보통 IClassFactory나 IClassFactory2를 구현한다. IClassFactory는 COM객체를 생성하는 함수와 서버의 생존을 관리하는 함수를 가지고 있으며, IClassFactory2는 라이선스에 대한 기능이 추가된 것이다. 중요한 점은 하나의 클래스 팩토리는 단 하나의 COM클래스에만 대응한다는 것이다.

클래스 팩토리의 이런 성격 때문에 클래스 팩토리는 시스템 레지스트리에 반영구적으로 등록되지 않는다. 대신 다른 표준적인 방법으로 클래스 팩토리를 얻어와야 한다. COM 라이브러리에는 어떤 COM클래스의 클래스 팩토리를 얻는 API를 제공한다. 이것이 바로 CoGetClassObject 함수이다. CoGetClassObject함수의 프로토타입은 다음과 같다.

STDAPI CoGetClassObject(
	REFCLSID rclsid,	// 생성하고자 하는 클래스ID
	DWORD dwClsContext,	// COM서버의 형태
	COSERVERINFO *pServerInfo, // 원격서버인 경우, 서버에 대한 정보
	REFIID riid,	// 얻고자 하는 인터페이스
	LPVOID *ppv	// 리턴되는 인터페이스 포인터
);	

CoGetClassObject 함수를 이용하여 클래스 팩토리를 얻으면 IClassFactory::CreateInstance 함수를 이용하여 원하는 COM클래스의 인스턴스를 만들 수 있다.

따라서 앞에서 설명한 CoCreateInstance 함수는 CoGetClassObject 함수를 이용하여 클래스 팩토리를 얻고, 이 클래스 팩토리로 COM객체를 생성한다. 이 과정을 <리스트 2>에 나타내 보았다.

STDAPI CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, 
			DWORD dwContext, REFIID iid, void **ppv) 
{ 
	HRESULT hr; 
	IClassFactory *pCF; 
	*ppv = NULL; 

	hr = CoGetClassObject(rclsid, dwContext, NULL, 
		IID_IClassFactory, (LPVOID*)&pCF); 

	if (FAILED(hr)) 
		return hr; 

	hr = pCF->CreateInstance(pUnkOuter, iid, ppv); 
	pCF->Release(); 

	return hr; 
}
<리스트 2> CoCreateInstance함수의 구현

일반적으로 CoCreateInstance를 쓰는 것만으로도 COM객체를 생성하는데 별 문제가 없지만, CoCreateIntance의 구현처럼 풀어서 CoGetClassObject를 사용해야 하는 경우도 있다.

첫째로는 라이선스를 위해서 IClassFactory2만을 구현한 객체를 생성하기 위한 경우가 있을 것이고, 둘째로는 COM클래스의 인스턴스를 한꺼번에 많이 만들 경우에는 효율을 위해서 클래스 팩토리의 생성과 제거를 반복하는 대신, 클래스팩토리를 직접 사용하여 IClassFactory::CreateInstance를 여러 번 호출하는 경우가 있을 것이다.

어쨌든 중요한 점은 CoGetClassObject 함수의 내부에서부터 인프로세스 서버냐, 로컬 서버냐, 원격서버냐에 따라 내부구현이 달라진다는 점이다. 즉 CoGetClassObject를 사용하는 단계까지는 클라이언트가 서버의 위치에 대해서 특별한 코딩을 할 필요는 없다는 점이다. (단 서버의 형태를 지정하는 인자는 변화시켜 주어야 할 것이다.)

<박스기사 1> 클래스 객체 (Class Object)

객체지향언어에서의 클래스와 객체간의 관계는 잘 알고 있을 것이다. 클래스는 데이터와 멤버함수를 가지는 논리적 구성단위를 의미한다. 객체는 클래스가 실제로 사용되는 인스턴스(instance)이다. 비유하자면 클래스는 붕어빵 기계이고, 객체는 붕어빵 기계로 만든 붕어빵이다.

클래스 팩토리는 객체의 인스턴스를 만드는 역할을 하기 때문에 객체지향언어의 클래스에 해당한다. COM에서는 객체지향언어에서의 클래스 역할을 하는 객체를 클래스 객체(Class Object)라고 한다.

CoGetClassObject는 앞의 설명에서 클래스 팩토리를 얻는 것이라고 했다. 그렇다면 CoGetClassFactory라고 이름붙여야 되지 않았을까? 여기서 유추할 수 있는 것은 엄밀한 의미에서 CoGetClassObject는 클래스의 인스턴스를 만드는 클래스 객체를 얻는 함수라는 것이다. 그렇다면 클래스 팩토리는 IClassFactory나 IClassFactory2를 구현한 클래스 객체의 일종이라고 할 수 있다.

따라서 CoGetClassObject가 반드시 IClassFactory(2)를 구현한 객체를 리턴하는 것은 아니다. CoGetClassObject가 리턴하는 것은 컴포넌트를 제작하는 측에서 만든 특수한 클래스 객체일 수도 있다는 의미이다. 특수한 클래스 객체를 사용하는 예는 <참고서적 5>의 7장을 참고하기 바란다.

그러나 일반적으로 클래스 객체로서 클래스 팩토리를 사용하는 것은 이것이 표준이기 때문이다. 만일 특수한 클래스 객체를 사용한다면 많은 COM클라이언트 개발환경에서 제대로 동작하지 않을 확률이 많다.

인프로세스 COM객체의 생성과정

COM서버를 위치에 따라 분류하면 인프로세스 서버, 로컬 서버, 리모트 서버로 나눌 수 있다. 이 중에서 가장 다루기 쉽고, 이해하기 쉬우며, 가장 많이 사용하는 COM서버의 형태는 인프로세스 서버이다.

인프로세스 서버는 DLL로 구성된다. 따라서 인프로세스 서버는 클라이언트와 같은 주소 공간에 로드된다. 바로 이점이 인프로세스 서버의 사용을 쉽게 만든다.

앞에서 언급했듯이 CoGetClassObject는 COM서버의 위치에 따라 다르게 동작한다. 그렇다면 인프로세스 서버인 경우는 어떤 방법으로 클래스팩토리를 얻어내는 것인지 차근차근 살펴보자.

모든 COM서버는 자신을 실행시킬 수 있게 그 위치를 레지스트리에 등록하게 되어있다. 레지스트리의 “\HKCR\CLSID\{<clsid>}\InprocServer32”라는 키에 있는 서버의 파일경로가 바로 그 정보이다.

CoGetClassObject는 레지스트리에서 인자로 주어진 CLSID로 서버의 위치를 찾아 CoLoadLibrary라는 함수를 이용하여 DLL을 로드한다.

<그림 1> 인프로세스 서버의 레지스트리 구성

DLL은 클라이언트와 같은 주소공간에 위치하기 때문에, DLL내부의 함수를 직접 호출할 수 있다. 인프로세스 서버는 COM규약에 의해 DllGetClassObject라는 함수를 노출(export)하고 있다. CoGetClassObject는 로드한 DLL에서 DllGetClassObject라는 함수를 호출한다.

DllGetClassObject는 인프로세스 서버의 클래스팩토리를 생성하여 그것의 인터페이스 포인터 (보통 IClassFactory이다)를 리턴한다. 이 인터페이스 포인터가 CoGetClassObject가 리턴하는 클래스 팩토리의 포인터이다. 이것으로 CoGetClassObject의 임무는 끝이 난다.

다음으로 CoCreateInstance함수는 IClassFactory::CreateInstance를 통해 객체의 인터페이스 포인터를 얻고, 이 포인터를 클라이언트에 리턴한다. 클라이언트는 이 인터페이스 포인터를 객체를 사용한 뒤에 Release하면 된다.

이러한 과정을 <그림 2>에 나타내었다. 인프로세스 COM객체의 경우 이렇게 단순하다. 이 단순함은 DLL의 수동적인 특성과 DllGetClassObject라는 노출된 함수에서 기인한다.

<그림 2> 인프로세스 객체의 생성과정

로컬 COM객체의 생성과정

로컬 COM객체의 생성과정은 인프로세스 객체의 그것에 비하면 복잡한 편이다. 그럴 수 밖에 없는 것이 로컬 COM객체는 실행파일(EXE)형태로 존재하여서 클라이언트와 다른 주소공간에서 작동하기 때문이다.

이로 인해 인프로세스 서버의 DllGetClassObject와 같이 로컬서버에 구현된 특정함수를 직접 호출할 수 없다. 또한 클라이언트와 로컬서버간의 데이터를 전달을 위해서 마샬링(marshalling)이라는 골치아픈 것을 고려해야 한다. 그러나 이런 복잡함은 COM서버를 제작하는 측에만 해당되는 이야기이고, 클라이언트의 입장에서는 인프로세스 서버를 사용하는 것과 동일하다. 이것이 바로 COM이 자랑하는 위치투명성(Location Transparency)이다. 이 위치투명성의 지원은 복잡하게 구현된 CoGetClassObject 함수에 의해서 이루어진다.

그렇다면 로컬 서버인 경우 CoGetClassObject는 어떻게 동작하는 것일까? 이 과정을 차근차근 살펴보자. 먼저 주어진 CLSID에 해당하는 COM객체 서버가 어디에 있는지 레지스트리에서 읽어야 할 것이다. 인프로세스 서버는 InProcServer32 키에서 서버의 경로를 얻었지만, 로컬 서버는 LocalServer32 키에서 서버의 경로를 얻는다. (<그림 3>)

<그림 3> 로컬서버의 레지스트리 설정

CoGetClassObject함수의 목적은 클래스팩토리를 얻는 것이었다. 인프로세스 서버는 DllGetClassObject라는 함수를 통해서 클래스팩토리를 제공했다. 그러나 로컬 서버에서는 이와 같은 메커니즘을 사용할 수 없다. 왜냐하면 EXE파일은 특정 함수를 노출하는 방법이 없기 때문이다.

이 문제를 해결하기 위해서 COM은 EXE형태 서버의 클래스 팩토리를 전역적으로 관리하는 “클래스 테이블(Class Table)”이라는 구조를 사용한다. 그리고 EXE형태의 서버는 자신이 실행될 때 빠른 시간내에 클래스 팩토리를 생성한 뒤 이를 클래스 테이블에 등록할 의무를 가지고 있다.

클래스 테이블에 클래스 팩토리를 등록하기 위해 COM은 CoRegisterClassObject라는 함수를 제공한다.

STDAPI CoRegisterClassObject(
	REFCLSID rclsid, // 클래스 팩토리가 생성할 객체의 CLSID
	IUnknown * pUnk, // 클래스 팩토리의 IUnknown포인터
	DWORD dwClsContext, // COM서버의 형태
	DWORD flags, // COM서버의 연결을 어떻게 할 것인가?
	LPDWORD lpdwRegister // 클래스 테이블에서 제거하기 위한 쿠키 
);

CoRegisterClassObject 함수의 인자 flags는 상당히 흥미로운 내용이다. 이 인자는 RGSCLS_ 류의 열거상수들을 사용할 수 있는데, RGSCLS_SINGLEUSE, REGCLS_MULTIPLEUSE, REGCLS_MULTI_SEPARATE 등과 같이 서버가 클라이언트와 어떻게 대응하는지를 지정하게 된다.

어쨌든 CoRegisterClassObject 함수로 로컬서버의 클래스 팩토리가 클래스 테이블에 등록되면 CoGetClassObject는 클래스 테이블로부터 클래스 팩토리를 얻은 다음 IClassFactory::CreateInstance를 호출한다.

정리해보면 인프로세스 서버와 로컬서버의 객체 생성과정은 DLL의 수동성과 EXE의 능동성의 차이, 같은 주소공간이냐, 다른 주소공간이냐에 따라 다를 수 밖에 없었다. 특히 로컬서버의 경우 서버가 실행될 때 CoRegisterClassObject를 이용하여 자신의 클래스 팩토리를 전역 클래스 테이블에 등록하게 된고, COM런타임 라이브러리는 이 등록된 클래스 팩토리로 로컬객체를 생성한다. 이 과정을 나타낸 것이 <그림 4>이다.

<그림 4> 로컬 객체의 생성과정

원격객체의 생성과정

인프로세스 서버와 로컬 서버까지는 COM에 대한 이야기였다. 그러나 지금부터 언급할 원격객체는 DCOM(Distributed COM)의 영역에 해당한다. DCOM이라고 해서 지레 겁먹을 필요는 없다. DCOM은 COM을 자연스럽게 확장한 것이기 때문에 COM에 대한 지식은 모두 DCOM에도 적용된다.

사실 원격 서버는 로컬 서버의 메커니즘과 거의 유사하다. 둘 다 클라이언트의 주소공간에 위치하지 않기 때문에, 마샬링과 프록시 / 스텁 같은 프로세스 경계를 넘어서기 위한 장치들을 사용해야 한다.

차잇점이라면 로컬 서버는 프로세스간 통신을 위해 LPC(Local Procedure Call)라는 프로토콜을 사용하지만, 원격서버는 RPC(Remote Procedure Call)이라는 프로토콜을 사용한다. 로컬 서버인 경우 플랫폼 독립성을 고려하지 않아도 되지만, 원격 서버인 경우 서버나 클라이언트가 반드시 Windows라는 보장이 없기 때문에 산업표준인 RPC를 사용하는 것이다.

그 외에도 원격 서버에 접속한다는 것은 다른 컴퓨터에 있는 자원을 사용한다는 의미이기 때문에 보안에 대한 장치가 필요하다. DCOM은 보안을 위한 여러가지 장치들을 Windows NT 기술과 맛물려서 구현하고 있다.

DCOM은 객체 서버와 그것을 사용하는 클라이언트가 서로 다른 기계에 위치할 경우 그 둘간의 통신을 어떻게 해 줄것인가에 대한 규약이다. 서로 다른 기계간의 통신이므로 당연히 네트웍 프로토콜이 사용된다. DCOM은 일반적으로 사용되는 프로토콜 위에 얹히는 ORPC(Object Remote Procedure Call)라는 프로토콜을 사용한다. ORPC는 산업표준인 DCE-RPC를 확장한 것이다.

그렇다면 이제 이글의 주제로 넘어가서, 원격서버에 있는 객체는 어떻게 생성되는가를 살펴보기로 하자. 원격 객체의 생성과정을 <그림 5>에 나타내 보았다.

<그림 5> DCOM의 동작 모습

우선 생소한 용어인 SCM(Service Control Manager)에 대해서 짚어보자. SCM은 COM 런타임 모듈의 일부로서, 주로 객체 서버의 활성화를 담당한다. 예를 들어 레지스트리 항목을 조사해서 객체 서버의 경로를 알아내고, 이를 실행하여 클래스 팩토리를 얻어내는 것이 SCM이 하는 일이다.

SCM은 하나의 기계의 하나의 인스턴스만 존재한다. 그렇다면 원격에 있는 객체를 실제로 생성하는 것은 원격 시스템의 SCM일 것이다.

클라이언트가 원격 객체를 요구하면 SCM은 DCOM 네트웍 프로토콜을 통해서 원격 시스템의 SCM과 통신한다. 원격 SCM은 컴포넌트의 인스턴스를 만든다. 그리고 클라이언트 / 서버 간의 통신을 위해 마샬링을 담당할 프록시 / 스텁을 클라이언트 / 서버에 활성화시킨다. 이때 SCM은 자신을 역할을 다하고 무대 뒤로 사라진다.

이제부터 실제 클라이언트와 서버의 통신은 프록시와 스텁이 담당한다. 물론 DCOM이기 때문에 보안인증을 거친 후 ORPC 프로토콜로 통신할 것이다.

<박스 기사 2> ORPC와 EntireX 프로젝트

OSF(Open Software Foundation)에 의해서 DCE-RPC라는 원격 프로시져의 호출 표준이 만들어졌지만, 마이크로소프트는 ORPC라는 DCE-RPC의 확장을 사용한다. 이는 DCOM을 만들면서 어쩔 수 없는 제한 때문에 표준을 확장한 것으로 보인다.

COM이 DCOM으로 발전하면서, 기존의 Unix 머신이나 메인프레임에 배치된 COM객체 (주로 데이터 서비스나, 비즈니스 로직을 구현한 객체일 것이다)와 PC의 Windows에 배치된 COM객체 (주로 유저 서비스 객체일 것이다)가 서로 연동될 수 있는가에 사람들의 관심이 모아지기 시작했다. DCOM이 명실상부한 분산환경을 위한 플랫폼이라면 이를 지원해야 하는 것은 당연한 의무였다.

DCOM의 강력한 경쟁자 CORBA가 이러한 플랫폼 독립성을 제공하는데 비해서, DCOM은 현재까지는 Windows플랫폼 끼리만 분산 컴퓨팅을 할 수 있었다. 이러한 제한은 주로 DCOM이 표준 RPC가 아닌 확장된 ORPC를 사용하는데 그 이유가 있다.

따라서 마이크로소프트는 Software AG Americas (SAGA) 라는 회사와 함께 Unix 기종과 다양한 메인프레임 및 미니컴퓨터에도 DCOM을 적용시키기 위한 프로젝트를 진행중이다. 이 프로젝트는 EntireX DCOM이라는 거창한 이름으로 DCOM을 모든 플랫폼에 이식시키려는 노력을 하고 있으며, 일부는 성공적으로 이식이 되었다고 한다. 자세한 정보는 http://www.sagus.com 에서 찾아볼 수 있다.

원격객체 생성을 위한 레지스트리 설정

인프로세스 서버나 로컬 서버는 그것을 실행시키기 위한 정보가 시스템 레지스트리에 기재되어 있다. 그러나 원격 객체인 경우 인프로세스 서버나 로컬 서버처럼 서버의 경로만을 기재하는 것으로는 부족하다. 당연히 원격 시스템에 대한 IP 어드레스가 기재되어야 할 것이다. 또한 보안인증에 관한 정보도 기재되어 있어야 한다. 따라서 DCOM은 새로운 레지스트리 항목들을 제안하고 있다.

우선 가장 중요한 것은 AppID라는 키일 것이다. AppID는 앞에서 이야기한 원격객체를 억세스하는데 필요한 인자들, 즉 원격 시스템의 주소, 보안에 관한 설정 등을 모아놓은 키이다. 또한 이러한 설정들은 하나의 원격객체에만 해당하는 것이 아니라, 여러 개의 원격객체가 같은 설정을 사용할 수 있다.

그렇다면 AppID와 CLSID간에는 어떤 맵핑정보가 있을 것이다. 이것은 시스템 레지스트리의 CLSID키에 그 정보가 있다. HKCR\CLSID\{<clsid>} 키에는 AppID라는 명명값 (Named value)이 있고, 여기에 AppID가 기록된다. 그리고 HKCR\AppID\{<appid>} 키에는 원격설정에 관한 정보들이 수록된다. 이 중에서 가장 중요한 것은 원격기계의 주소를 나타내는 RemoteServerName 명명값일 것이다. (<그림 6> 참조)

<그림 6> 원격서버의 레지스트리 설정

<그림 6>에는 RemoteServerName만 설정되어 있지만, AppID에 설정할 수 있는 값들은 <표 1>과 같다.

AppID 설정값들 의미
RemoteServerName 원격기계의 주소
ActivateAtStorage 데이터가 있는 곳의 서버가 실행되도록 함
LocalService 어플리케이션을 Win32 서비스로 설정
ServiceParameters LocalService가 설정되었을 때, 서비스로 전달될 인자
RunAs 서버를 사용할 수 있는 사용자를 설정
LaunchPermission 서버를 활성화할 수 있는 ACL(Access Control List)를 설정.
AccessPermission 서버에 접근할 수 있는 ACL을 설정한다.
DllSurrogate DLL 대행자를 지정. 빈 문자열이면 디폴트 대행자 사용
AuthenticationLevel AppID의 인증레벨을 설정
<표 1> AppID 설정값들

원격객체 활성화를 위해 레지스트리를 설정하고, 이것이 유효하게 하려면 COM객체를 생성하는 클라이언트는 CoCreateInstance를 CLSCTX_SERVER 로 지정해야 한다. 이 지정은 가능한 서버형태를 모두 찾아서 실행하게 한다. 만일 가능한 서버형태가 여러 개라면, 인프로세스 서버, 로컬 서버, 원격 서버의 순으로 실행한다.

따라서 원격객체를 실행하게 하려면 레지스트리의 HKCR\CLSID\{<clsid>} 키에 InprocServer32나 LocalServer32 키를 없애거나, 임시로 이름을 바꾸어야 한다. 어쨌든 클라이언트가 CLSCTX_SERVER로 객체를 생성할 경우에는 순전히 레지스트리의 조작으로 서버의 위치를 지정할 수 있다.

DCOM 패키지가 설치된 시스템에는 DCOMCNFG.EXE라는 프로그램이 제공된다. 이 프로그램은 원격객체의 활성화를 위해서 앞에서 설명한 레지스트리 항목들을 설정해주는 매우 편리하고 유용한 프로그램이다.

그러나 레지스트리를 통한 원격객체 활성화는 배포와 관리의 문제에 봉착한다. 예를 들어 하나의 객체서버에 접근하는 100개의 클라이언트 PC가 있다고 하자. 그런데, 객체서버의 위치가 A주소에서 B주소로 바뀌었다면, 100개의 클라이언트 PC의 레지스트리를 모두 바꾸어 주어야 한다.

이런 문제를 해결하기 위해 내년 상반기 출시예정인 Windows NT 5.0은 객체 스토어(Object Store)라는 개념을 도입했다. 이 개념은 원격객체에 대한 설정을 하나의 장소(객체 스토어)에 모아놓고 관리하며, 클라이언트들은 지정된 객체 스토어에서 원격객체 활성화에 대한 정보를 얻는다. 따라서 앞에서와 같은 클라이언트 레지스트리 변경과 같은 문제가 해결된다. 이 기능 하나만으로도 Windows NT 5.0은 매우 기대되는 제품이 될 것이다.

<박스기사 3> DLL 대행자 (Dll Surrogate)

일반적인 상식으로 원격서버나 로컬서버는 반드시 실행파일(EXE) 형태여야 한다고 생각된다. 왜냐하면 원격서버나 로컬서버는 클라이언트와 다른 주소공간에서 실행되기 때문에 클라이언트가 서버를 직접적으로 제어할 수 없기 때문이다. 그래서 로컬서버나 원격서버는 스스로 실행되면서, 클라이언트와의 최소한의 통신으로 스스로를 제어하기 마련이다.

그러나 DLL이 로컬서버나 원격서버가 될 수도 있다. DLL은 수동적인 모듈이기 때문에 자신이 스스로 실행될 수 없다. 따라서 DLL 서버들을 수용하여 그것들을 작동시키는 일종의 대행자(Surrogate)가 필요하다. 당연히 대행자는 EXE형태의 서버일 것이다.

Windows NT 4.0에 서비스팩 3를 설치했거나, Windows 95/98에서 DCOM패키지를 설치할 경우 디폴트 대행자로 dllhost.exe 라는 프로그램이 설치된다. 이 프로그램은 인자로 인프로세스 서버의 CLSID를 주면, 서버를 실행시켜 자신이 관리한다. 그리고 외부에서 볼때는 마치 EXE형태의 서버가 실행된 것처럼 보이게 된다.

dllhost.exe라는 Windows의 기본 DLL 대행자로 사용된다. 실제로 CoCreateInstance를 실행할 때, CLSCTX_LOCAL_SERVER로 지정하고 DLL객체를 생성하면 자동적으로 dllhost.exe가 실행되고, DLL객체를 자신이 관리하게 된다.

요즘 뜨고 있는 MTS(Microsoft Transaction Server)도 바로 정교하고, 부가기능이 추가된 DLL 대행자의 일종이다. MTS가 DLL형태의 인프로세스 서버만을 관리한다는 것과 MTS에 등록된 객체가 원격에서 사용가능하다는 것을 생각하면 쉽게 수긍이 갈 것이다.

DLL 대행자의 의미는 DLL서버가 COM객체를 구축하는 프로그래머에게 가장 친숙하다는 데 있다. 또한 특별한 UI가 없는 비즈니스 객체나, 데이터 서비스 객체의 경우 EXE로 서버를 작성한다는 것이 번거롭기만 하다. 결정적으로 DLL서버 하나만 만들면, 이를 로컬에서도 원격에서도 사용할 수 있어서 실행위치에 관계없이 하나의 버전만을 만들어도 된다는 장점이 돋보인다.

동적으로 원격객체 생성을 설정

원격객체를 활성화하기 위해서 사용할 수 있는 또다른 방법은 클라이언트 코드에서 명시적으로 원격서버를 지정하는 방법이 있다. 이는 주로 CoCreateInstanceEx 라는 확장된 객체생성 API를 사용한다. CoCreateInstanceEx의 프로토타입은 다음과 같다.

HRESULT CoCreateInstanceEx(
	REFCLSID rclsid, // 생성할 객체의 CLSID
	IUnknown *punkOuter, // Aggregation을 위한 IUnknown 포인터
	DWORD dwClsCtx, // 서버의 위치를 지정
	COSERVERINFO *pServerInfo, // 원격머신에 대한 정보 
	ULONG cmq, // 얻어올 인터페이스의 개수
	MULTI_QI *pResults // 얻어올 인터페이스와 리턴값의 배열
);

CoCreateInstance와 유사하지만 몇가지 새로운 인자들이 눈에 띈다. 먼저 pServerInfo는 원격서버가 실행될 기계에 대한 정보를 지정하게 된다. 여기에 RemoteServerName에 해당하는 정보가 지정된다.

또 다른 눈에 띄는 점은 CoCreateInstance가 객체 생성 후 얻어올 인터페이스를 하나만 지정하는데 비해, CoCreateInstanceEx는 여러 개의 인터페이스를 한번에 얻어올 수 있다는 점이다.

분산환경에서는 원격함수를 한번 호출하는 것 자체가 네트웍 트래픽을 일으킨다. 따라서 최적화된 분산환경은 필요한 정보는 한꺼번에 얻어와 빈번한 원격함수 호출을 자제하는 것이 일반적이다. CoCreateInstanceEx도 이를 배려한 흔적이다. DCOM규약에는 CoCreateInstanceEx 외에도 네트웍 트래픽을 줄이기 위한 많은 노력이 보인다.

다음의 코드는 CoCreateInstanceEx를 사용하는 일례를 보여준다.

HRESULT hr = S_OK;

MULTI_QI mqi[] = {
	{ IID_IProvideClassInfo, NULL, 0 },
	{ IID_IDispatch, NULL, 0 }
};

COSERVERINFO srvinfo = { 0, OLESTR(“server.com”), NULL, 0 };

hr = CoCreateInstanceEx(CLSID_MyServer,
	NULL,
	CLSCTX_SERVER,
	&srvinfo,
	sizeof(mqi) / sizeof(mqi[0]),
	&mqi);

if (SUCCEEDED(hr)) {
	if (SUCCEEDED(mqi[0].hr)) {
		IProvideClassInfo *pClassInfo = mqi[0].pItf;
		// pClassInfo 사용
		pClassInfo->Release();
	}
	// mqi[1].pItf도 같은식으로 사용
}

결국 원격객체를 설정하는 방법은 레지스트리 설정을 사용하는 방법과 직접 동적으로 설정하는 방법 두가지가 있다.

첫번째 방법은 주로 분산 컴퓨팅을 통해서 임무 수행 속도를 증진시키는 경우에 사용될 것이다. 왜냐하면 이 방법을 사용할 경우 서버객체가 더 좋은 성능을 위해 다른 곳으로 옮겨지더라도 클라이언트 프로그램을 수정할 필요가 없기 때문이다. 단지 레지스트리 설정만 변경하면 된다. 즉 잠정적으로 고정된 분산구조에 적합하다.

두번째 방법은 멀티플레이어 게임이나, 채팅과 같이 동적으로 네트웍으로 참여하는 기계가 다를 경우에 주로 사용된다. 예를 들어 채팅 어플리케이션의 경우 서버 컴포넌트의 위치는 정해지겠지만, 클라이언트의 위치는 시시각각으로 변한다. 이럴 경우 동적으로 연결을 관리하기 때문에 이 방법이 적절하다.

모니커에 의한 생성

지금까지 객체를 생성하는 서비스 API와 그것의 내부 동작에 대해서 살펴보았다. 그런데 객체생성 서비스에서 모니커(Moniker)를 빼면 참으로 서운해할 것이다. 모니커는 매우 고수준의 객체 생성 서비스를 제공한다. 고수준이라는 의미는 그만큼 사용하기 쉽고 직관적이라는 의미이다.

모니커는 “지능적인 이름(Intelligent Names)”이라고 불리운다. 지능적이라는 의미는 모니커가 사람이 읽을 수 있는 이름으로 객체를 바인딩하는 능력을 말한다. 즉 이름을 통해서 그 이름과 연관된 객체서버를 찾아내는 능력을 지능적이라고 한다.

모니커는 IMoniker를 구현한 객체이며, 파일모니커, 아이템모니커, 안티모니커, URL 모니커, 클래스 모니커, 복합모니커 등이 기본적으로 제공된다.

예를 들어보자. “c:\document\myword.doc”이라는 Word파일이 있다고 하자. 이 이름을 MkParseDisplayName이라는 API의 인자로 주면 파일모니커가 생성된다. 생성된 파일모니커의 IMoniker::BindToObject를 실행하면 doc이라는 확장자에 연결된 Word 프로그램을 실행시키고, myword.doc을 읽어들이게 된다. IMoniker::BindToObject함수도 CoCreateInstance처럼 객체의 원하는 인터페이스 포인터를 리턴한다.

즉 모니커를 통해 Word라는 객체 서버를 활성화하고, 거기에 덧붙여 객체의 상태까지 읽어들인 것이다. CoCreateInstance류의 객체 생성 서비스와 모니커는 C++의 인자없는 디폴트 생성자(constructor)와 인자가 있어 상태를 설정해주는 생성자와 비교할 수 있을 것이다. 즉 CoCreateInstance류의 객체 생성 서비스는 오직 초기화상태로만 생성할 수 있다. 그러나 모니커는 생성과 영속성(persistence)을 모두 지원한다.

그러나 아쉽게도 이 글에서는 모니커에 대한 설명을 이 정도로 줄여야겠다. 모니커에 대해서는 별도의 기사로 다시 준비할 것을 약속한다.

마치면서

지금까지 COM의 객체 생성 메커니즘에 대해서 집중적으로 살펴보았다. COM의 객체 생성 메커니즘은 DCOM이 등장하면서, ActiveX 기술이 등장하면서 점점 더 복잡해지고 다양해지고 있다. 이 다양함 속에서 하나의 줄기를 찾아보자는 것이 이 글의 목적이었는데, 제대로 달성되었는지는 의문이다.

이 글이 COM클라이언트를 작성하는 독자든, 서버를 작성하는 독자든 객체 생성과정에 대해서 충분한 이해를 가지고, 오류없는 프로그램을 작성하는데 도움을 줄 수 있기를 바란다.

참고자료

    1. Component Object Model Specification 0.9 - Microsoft
    2. DCOM Specification 1.0 - Microsoft
    3. Inside OLE - Kraig Brockschmidt - Microsoft Press
    4. Inside COM - Dale Rogerson - Microsoft Press
    5. Inside Distributed COM - Guy Eddon, Henry Eddon - Microsoft Press
    6. MSDN98 - Technical Articles / Component Object Model / OLE / Managing Object Lifetime in OLE
    7. MSDN98 - Backgrounders / Component Object Model / OLE / DCOM Architecture