COM, ATL

컴포넌트 기본 개념

디버그정 2008. 7. 26. 15:43
COM(1)
조회(108)
C/C++, MFC | 2008/05/04 (일) 10:58
추천하기 | 스크랩하기

컴포넌트

컴포넌트는 DLL 또는 EXE 확장자로 끝나는 파일이다. 컴포넌트는 실행 파일이나 동적 연결 라이브러리 안에 존재하고 있기 때문에 우리가 실제로 눈에 볼 수 있는 것은 이러한 확장자를 가진 파일이다. 간혹 OCX 확장자로 끝나는 파일도 있으나 이는 겉으로 보이는 확장자만 다를 뿐 내부적으로는 DLL과 동일하다. 컴포넌트는 실행 파일과 동적 연결 라이브러리 이 두 가지 형태의 겉모습을 가지고 존재한다.
 
객체 지향 프로그래밍(OOP) 관점에서 컴포넌트는 캡슐화, 다형성, 상속성을 갖춘 객체이다. 게다가 재사용성까지 갖추었다.
컴포넌트는 객체로서 인스턴스 생성이 가능하기 때문에 객체 지향 언어인 C++ class와 유사하다고 볼 수 있다. 또한 실제로 컴포넌트는 C++ class를 통해 구현이 된다.
그러나 class는 소스 파일상에 존재하는 코드일 뿐이고 컴포넌트는 코드가 컴파일되어 만들어진 바이너리 파일이라는 것을 잊어서는 안 된다.
 
C++ 프로그래머 입장에서 컴포넌트는 구조체의 포인터이다.
컴포넌트를 생성하면 구조체의 포인터를 얻게 된다.
프로그래머는 이 포인터를 통해서 구조체가 가지고 있는 멤버함수에 접근할 수 있다.
컴포넌트를 사용한다는 것은 구조체의 포인터를 통해 멤버함수를 호출하는 것을 뜻한다.
프로그래머가 코드상에서 볼 수 있는 것은 오직 구조체의 포인터와 그 구조체가 가지고 있는 멤버함수들뿐이다.
 
C++ 코드 상에서 컴포넌트를 생성하면 구조체의 포인터를 얻고 그 포인터를 통해서 구조체의 멤버함수에 접근할 수 있다. 컴포넌트가 제공하는 이러한 구조체를 인터페이스라고 부른다.
모든 컴포넌트는 최소한 2개 이상의 인터페이스를 반드시 제공해야 하고 프로그래머는 오직 인터페이스를 통해서만 컴포넌트에 접근이 가능하다.
objbase.h 헤더파일을 열어보면 인터페이스에 대한 정의를 다음과 같이 볼 수 있다.
 
#define interface struct
 
인터페이스는 우리가 흔히 사용하는 구조체임에 틀림없지만 불행히도 이 구조체는 사용상 몇 가지 제약이 따른다.
1.        인터페이스는 자신의 멤버로 함수만 갖는다 (변수를 멤버로 가질 수는 없다)
2.        인터페이스 내의 멤버함수는 순수 가상함수 형태로 선언되어야 한다.
3.        인터페이스 멤버함수는 HRESULT라는 데이터 타입을 리턴해야 한다.
4.        위와 같은 방법으로 정의된 인터페이스를 외부에 공표하면 절대로 바꾸면 안 된다.
 
 
C++
인터페이스를 정의한다는 것은 헤더파일에 구조체를 정의하고 그 속에 순수 가상함수를 선언하는 작업을 의미한다.
그리고 정의된 인터페이스를 구현한다는 것은 인터페이스를 상속 받는 클래스를 만들고, 그 클래스를 통해 인터페이스가 가진 순수 가상함수를 구현한다는 것을 뜻한다.
 
여기서 한가지 주목할 점은 인터페이스의 정의는 선언만 있을 뿐이지 인터페이스의 구현과는 별개의 일이라는 것이다. 이를 구현으로부터 인터페이스의 분리(separation of interface from implementation)라고 하는데 이를 통해서 인터페이스는 어떤 기술의 표준으로서 역할을 할 수 있다.
 
이 모든 것은 인터페이스의 정의가 구현으로부터 분리됐으며, 일단 정의된 인터페이스는 다시는 바뀌지 않는다는 원칙 때문에 가능하다. 구현으로부터 인터페이스의 분리(separation of interface from implementation)는 인터페이스를 정의하는 이와 구현하는 이를 분리시킨다. 이 둘은 공표된 인터페이스는 절대로 바뀌지 않는다는 규칙으로 서로를 신용한다. 인터페이스의 불변은 인터페이스의 이름과 멤버함수들의 이름, 개수, 그리고 프로토타입의 불변을 뜻한다.
 
마이크로소프트는 10개가 넘는 인터페이스를 정의하여 공표하고, ActiveX 컨트롤이란 이름을 붙였다. 10개가 넘는 인터페이스들을 구현하면 그게 바로 ActiveX 컨트롤이 되는 것이다. ActiveX 컨트롤뿐만 아니라 ActiveX Document, OLE DB, ADO, ADSI 등 다른 기술 표준들도 그 핵심에는 인터페이스의 정의와 공표가 있다.
어떠한 인터페이스를 구현했느냐에 따라 ActiveX 컨트롤도 되고 ActiveX Document도 되지만 무엇이 됐든 이들은 결국 컴포넌트라는 큰 범주에 포함된다.
 
EXE 또는 DLL 파일
 
컴포넌트
컴포넌트
컴포넌트
컴포넌트
 
인터페이스
메소드
메소드
 
인터페이스
메소드
메소드
 
인터페이스
메소드
메소드
 
인터페이스
메소드
메소드
 
하나의 파일 안에는 여러 개의 컴포넌트가 들어갈 수 있고
하나의 컴포넌트 안에는 여러 개의 인터페이스가 들어갈 수 있다.
 
 

COM 서브 시스템

클라이언트/서버 개념으로 볼 때 컴포넌트는 서버이고, 이를 사용하는 어플리케이션은 클라이언트이다.
 
클라이언트가 컴포넌트를 생성하는 방법
COM 규약은 컴포넌트로부터 인스턴스, 즉 객체를 생성하는 방법을 규정하고 있는데 그 방법은 바로 클라이언트가 CoCreateInstance() 라는 COM API 함수를 호출하는 것이다.
 
CoCreateInstance() 는 매개변수로 컴포넌트 식별자와 인터페이스 식별자를 받아 컴포넌트 식별자와 일치하는 컴포넌트를 찾아 객체를 생성하고 인터페이스 식별자와 일치하는 인터페이스를 찾아 그에 대한 포인터를 리턴시킨다.
 
COM API는 컴포넌트 인스턴스를 생성하는 기본적인 함수 외에도 다양한 서비스를 제공하는 함수를 포함한다. 이렇게 API를 통해 서비스를 제공할 수 있는 것은 COM이 운영체제에 포함되어 일종의 서브 시스템으로 존재하기 때문에 가능하다. 어플리케이션이 운영체제에서 제공되는 COM 서비스를 사용하기 위해서는 COM API가 들어있는 라이브러리(이를 COM 라이브러리라 한다.)를 프로세스의 주소 공간에 로드시키고 이를 초기화해 주어야 한다.
 
이러한 작업은 CoInitialize() 라는 API 함수를 통해 이루어지고 클라이언트는 COM 서비스를 사용하기 전에 이를 반드시 호출해야 한다. COM 라이브러리 초기화는 스레드와 밀접한 관련이 있다. 만약 여러 개의 스레드가 COM 서비스를 이용하고자 한다면 각각의 스레드는 개별적으로 CoInitialize()를 호출해서 COM 라이브러리를 초기화시켜 주어야 한다. COM 서비스의 사용이 모두 끝나면 각각의 스레드는 CoUninitialize() 함수를 호출해서 스레드 별로 COM 라이브러리가 차지한 리소스를 풀어주어야 한다.
 
윈도우 운영체제에서는 모든 어플리케이션이 메시지 펌프를 가지고 있다는 전제 하에 메시지를 통해 각종 서브 시스템과 어플리케이션 간의 의사 소통을 가능하게 한다. COM 서브 시스템은 컴포넌트의 프로세스가 기본적으로 메시지 펌프를 가지고 있다는 전제 하에 메시지를 통해 클라이언트와 컴포넌트 간의 의사소통을 중재하게 된다.
 
만약 컴포넌트가 클라이언트와 같은 프로세스 내에 존재하는 경우(DLL 컴포넌트의 경우) 라면 컴포넌트 자체는 메시지 펌프를 가지고 있지 않아도 되지만 대신 클라이언트는 메시지 펌프를 가지고 있어야 한다. 컴포넌트가 클라이언트와 다른 프로세스에 존재하는 경우라면 각자가 펌프를 갖춰야 하겠다.
 
 

컴포넌트 식별자

컴포넌트와 인터페이스는 자신을 식별하기 위해 128비트 길이의 구조체를 사용하여 여기에 고유한 값을 넣고 자신의 식별자로 사용한다. 이 식별자는 지구상에서 유일하다는 뜻으로 GUID(Globally Unique Identifiers)라고 한다. 새로운 GUID를 만들기 위해서는 COM API CoCreateGuid()를 호출하거나 플랫폼 SDK의 툴에 포함된 Guidgen.exe 프로그램(이 프로그램은 Visual Studio.NET Common7\Tools 디렉토리 밑에 들어 있다.)을 사용하면 된다. 언제 어느 컴퓨터에서 누가 GUID를 생성하건 일단 생성된 값은 지구상에서 유일하다. GUID는 다음과 같은 구조체로 정의된다.
 
typedef struct GUID
{
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[8];
} GUID;
 
typedef GUID CLSID;
typedef GUID IID;
 
컴포넌트와 인터페이스 식별자 모두 같은 구조체를 사용하지만 컴포넌트 식별자는 CLSID로 인터페이스 식별자는 IID로 구분하여 사용한다. 공표된 인터페이스는 절대로 바뀌지 않는다는 가정에는 인터페이스에게 부여된 식별자, IID는 절대로 바뀌지 않는다는 뜻이 포함되어 있다.
 
GUID 구조체를 가지고 인터페이스 A에 대한 IID를 만들고 고유한 값을 넣은 예
IID IID_A =
{
0x1df0111, 0x0011, 0xda90,
{0xb2, 0x00,0x12, 0x23, 0x00, 0x11, 0xff, 0x61}
};
 
 
컴포넌트는 CLSID 외에도 ProgID 라고 불리는 문자열로 만들어진 식별자를 하나 더 가지고 있다. ProgID는 적어도 같은 컴퓨터 내에서는 중복이 되어서는 안 된다. ProgID는 일반적으로 {어플리케이션.컴포넌트명.버전번호} 형태를 갖는다.
 
) MS-Word 8.0에 존재하는 Application이라는 컴포넌트의 ProgID
Word.Application.8
 
왜 컴포넌트는 CLSID를 가지고 있음에도 불구하고 ProgID 라는 불완전한 식별자를 가질까?
1.        ProgID CLSID 1 1로 짝을 이루어 그 정보가 레지스트리에 저장된다.
2.        COM API CLSIDFromProgID() 가 있는데 이 함수는 ProgID를 매개변수로 받고 이에 해당하는 CLSID를 리턴한다.
3.        CLSIDFromProgID(LWord.Application.8) MS-Word 8.0 Application 컴포넌트의 CLSID를 리턴한다.
4.        CLSIDFromProgID(LWord.Application.9) MS-Word 9.0 Application 컴포넌트의 CLSID를 리턴한다.
5.        CLSIDFromProgID(LWord.Application)는 가장 최신 버전의 MS-Word Application 컴포넌트의 CLSID를 리턴한다.
6.        Word.Application 과 같이 버전이 생략된 ProgID VersionIndependentProgID라 한다.
7.        VersionIndependentProgID를 사용하여 CLSIDFromProgID()를 호출하면 가장 최신 버전의 컴포넌트의 CLSID를 얻을 수 있다.
8.        컴포넌트의 인스턴스를 생성하려면 CoCreateInstance() 를 호출해야 하고 이 함수는 매개변수로 CLSID IID를 요구한다.
9.        VersionIndependentProgID를 사용하여 CLSIDFromProgID()를 호출하면 가장 최신 버전의 컴포넌트 CLSID를 얻을 수 있고 이를 CoCreateInstance() 의 파라미터로 전달하면 가장 최신 버전의 컴포넌트 인스턴스를 생성할 수 있다.
10.    9번의 방식대로 컴포넌트를 생성하는 클라이언트 프로그램은 컴포넌트가 업데이트될 때마다 새로운 버전의 컴포넌트를 사용하기 위해 코드를 수정하고 다시 컴파일할 필요가 없다.
 
ProgID를 사용하는 이유는 무엇보다도 가독성이 좋고
버전관리가 편리하기 때문이라고 결론 지을 수 있다.
 
다음은 1~8까지 설명한 내용을 C++ 코드로 작성한 것이다.
CoInitialize(NULL);
CLSID clsid;
CLSIDFromProgID(LWord.Application, &clsid);
CoCreateInstance(clsid, );
 
// 컴포넌트 사용
 
CoUninitialize();
 

컴포넌트와 레지스트리

컴포넌트의 인스턴스를 생성하기 위해서는 CoCreateInstance() 를 호출해야 하고 파라미터로 CLSID IID를 전달한다고 하였다. 그럼 CoCreateInstance() 내부에서는 파라미터로 받은 CLSID와 일치하는 컴포넌트를 찾는 일을 해야 하는데 과연 어디서 찾을 것인가? 이 물음에 대한 해답이 레지스트리에 있다.
HKEY란 이름으로 시작하는 최상위 키를 루트키라 하는데 컴포넌트와 관련된 정보는 루투키 중 HKEY_CLASSES_ROOT 밑에 들어 있다. 루트키 HKEY_CLASSES_ROOT 밑에는 우리가 찾고자 하는 CLSID 뿐만 아니라 IID 그리고 ProgID 등이 포함되어 있다.
 
HKCR 바로 밑에는 먼저 ProgID가 나열된다.
CLSID 128비트 길이의 구조체이다. COM API StringFromCLSID() 는 파라미터로 CLSID를 받고 이를 다음과 같은 형태의 문자열로 바꿔준다. CLSID 뿐 아니라 모든 GUID는 레지스트리 상에서 다음과 같은 형태의 문자열로 표현된다.
 
{AE002040-1300-5300-C000-By00DF000046}
 
 
HKCR\CLSID 키 밑에는 {CLSID 문자열} 서브키가 나열된다.
 
HKCR\CLSID\{CLSID 문자열} 키 밑에는 컴포넌트 파일의 경로를 담고 있는 키가 존재해야 하는데 그 키의 이름은 컴포넌트의 종류와 위치에 따라 다르다. 다음은 컴포넌트의 종류와 위치에 따라 컴포넌트 파일의 경로를 갖는 키 이름이 어떻게 다른지를 나타낸다.
 
1.        컴포넌트가 클라이언트 컴퓨터 상에 위치하고 DLL 파일인 경우: InprocServer32
2.        컴포넌트가 클라이언트 컴퓨터 상에 위치하고 EXE 파일인 경우: LocalServer32
3.        컴포넌트가 리모트 컴퓨터 상에 위치하고 DLL 파일인 경우: 키 없음
4.        컴포넌트가 리모트 컴퓨터 상에 위치하고 EXE 파일인 경우: 키 없음
 
1,2 번의 경우 InprocServer32, LocalServer32 키 안에 컴포넌트 파일에 대한 경로를 표시하는 문자열 값이 들어간다. CoCreateInstance() 는 매개변수로 받은 CLSID로부터 레지스트리를 검색하여 InprocServer32 같은 키 안에 들어있는 값을 얻어 컴포넌트 파일의 경로를 알아낸다. CoCreateInstance() 는 경로 값을 가지고 1번의 경우 내부적으로 LoadLibrary() 를 호출해서 클라이언트 프로세스 안에 DLL 컴포넌트를 로드시킬 것이고, 2번의 경우 CreateProcess()를 호출해서 EXE 컴포넌트를 기동시킬 것이다.
 
3,4번의 경우 컴포넌트 파일의 경로 값을 갖는 키가 존재하지 않는다. 대신 이들은 HKCR\CLSID\{CLSID 문자열} 키 안에 AppID라는 이름의 GUID 값을 갖는다. 그리고 AppID는 컴포넌트 파일의 경로를 찾을 수 있는 힌트를 제공한다. HKCR\AppID 키 밑에는 많은 {AppID 문자열} 키들이 존재한다.
 
HKCR\AppID\{AppID의 문자열} 키 안에는 RemoteServerName 이란 이름으로 리모트 컴퓨터의 이름 또는 IP 주소를 나타내는 값이 존재한다. CoCreateInstance()는 파라미터로 전달된 CLSID를 가지고 HKCR\CLSID\{CLSID 문자열} 키 밑에 InprocServer32 혹은 LocalServer32 키가 있는지 살피고, 없다면 AppID 이름값이 존재하는지 살핀다. 만약 이름값이 존재하면 이 값과 일치하는 키를 HKCR\AppID키 밑에서 찾는다.
 
만약 일치하는 키를 발견하면 HKCR\AppID\{AppID 의 문자열} 키 안에서 RemoteServerName 이름 값이 존재하는지 살핀다. 만약 존재하면 리모트 컴퓨터의 이름 또는 IP 주소를 얻게 되고, 이 정보를 바탕으로 네트워크 상의 리모트 컴퓨터의 이름 또는 IP 주소를 얻게 되고, 이 정보를 바탕으로 네트워크 상의 리모트 컴퓨터에게 CLSID를 전달해서 컴포넌트를 생성하도록 요청하게 된다. 컴포넌트 생성 요청을 받은 컴퓨터는 앞의 1,2번의 경우와 같은 방식으로 레지스트리에서 컴포넌트 파일의 경로를 찾고 이를 생성한다. HKCR\AppID\{AppID의 문자열} 키 안에는 RemoteServerName 외에도 보안과 관련된 다양한 정보가 들어 있다. 다음과 같은 경우에는 클라이언트 컴퓨터의 레지스트리, HKCR\CLSID\{CLSID의 문자열} 키 안에 AppID 이름값이 존재해야 한다.
 
1.        리모트 컴퓨터 상에 컴포넌트를 생성하고자 할 경우
2.        EXE 컴포넌트를 NT 서비스로 구동시키고자 할 경우
3.        DLL 컴포넌트를 클라이언트 프로세스가 아닌 다른 프로세스(이를 대리자(Surrogate) 프로세스라 한다.)의 주소공간(Address space)에서 구동시키고자 할 경우
 

IUnknown 인터페이스

IUnknown 인터페이스는 모든 컴포넌트가 반드시 제공해야 하며, 다른 모든 인터페이스의 부모가 되는 인터페이스이다. , 모든 인터페이스는 IUnknown 인터페이스를 상속받아야 한다. IUnknown 인터페이스는 unknwn.h 파일에 다음과 같이 정의 되어 있다.
 
Interface IUnknown
{
virtual HRESULT _stdcall QueryInterface(const IID& iid, void** ppv) = 0;
virtual ULONG _stdcall AddRef() = 0;
virtual ULONG _stdcall Release() = 0;
};
 
IUnknown 인터페이스를 제외한 다른 모든 인터페이스는 IUnknown 인터페이스를 상속받아야 한다는 원칙에 따라 인터페이스 A를 정의해 보면 다음과 같다.
 
#include <unknwn.h>
 
Interface A : public IUnknown
{
STDMETHOD(Function1)(long lParam) PURE;
STDMETHOD(Function2)(long lParam1, long lParam2) PURE;
STDMETHOD(Function3)(long* plParam1) PURE;
STDMETHOD(Function4)(long* plParam1, long plParam2) PURE;
};
 
모든 인터페이스는 IUnknown 인터페이스를 상속받아야 하므로 어떤 인터페이스를 구현한다는 것은 기본적으로 IUnknown 인터페이스의 세 가지 멤버함수를 구현해야 함을 의미한다. 클라이언트가 컴포넌트로부터 어떠한 인터페이스를 얻든지 간에 그 인터페이스는 IUnknown 인터페이스를 상속받으므로 IUnknown의 세 가지 멤버함수를 반드시 갖게 된다.
 

QueryInterface()

인터페이스 식별자인 IID in 파라미터(클라이언트가 컴포넌트에게 전달하는 파라미터)로 받아서 이와 일치하는 인터페이스를 컴포넌트가 제공하는지를 살펴보고, 만약 제공한다면 그 인터페이스에 대한 포인터를 두 번째 파라미터인 ppv out 파라미터(컴포넌트가 클라이언트에게 전달하는 파라미터)로 넘겨준다.
함수의 이름이 그 기능을 암시하듯이 IID를 조회(Query)해서 일치하는 인터페이스의 포인터를 넘겨주는 것이다.
 
다음 코드는 A B 두 개의 인터페이스를 가지고 있는 컴포넌트로부터 인터페이스 A를 얻고 인터페이스 A로부터 QueryInterface() 를 호출하여 인터페이스 B를 얻는 예를 보여준다.
#include FirstKiss.h
 
int main()
{
IA* PA = NULL;
IB* PB = NULL;
CoInitialize(NULL);
CoCreateInstance(CLSID_FirstKiss, NULL, CLSCTX_SERVER, IID_A /* 인터페이스 A IID */, (void**)&PA /* 인터페이스 A의 포인터 */);
 
// 인터페이스 A 사용
 
PA->QueryInterface(IID_B /* 인터페이스 B IID */, (void**)&PB /* 인터페이스 B의 포인터 */);
 
// 인터페이스 B 사용
 
CoUninitialize();
}
CoCreateInstance()를 통해 인터페이스 A의 포인터를 얻게 되면 A를 통해 QueryInterface()를 호출하여 인터페이스 B의 포인터를 얻을 수 있다. 이런 식으로 컴포넌트가 제공하는 인터페이스 중 하나만 가지고 있다면 QueryInterface()를 통해 다른 모든 인터페이스를 얻을 수 있는 것이다.
 

AddRef() / Release()

컴포넌트의 생명을 관리한다.
AddRef(): Add Reference Count의 줄임말로 참조 카운트(Reference Count)를 증가시키라는 의미이다. 참조 카운트란 몇 명의 클라이언트가 인터페이스를 사용하고 있는지를 의미하는 변수이다. 그러나 참조 카운트가 반드시 그 인터페이스를 사용중인 클라이언트 수와 일치하는 것은 아니다. 왜냐하면 동일한 클라이언트가 AddRef() 를 여러 번 호출할 수 있기 때문이다. 아무튼 AddRef() 를 호출하면 참조카운트가 하나씩 증가되고, 반대로 Release() 를 호출하면 참조 카운트가 하나씩 감소된다. 참조카운트가 하나씩 감소해서 0이 되면 아무도 인터페이스를 사용하지 않는다고 보고, 컴포넌트는 스스로 소멸시켜야 한다. Release()는 컴포넌트를 소멸시키는 코드를 구현하는 곳이다.
 
컴포넌트 상에서 IUnknown 인터페이스를 어떻게 구현하는지 살펴보자. 다음 코드는 인터페이스 A, B를 구현한 클래스를 보여준다.
class CFirstKiss : public A, public B
{
public:
ULONG m_lReferenceCount; // 참조 카운트
CFirstKiss(){m_lReferenceCount = 0;}
~CFirstKiss();
 
// IUnknown
ULONG STDMETHODCALLTYPE AddRef(){return ++ m_lReferenceCount;}
ULONG STDMETHODCALLTYPE Release()
{
  if(--m_lReferenceCount == 0)
    delete this;// 컴포넌트 소멸
  return m_lReferenceCount;
}
STDMETHOD(QueryInterface)(const IID& iid, void** ppv)
{
  if(iid == IID_IUnknown) || (iid == IID_A))
    *ppv = (A*)this;
  else if(iid = IID_B)
    *ppv = (B*)this;
  else
  {*ppv = NULL;return E_NOINTERFACE;}
  AddRef();
 
  return S_OK;
}
// 인터페이스 A 멤버함수 구현 생략
// 인터페이스 B 멤버함수 구현 생략
};
 
 

IClassFactory 인터페이스

IClassFactory 인터페이스는 IUnknown 인터페이스와 마찬가지로 컴포넌트가 반드시 제공해야 하는 인터페이스이다. 클래스 팩토리라는 이름을 클래스를 만드는 공장이란 뜻으로 해석할 때 IClassFactory 인터페이스는 컴포넌트를 만드는 일을 할 것으로 예상할 수 있다. 앞에서 컴포넌트를 생성하기 위해서는 CoCreateInstance()를 호출해야 한다고 하였다. 하지만 CoCreateInstacne()는 실질적으로 컴포넌트를 생성하지는 않는다. 대신 컴포넌트로부터 IClassFactory() 인터페이스를 얻어서 이를 통해 컴포넌트를 생성하는 것이다. 컴포넌트로부터 인터페이스를 얻은 뒤 컴포넌트를 생성한다고 하니 무엇인가 앞뒤가 맞지 않는다고 생각될 것이다. 컴포넌트가 만들어지지도 않았는데 어떻게 컴포넌트로부터 IClassFactory 인터페이스를 얻을 수 있단 말인가?
 
unknwn.h 파일에 정의된 IClassFactory 인터페이스
Interface IClassFactory : public IUnknown
{
virtual HRESULT _stdcall CreateInstance(IUnknown* pUnkOuter, const IID& iid, void** ppv) = 0;
virtual HRESULT _stdcall LockServer(BOOL fLock) = 0;
};
 

IClassFactory::CreateInstance()

IClassFactory::CreateInstance() CoCreateInstance() 2,4,5번째 매개변수를 그대로 넘겨 받는다.
2번째 매개변수인 pUnkOuter 는 컴포넌트 내부에 또 다른 컴포넌트를 생성하여 두 컴포넌트가 마치 하나의 컴포넌트인 것처럼 바깥쪽 컴포넌트를 통해 안쪽 컴포넌트가 보유하고 있는 인터페이스를 제공하고자 할 때 바깥쪽 컴포넌트의 IUnknown 포인터를 의미한다. 이를 두고 바깥쪽 컴포넌트가 안쪽 컴포넌트를 집합체화(Aggregating) 하였다고 한다. 어떤 컴포넌트가 다른 컴포넌트를 집합체화 하려면 CoCreateInstance()를 호출할 때 두 번째 매개변수에 자신의 IUnknown 포인터를 전달하면 된다.
 
바깥쪽 컴포넌트를 다른 말로 Aggregator 라고 하고, 안쪽 컴포넌트를 Aggregatee 라고도 한다. 일반적인 클라이언트는 집합체화 할 일이 없으므로 CoCreateInstance() 함수의 pUnkOuter 매개변수에는 NULL 값을 넣어주면 된다.
 
IClassFactory::CreateInstance() 에서 pUnkOuter 외 나머지 두 매개변수는 QueryInterface() 의 매개변수와 동일하므로 넘어간다. 그런데 여기서 한가지 주목할 점은 IClassFactory::CreateInstance() CLSID를 파라미터로 요구하지 않는다는 것이다. IClassFactory::CreateInstance() 는 컴포넌트를 생성하기 위한 목적을 가지고 있는데, CLSID 없이 어떤 컴포넌트를 생성해야 하는지를 안다는 것은 컴포넌트와 클래스 팩토리가 한 쌍으로 묶여 있음을 암시한다. 그 이유에 대해서는 다음에 살펴볼 것이다. 어떻게 보면 클래스 팩토리는 메인 컴포넌트를 생성하기 위한 보조 컴포넌트라고 볼 수 있다. 이 보조 컴포넌트는 오직 하나의 인터페이스, IClassFactory만을 갖고 있기 때문에 비교적 작고 간단한 코드로 만들어 질 것이다.
 

IClassFactory::LockServer()

IClassFactory::LockServer()는 컴포넌트의 참조 카운트가 0이 되었을 때 EXE 컴포넌트의 경우 프로세스가 종료되거나 DLL 컴포넌트의 경우 DLL이 메모리에서 언로드 되는 것을 방지하기 위해 서버에 자물쇠를 걸고자 할 때 쓰인다. LockServer() 는 파라미터로 TRUE 또는 FALSE 값을 받는데, TRUE이면 서버에 자물쇠를 거는 것이고, FALSE이면 자물쇠를 푼다는 의미가 된다.
 
EXE 컴포넌트의 경우 서버의 생명을 스스로 관리해야 하는데, 이를 위해 컴포넌트의 인스턴스 카운트와 LockServer() 에서 쓰는 락 카운트를 항상 유심히 살펴보아야 한다. EXE 서버 모듈은 이 두 변수의 상태를 수시로 체크해서 자신의 소멸 시점을 스스로 결정해야 한다. 하지만 DLL 컴포넌트의 경우 클라이언트가 서버의 생명을 관리할 모든 권한을 갖기 때문에 EXE 컴포넌트처럼 스스로 서버 모듈을 소멸시킬 필요는 없다. 그러나 클라이언트에게 소멸시켜도 무방한 시점이 언제인지 알려줄 의무는 있다.
 
CoFreeUnusedLibraries() 는 클라이언트에게 DLL 서버 모듈을 효과적으로 소멸시킬 수 있는 방법을 제공한다. 클라이언트가 CoFreeUnusedLibraries()를 호출하면 이 함수는 프로세스에 로드된 DLL 서버 모듈에게 소멸시켜도 무방한지 묻고 그 응답이 참인 경우 CoFreeLibrary() 를 사용하여 서버 모듈을 메모리상에 해제시킨다.
 
CreateInstance() 도 그렇지만 LockServer() 도 실제로는 클라이언트가 직접 호출할 일이 없으므로 (CoCreateInstance()가 대신 호출한다.) 그냥 이런 기능을 가진 함수가 잇다는 정도만 알아도 컴포넌트를 사용하는데 크게 지장이 없다.
 
클래스 팩토리 구현
class CFirstKiss : public A, public B{ /* 중간 생략 */ };
ULONG g_lLockCount = 0;
CFirstKiss* g_pFirstKiss = NULL;
 
class CFactory : public IClassFactory
{
// IUnknown 구현 코드 생략
// IClassFactory
STDMETHOD(CreateInstance)(IUnknown* pUnkOuter, const IID& iid, void** ppv)
{
  g_pFirstKiss = new CFirstKiss;// 실제 컴포넌트 생성
  HRESULT hr = g_pFirstKiss->QueryInstance(iid, ppv);
  return hr;
}
STDMOTHOD(LockServer)(BOOL bLock)
{
  if(bLock)
    ++g_lLockCount;
  else
    --g_iLockCount;
  return S_OK;
}
};
 
 

DLL 컴포넌트 시작과

CoCreateInstance()는 컴포넌트가 제공하는 IClassFactory 인터페이스를 통해서 실질적으로 컴포넌트를 생성한다고 할 때 컴포넌트로부터 IClassFactory 인터페이스를 어떻게 얻을 수 있는지 의문이 생긴다.
CoCreateInstance() IClassFactory 인터페이스를 구현한 일종의 보조 컴포넌트, 클래스 팩토리 객체를 생성해야 한다. 따라서 클래스 팩토리를 생성하고 그로부터 IClassFactory 인터페이스를 제공하기 위해서 DLL 컴포넌트는 이러한 목적을 가진 함수를 내보내기 시켜야한다.
 
DLL 컴포넌트의 내보내기 함수
멤버함수
내 용
DllGetClassObject
클래스 팩토리를 생성하고 IClassFactory 인터페이스를 전달한다.
DllCanUnloadNow
DLL을 소멸시켜도 무방한지를 S_OK 또는 S_FALSE 값을 통해 알린다.
DllRegisterServer
레지스트리에 컴포넌트를 등록한다.
DllUnregisterServer
레지스트리에 등록했던 정보를 지운다.
 

CoCreateInstance()

는 첫 번째 매개변수로 넘어온 CLSID 를 가지고서 레지스트리를 검색하여 컴포넌트 파일에 대한 경로를 얻고 클라이언트 메모리 상에 서버 모듈을 로드시킨다.
CoCreateInstance()는 서버 모듈의 내보내기 함수 DllGetClassObject 를 호출하여 클래스 팩토리를 생성하고, IClassFactory 인터페이스를 얻게 된다
그런 다음 IClassFactory::CreateInstance() 를 호출하여 CLSID가 의미하는 실질적인 컴포넌트를 생성하고 인터페이스 포인터를 얻어서 클라이언트에게 전달하는 것이다.
 
다음은 DLL 컴포넌트에서 DllGetClassObject 내보내기 함수를 구현한 예를 보여준다.
class CFactory : public IClassFactory{};
 
HRESULT DllGetClassObject(CLSID& clsid, IID& iid, void** ppv){
if(clsid == CLSID_FirstKiss)
{
  CFactory* pFactory = new CFactory;
  HRESULT hr = pFactory->QueryInterface(iid, ppv);
  // iid IID_IClassFactory일 것이다.
  return hr;
}
else
  return CLASS_E_CLASSNOTAVAILABLE;
}
 
 

CoFreeUnusedLibraries()

는 클라이언트 메모리 상에 로드된 각각의 DLL 서버 모듈에 대해 DllCanUnloadNow 내보내기 함수를 호출하여 DLL을 소멸시켜도 무방한지 여부를 판단한다.
DllCanUnloadNow() 는 락 카운트 같이 서버에 존재하는 여러 변수들을 조사하여 서버 모듈을 소멸시켜도 좋은지 판단하고 그 결과를 리턴한다.
 
다음은 DLL 컴포넌트에서 DllCanUnloadNow() 를 구현한 예를 보여준다.
ULONG g_lComponent = 0;
ULONG g_lLockCount = 0;
 
HRESULT DllCanUnloadNow() {
if((g_lLockCount == 0) && (g_lComponents == 0)) {
  return S_OK;
else
  return S_FALSE;
}
 
앞에서 CoCreateInstance() 는 첫 번째 매개변수로 CLSID를 받고, 이와 일치하는 키 값을 레지스트리 HKCR\CLSID 밑에서 찾아 컴포넌트 파일의 경로를 알아낸다고 했다. 그럼 누가 레지스트리에 이러한 정보를 기록할 것인가?
DllRegisterServer() 내보내기 함수가 그 일을 맡는다. DllRegisterServer()가 하는 일은 너무나 명백하기 때문에 굳이 예를 들지 않더라도 어떠한 코드가 들어갈 것이라고 짐작할 수 있다. 우리는 RegCreateKeyEx() RegSetValueEx()를 사용하여 원하는 키를 만들고 원하는 이름값을 넣을 수 있다.
 

DllRegisterServer()

는 컴포넌트를 사용하기 이전 시점에 딱 한 번만 호출하면 된다. Regsvr32.exe 프로그램은 DLL 서버 모듈을 로드시키고, DllRegisterServer()를 호출해 주는 역할을 한다. 만약 여러분이 FirstKiss.dll 이라는 컴포넌트 파일을 등록시키고자 한다면 실행 윈도우에 다음과 같은 명령어를 입력하면 된다.
 
Regsvr32 firstkiss.dll
 

DllUnregisterServer()

내보내기 함수는 DllregisterServer()의 호출로 인해서 레지스트리에 기록된 정보를 삭제하기 위해 존재한다. 레지스트리에 이미 등록된 컴포넌트 파일을 삭제하려고 할 때는 먼저 DllUnregisterServer() 함수를 호출하여 레지스트리에 기록한 내용을 반드시 지워야 한다. 만약 이렇게 하지 않는다면 이미 레지스트리에 기록된 정보를 일일이 찾아다니며 지우지 않는 한 깨끗하게 지울 도리가 없다.
 
Regsvr32.exe 프로그램을 이용하며 DllUnregisterServer()를 간단하게 호출할 수 있다. 실행 윈도우에 다음과 같은 명령어를 입력하면 FirstKiss.dll 컴포넌트의 DllUnregisterServer() 내보내기 함수가 호출된다.
 
Regsvr32 u firstkiss.dll
 

FirstKiss라는 DLL 컴포넌트의 등록에서부터 사용, 삭제까지 모든 과정

1.        실행 윈도우에서 Regsvr32 firstkiss.dll 을 실행시키고 FirstKiss 컴포넌트를 레지스트리에 등록한다.
2.        클라이언트는 FirstKiss 컴포넌트를 생성하기 위해 CoCreateInstance() 를 호출한다.
3.        CoCreateInstance()는 첫 번째 매개변수로 전달된 CLSID를 가지고 일치하는 키를 레지스트리에서 찾고 FirstKiss 컴포넌트 파일의 경로를 알아낸다.
4.        CoCreateInstance() FirstKiss 컴포넌트 파일의 경로를 가지고 CoLoadLibrary()를 호출하여 DLL 서버 모듈을 클라이언트 메모리 상에 로드시킨다.
5.        CoCreateInstance()는 클래스 팩토리를 얻기 위해 내부적으로 CoGetClassObject()를 호출하고, CoGetClassObject() FirstKiss 컴포넌트의 내보내기 함수 DllGetClassObject()를 다시 호출하여 클래스 팩토리를 얻게 된다.
6.        CoCreateInstance() IClassFactory::CreateInstance() 를 호출하여 실제 컴포넌트를 생성하고, CoCreateInstance()의 네번째 매개변수로 전달된 IID를 조회해서 (QueryInterface()를 호출한다) 그 인터페이스에 대한 포인터를 클라이언트에게 전달한다.
7.        클라이언트는 FirstKiss 컴포넌트가 제공하는 인터페이스를 통해 원하는 작업을 수행한다.
8.        인터페이스 사용 후 Release()를 호출한 다음 CoFreeUnusedLibraries()를 호출하여 DLL 서버 모듈을 소멸시킨다.
9.        CoFreeUnusedLibraries() FirstKiss 컴포넌트의 내보내기 함수 DllCanUnloadNow()를 호출하고 S_OK 값을 리턴받으면 CoFreeLibrary()를 다시 호출하여 DLL 서버모듈을 클라이언트 메모리 상에서 해제시킨다.
10.    FirstKiss 컴포넌트가 더 이상 필요 없게 되서 컴포넌트 파일을 삭제하고자 할 경우 먼저 실행 윈도우에서 regsvr32 u firstkiss.dll 을 실행하여 레지스트리에 등록된 정보를 삭제한다.
11.    컴포넌트 파일을 삭제한다.
 
 

EXE 컴포넌트의 시작과

CoCreateInstance()는 내부적으로 첫 번째 매개변수로 전달된 CLSID를 가지고 레지스트리를 검색하여 일치하는 키를 찾고 그 키 밑에 기록된 컴포넌트 파일 경로명을 바탕으로 컴포넌트 서버 타임이 무엇인지 판별한다. 서버 타입은 다음과 같이 3종류로 분류된다.
 
서버 타입
설명
Inprocess 서버
클라이언트와 같은 프로세스상에 기동되는 컴포넌트를 Inprocess 서버 라 한다. 일반적으로 DLL컴포넌트는 Inprocess 서버로 보면 된다. 대표적인 Inprocess 서버로는 ActiveX 컨트롤과 ADO 등이 있다.
Local 서버
클라이언트와 같은 컴퓨터 상에 존재하나 다른 프로세스에서 기동되는 컴포넌트를 Local 서버라 한다. 프로세스가 다르다 보니 IPC(Inter Process Call) 메커니즘이 추가적으로 필요하다. 속도면에서 Inprocess 서버보다 뒤떨어지나 클라이언트로부터 독립된 프로세스 상에 기동되므로 클라이언트의 내부 오류로부터도 독립되는 장점이 있다. 예로 IIS 서버가 같은 컴퓨터상에 존재하는 MTS 컴포넌트를 사용할 때 MTS (혹은 COM+) 컴포넌트는 Local 서버이다.
Remote 서버
클라이언트와 다른 컴퓨터상에 기동되는 컴포넌트를 Remote 서버라 한다. Local 서버와 비교할 때 IPC 매커니즘 외에 네트워크 통신 메커니즘이 추가로 필요하다. 따라서 속도면에서는 가장 느리지만 클라이언트 컴퓨터로부터 독립된 컴퓨터상에 기동되므로 클라이언트 컴퓨터의 오류로부터도 독립되는 장점이 있다. IIS 서버가 다른 컴퓨터 상에 존재하는 MTS 컴포넌트를 사용할 때 MTS 컴포넌트는 Remote 서버이다.
 
CoCreateInstance()는 레지스트리를 검색하여 HKCR\CLSID\{CLSID 문자열} 키 밑에 InprocServer32 키가 있으면 Inprocess 서버로 간주하고, LocalServer32 키가 있다면 Local 서버로 간주한다.
EXE 컴포넌트의 경우 CoCreateInstance() LocalServer32 키 안에 들어 있는 경로명을 가지고 EXE 서버 모듈을 기동시킬 것이다.
CoCreateInstance()는 내부적으로 CoGetClassObject()를 호출하여 클래스 팩토리를 얻으려 하는데 이때 EXE 컴포넌트는 DLL 컴포넌트와 달리 DllGetClassObject() 내보내기 함수가 없기 때문에 CoGetClassObject() DllGetClassObject()를 호출할 수 없다. 클래스 팩토리를 CoGetClassObject()에게 전달하기 위해서 EXE 컴포넌트는 CoRegisterClassObject() 를 호출하게 된다.
 
다음은 CoRegisterClassObject() 를 사용하여 클래스 팩토리를 클라이언트에게 전달하는 EXE 컴포넌트의 코드이다.
class CFactory : public IClassFactory{ /* 중간생략 */ };
 
int main(int argc, char* argv[])
{
CoInitialize(NULL);
CFactory* pFactory = new CFactory;
IUnknown* pUnk = NULL;
pFactory->QueryInterface(IID_IUnknown, &pUnk);
DWORD dwKey = NULL;
/* EXE 컴포넌트는 DllGetClassObject()를 구현하는 대신 CoRegisterClassObject()를 호출해야 한다. */
CoRegisterClassObject(CLSID_FirstKiss, pUnk, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwKey);
MSG msg;
while(GetMessage(&msg,0,0,0))
  DispatchMessage(&msg);
 
// EXE 컴포넌트는 기본적으로 메시지 펌프를 가지고 있어야 한다.
CoRevokeClassObject(dwKey);
pUnk->Release();
CoUninitialize();
return 0;
}
 
DLL 컴포넌트를 레지스트리에 등록시키기 위해서는 regsvr32.exe 프로그램을 사용하면 됐으나 EXE 컴포넌트를 등록시키기 위해서는 regsvr32.exe 프로그램을 사용할 수 없다. 왜냐하면 EXE 컴포넌트는 DllRegisterServer() 내보내기 함수를 가지고 있지 않기 때문이다. 대신 RegServer 시작 옵션을 주고 EXE 서버 모듈을 실행시키면 컴포넌트 등록 작업이 수행된다.
 
다음은 실행 윈도우에서 EXE 형태의 FirstKiss 컴포넌트를 등록시키기 위한 명령어를 보여준다.
Firstkiss regserver
 
컴포넌트를 레지스트리에서 삭제하려면 UnregServer 시작옵션을 주고 EXE 서버 모듈을 실행시키면 된다. EXE 컴포넌트는 시작 옵션을 구문 분석하여 컴포넌트를 등록시키거나 삭제하는 작업을 처리해야 할 의무가 있다.
 
class CFactory : public IClassFactory{};
 
int main(int argc, char* argv[])
{
if(lstrcmpi(argv[1], -UnregServer) == 0)
{
  UnregisterServer();// 컴포넌트 삭제를 수행하는 함수
  return 0;
}
if(lstrcmpi(argv[1], -RegServer) == 0)
{
  RegisterServer();// 컴포넌트 등록을 수행하는 함수
  return 0;
}
 
CoInitialize(NULL);
CFactory* pFactory = new CFactory;
// 클래스 팩토리 등록 및 메시지 펌프 코드 생략
CoUninitialize();
return 0;
}
 
 
서버 모듈이 DLL이냐 EXE 이냐에 따라 코드상 구현 방법에 있어서 조금씩 차이가 있음을 알 수가 있다. 이러한 코드상에 차이를 정리하면 DLL 서버의 4개의 내보내기 함수가 EXE 서버의 main() 안으로 옮겨졌다고 할 수 있다.
그러나 그외 나머지 부분, 클래스 팩토리를 구현한 CFactory 클래스와 인터페이스를 구현한 CFirstKiss 클래스는 DLL 서버와 EXE 서버에서 동일하게 사용할 수 있다.
 

EXE 형태의 FirstKiss 컴포넌트의 등록에서부터 사용, 삭제까지 모든 과정

1.        실행 윈도우에서 firstkiss regserver 명령으로 FirstKiss 컴포넌트를 레지스트리에 등록시킨다.
2.        클라이언트는 FirstKiss 컴포넌트를 생성하기 위해 CoCreateInstance()를 호출한다.
3.        CoCreateInstance()는 첫 번째 매개변수로 전달된 CLSID를 가지고 일치하는 키를 레지스트리에서 찾고, FirstKiss 컴포넌트 파일의 경로를 알아낸다.
4.        CoCreateInstance() FirstKiss 컴포넌트 파일의 경로를 바탕으로 CreateProcess()를 사용하여 EXE 서버 모듈을 기동시킨다.
5.        CoCreateInstance()는 클래스 팩토리를 얻기 위해 내부적으로 CoGetClassObject()를 호출하는데, 4번 과정에서 FirstKiss 컴포넌트가 기동될 때 CoRegisterClassObject()를 호출했다면 클래스 팩토리를 성공적으로 얻을 수 있다.
6.        CoCreateInstance()는 클래스 팩토리의 CreateInstance()를 호출하여 실제 컴포넌트를 생성하고, CoCreateInstance() 4번째 매개변수로 전달된 IID를 조회해서(QueryInterface()를 호출한다는 의미) 그 인터페이스에 대한 포인터를 얻고 클라이언트에게 전달한다.
7.        클라이언트는 FirstKiss 컴포넌트가 제공하는 인터페이스를 통해 원하는 작업을 한다.
8.        인터페이스 사용 후 클라이언트가 Release() 를 호출하면 EXE 컴포넌트는 서버 모듈이 잠기지 않았을 경우 (IClassFactory:LockServer(TRUE)가 호출되지 않았을 경우) 카운터 변수를 조사하여 프로세스를 종료시킨다.
9.        FirstKiss 컴포넌트를 다시 사용하고자 할 경우 2~8까지의 과정이 반복된다.
10.    FirstKiss 컴포넌트가 더 이상 필요 없게 되어서 컴포넌트 파일을 삭제하고자 할 경우 먼저 실해 윈도우에서 firstkiss unregserver 명령으로 레지스트리에 등록된 컴포넌트를 삭제한다.
11.    컴포넌트 파일을 삭제한다.
 
 

클라이언트와 서버간의 통신

클라이언트 입장에서는 서버 모듈이 EXE DLL이냐에 상관없이 컴포넌트를 사용하는데 있어서 코드상에 차이가 없다. 클라이언트는 단지 CoCreateInstance()를 호출하여 인터페이스 포인터를 얻고, 이것을 사용한 후에 Release()를 호출해 주면 되는 것이다. 클라이언트는 컴포넌트가 DLL인지 EXE인지 혹은 같은 컴퓨터 상에 존재하는지 다른 컴퓨터 상에 존재하는지 따위의 문제와는 상관없이 다음과 같은 동일한 코드를 사용할 수 있다.
 
CoInitialize(NULL);
CLSID clsid;
CLSIDFromProgID(LFirstKiss.1, &clsid);
// FirstKiss 컴포넌트의 ProgID FirstKiss.1이라고 가정한다.
CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_A, (void**)&pA);
// CLSCTX_SERVER 플래그를 사용하는 것에 주목하자
// 인터페이스 A 사용
pA->Release();
CoUninitialize();
 
클라이언트 코드에서는 컴포넌트의 위치와 서버 타입에 관한 정보는 전혀 나타나 있지 않다. 비록 클라이언트 코드상에 이러한 정보가 없더라도 COM 서버 시스템은 레지스트리에 등록된 내용을 바탕으로 이러한 정보를 찾을 수 있다.
CoCreateInstance()는 레지스트리에 등록된 정보를 바탕으로 서버의 타입(Inprocess, Local, Remote)을 알아내고 각각의 상황에 맞게 컴포넌트를 생성한다. 레지스트리에 기록된 정보를 보면 컴포넌트가 로컬에서 동작하는지, 아니면 리모트에서 동작하는지, 서버 타입은 무엇인지 따위의 정보를 정확히 유추해 볼 수 있다. 컴포넌트와 레지스트리의 상호관계를 자세히 알고 있는 사람은 레지스트리의 내용을 조금만 바꿔주는 것만으로 클라이언트 프로세스 상에 동작하던 컴포넌트를 원격 컴포넌트에서 동작하게 할 수 있다.
 
DLL 컴포넌트의 생성과정을 보면
CoCreateInstance() CoGetClassObject()를 호출하고,
CoGetClassObject() DllGetClassObject()를 호출하고,
DllGetClassObject()는 클래스 팩토리 객체를 생성하여 그 포인터를 CoGetClassObject()에게 넘겨주고,
CoGetClassObject() CoCreateInstance()에게 넘겨준다.
 
따라서 CoCreateInstance()가 전달받은 클래스 팩토리의 포인터가 DllGetClassObject()에서 생성했던 클래스 팩토리의 포인터와 동일하다는 것은 의심의 여지가 없다.
 
그러나 EXE 컴포넌트의 경우에는
CoRegisterClassObject()에 전달된 클래스 팩토리의 포인터는 CoGetClassObject()가 받게 되는 포인터와 서로 일치하지 않다.
상식적으로 그럴 수 밖에 없는 것이
CoRegisterClassObject()를 호출하는 곳은 서버 프로세스이고,
CoGetClassObject()를 호출하는 곳은 클라이언트 프로세스이므로
만약 포인터 값이 같다면
서버 프로세스에서 생성된 이 포인터는
클라이언트 프로세스에서는 단지 Access Violation 에러만을 발생시키게 될 것이다.
 
그럼 클라이언트 프로세스에서 CoGetClassObject()가 받게 되는 클래스 팩토리 포인터의 정체는 무엇인가? 한 가지 분명한 것은 서버 프로세스에서 CoRegisterClassObject()를 호출할 때 전달됐던 클래스 팩토리는 아니라는 것이다. 이 포인터는 서버가 아니라 클라이언트상에 존재하는 클래스 팩토리를 가르키는 포인터이다.
그런데 신기한 것은 이 포인터를 사용하여 그 멤버함수를 호출하면 서버 프로세스에 있는 클래스 팩토리의 멤버함수가 호출되어서 실행된다는 것이다. 클라이언트는 이 정체 불명의 포인터를 통해 서버 프로세스에 있는 클래스 팩토리에 간접적으로 접근을 하고 있는 것이다. 이런 상황을 놓고 봤을 때, 클라이언트 프로세스에 있는 포인터는 서버 프로세스에 있는 클래스 팩토리에 대한 대리자(Proxy)가 되는 셈이다. 이 포인터와 같이 클라이언트 프로세스에 존재하면서 서버 프로세스에 존재하는 인터페이스에 접근을 가능하게 하는 포인터를 인터페이스 프록시라 한다.
 

인터페이스 프록시(인터페이스 대리자) 대한 정리.

1.        CoGetClassObject()를 호출하면 IClassFactory 인터페이스에 대한 포인터를 얻을 수 있다.
2.        CoGetClassObject()를 호출하는 동일한 클라이언트 코드에 대해 DLL 컴포넌트의 경우 실제 클래스 팩토리의 포인터를 얻지만 EXE 컴포넌트의 경우 실제 클래스 팩토리의 포인터가 아닌 클래스 팩토리 프록시 포인터를 얻게 된다.
3.        클라이언트 코드상에 IClassFactory* pFactory 라고 선언하고 이를 가지고 CoGetClassObject()를 호출하면 pFactory 에 값을 얻게 되는데 이 값이 실제 클래스 팩토리의 포인터인지 클래스 팩토리 프록시의 포인터인지 코드를 보고서는 구분 할 수 없다.
4.        클라이언트 코드상에서 pFactory 의 멤버함수를 호출할 때 이것이 실제 클래스 팩토리의 멤버함수를 호출하는지 클래스 팩토리 프록시의 멤버함수를 호출하는지 코드를 보고서는 알 수 없다.
5.        클라이언트 코드상에 존재하는 모든 인터페이스 포인터에 대해 이것이 실제 인터페이스 포인터인지 아니면 인터페이스 프록시 포인터인지 알 수 없고 이는 런타임시에 결정된다.
6.        런타임시 얻게 되는 IClassFactory 인터페이스 프록시는 실제 IClassFactory 인터페이스의 멤버함수와 동일한 멤버함수들을 갖고 있다. 만약 그렇지 않다면 런타임시 IClassFactory 인터페이스 프록시의 멤버함수가 호출될 때마다 에러가 발생할 것이다.
7.        클라이언트상에 존재하는 IClassFactory 프록시의 멤버함수에게 전달되는 매개변수는 서버 프로세스상에 존재하는 실제 IClassFactory 인터페이스의 멤버함수에게 그대로 전달되어야 할 것이다.
8.        인터페이스 프록시는 클라이언트 프로세스로부터 서버 프로세스로 매개변수를 전달하고 함수를 호출하는 IPC 통신을 한다고 예상할 수 있다.
9.        일반적으로 클라이언트가 다른 프로세스 혹은 다른 컴퓨터 상에 컴포넌트를 생성하고 인터페이스 프록시를 통해 멤버함수를 호출할 때 IPC 통신이 일어난다.
 
클라이언트는 인터페이스 프록시를 통해서 서버와 통신한다고 했는데
사실 서버도 무엇인가를 통해서 클라이언트의 인터페이스 프록시와 통신한다.
서버 프로세스에 존재하면서 클라이언트의 인터페이스 프록시와 통신하는 무언가를 인터페이스 스텁(Stub)이라 한다. 클라이언트가 인터페이스 프록시를 통해서 파라미터를 보낼 때 서버는 인터페이스 스텁을 통해서 파라미터를 받고 실제 멤버함수를 호출한다. 때로는 반대로 인터페이스 스텁이 파라미터를 보내기도 하는데(out 파라미터의 경우), 그럴 경우에 클라이언트는 인터페이스 프록시를 통해 파라미터를 전달받는다.
 
클라이언트 프로세스
IPC 통신
서버프로세스
IClassFactory Proxy
CreateInstance()
LockServer()
 
IClassFactory Stub
CreateInstance() (재호출)
 
IClassFactory
CreateInstance()
LockServer()
 

인터페이스 정의 언어

지금까지는 인터페이스를 정의할 때 C++ 문법을 사용하였는데, 원래는 인터페이스를 정의하기 위한 언어, 즉 인터페이스 정의 언어가 존재한다. 인터페이스 정의 언어(Interface Description Language), IDL 이라 불리는 언어를 사용해서 인터페이스를 정의하면 인터페이스 프록시, 스텁을 손쉽게 만들 수가 있다. IDL C++ 와 매우 유사한 문법을 가지고 있어서 IDL을 사용하여 인터페이스를 정의한 코드를 보면 마치 C++로 작성된 헤더파일을 보는 것 같은 느낌이 들 것이다. Unknwn.i이 파일은 IUnknown 인터페이스에 대해 다음과 같이 정의하고 있다.
 
[
local,
object,
uuid(000000000-0000-00000-C0000-0000000000046),
pointer_default(unique)
]
interface IUnknown
{
HRESULT QueryInterface(
  [in] REFIID riid,
  [out, iid_is(riid)] void **ppvObject);
ULONG AddRef();
ULONG Release();
}
 
[]안에 들어있는 정보를 특성(Attribute) 이라 한다.
IDL 코드를 보면 interface 키워드와 인터페이스 멤버함수의 각 파라미터 앞에 특성이 선언되었음을 알 수 있다.
 
특성
내용
object
인터페이스가 COM 인터페이스임을 나타낸다.
uuid
GUID 식별자를 지정한다.
in
클라이언트에서 서버로 전달되는 파라미터임을 나타낸다.
out
서버에서 클라이언트로 전달되는 파라미터임을 나타낸다.
 
 

IDL 컴파일러

Visual Studio.NET 개발 환경은 IDL 파일을 컴파일 하기 위해 midl.exe 컴파일러를 제공한다. 가령 MyInterface.idl 파일을 컴파일 하려면
Visual Studio.NET 명령 프롬프트를 실행시키고
작업 디렉토리로 이동한 뒤 midl myinterface.idl" 명령을 입력하면 된다.
 
MIDL 컴파일러를 사용하여 myinterface.idl 파일을 컴파일하면 3개의 C 소스 파일과 1개의 헤더파일이 생성된다.
 
파일 이름
내용
myinterface.h
C C++ 문법으로 인터페이스를 정의한 헤더파일이다.
dlldata.c
프록시/스텁 DLL에 필요한 구현 코드를 담고 있는 파일이다.
myinterface_i.c
IID CLSID 등 식별자 정보를 담고 있는 파일이다.
myinterface_p.c
인터페이스 프록시와 인터페이스 스텁에 대한 코드를 담고 있는 파일이다.
 
myinterface.h 헤더파일과 3개의 C 소스 파일을 Visual C++ 컴파일러로 컴파일한 다음 링크시키면, myinterface.idl 파일에 정의된 IMyInterface 인터페이스에 대한 프록시/스텁이 구현된 DLL이 만들어진다.
3개의 C 소스 파일을 컴파일, 링크시키기 위해서는 def 파일과 mk 파일을 작성해야 하는데 다음과 같이 생성한다.
 
MyInterface.def 파일
LIBRARY                          myinterfacePS
 
DESCRIPTION                   Proxy/Stub DLL
 
EXPORTS
                           DllGetClassObject           @1         PRIVATE
                           DllCanUnloadNow             @2         PRIVATE
                           GetProxyDllInfo                @3         PRIVATE
                           DllRegisterServer             @4         PRIVATE
                           DllUnregisterServer          @5         PRIVATE
 
MyInterface.mk 파일
myinterfaceps.dll: dlldata.obj myinterface_p.obj myinterface_i.obj link /dll /out:myinterfaceps.dll /def:myinterfaceps.def \/entry:DllMain dlldata.obj myinterface_p.obj myinterface_i.obj \kernel32.lib rpcndr.lib rpcns4.lib rpcrt4.lib oleaut32.lib \uuid.lib \
 
.c.obj:
                           cl /c /0x /DWIN32 \
             /DREGISTER_PROXY_DLL $<
 
이제 명령 프롬프트 창에 nmake myinterface.mk 를 실행시키면 프록시/스텁 DLL이 만들어진다. 이렇게 해서 만들어진 프록시/스텁 DLL을 런타임시 사용하려면 먼저 레지스트리에 등록시켜야 한다. 다행히 프록시/스텁 DLL DllRegisterServer 내보내기 함수를 가지고 있으므로 regsvr32.exe 프로그램을 통해 쉽게 등록 시킬 수 있다.
 
레지스트리 HKCR\Interfaces 키 밑에는 인터페이스 식별자를 이름으로 갖는 수많은 키가 존재하는데 각각의 키마다 프록시/스텁의 CLSID가 들어 있다.
프록시/스텁의 CLSID를 가지고 레지스트리 HKCR\CLSID 키 밑에서 일치하는 키를 찾으면 프록시/스텁의 경로를 알 수 있다.
COM 서브 시스템은 이 경로를 가지고 클라이언트와 서버 프로세스 상에 프록시/스텁 DLL을 로드시키고 이를 통해 IPC 통신을 하게 된다.
 
우리는 IDL 파일을 가지고 프록시/스텁이 구현된 소스 파일을 쉽게 생성할 수 있고, 이를 컴파일하여 프록시/스텁 DLL을 만들 수 있다. 우리는 인터페이스를 구현한 적도 없고 물론 컴포넌트를 만든 적도 없다. 단지 지금까지 한 일이라고는 MyInterface.i이 파일에 인터페이스를 정의한 것 밖에는 없다. IDL파일만 있으면 프록시/스텁 DLL을 생성하는 것은 아주 쉬운 일이다. 프로시/스텁 DLL을 만들었으면 이를 클라이언트와 서버 각각의 컴퓨터에 복사한 뒤 등록하면 된다.
 
 

타입 라이브러리

타입 라이브러리(Type Library) COM 프로그래밍에 존재하는 다양한 타입들(컴포넌트, 인터페이스 등)에 대한 정보의 집합체이다. 어떤 CLSID를 가진 컴포넌트가 있는데, 그 컴포넌트에는 어떠한 인터페이스가 있으며 각각의 인터페이스는 어떠한 함수를 가지고 있고 각각의 함수는 어떠한 파라미터를 가지고 있는지, 이러한 정보를 타입 라이브러리를 통해 얻을 수 있다. 물론 이러한 정보는 헤더파일을 통해 충분히 전달할 수 있지만 C++를 사용하지 않는 다른 컴포넌트 개발자를 위해 특정 언어에 종속되지 않은 헤더파일을 만들 필요가 있고, 이러한 이유로 타입 라이브러리가 존재한다고 생각하자
 
타입 라이브러리는 프록시/스텁 DLL을 만들 때와 마찬가지로 MIDL 컴파일러를 통해 만들 수 있다. MIDL 컴파일러를 통해 타입 라이브러리를 생성하려면 IDL 코드상에 library 키워드가 반드시 있어야 한다. Library 키워드는 컴포넌트를 뜻하는 coclass 키워드를 포함할 수 있고, coclass 키워드는 interface 키워드를 포함할 수 있다.
 
import unknwn.idl;
 
[
object,
uuid(EE3BB533-085b-412e-a672-38f8d139b54e),
oleautomation
]
Interface ICalculator : IUnknown
{
             HRESULT Add([in] long a, [in] long b, [out] long* total);
}
 
[
uuid(E3965FED-F39A-4582-B427-3C09B83CF895),
helpstring(Math Tools Type Library),
version(1,0)
]
Library MathToolsLib
{
             importlib(stdole32.tlb);
             [
             uuid(18FF7E63-77A9-4dc6-ac71-33b9dad5a728)
             ]
             Coclass MathTools
             {
                           interface ICalculator;
             }
};
 
위 코드를 mathtools.idl 파일로 저장하고 MIDL로 컴파일하면 5개의 파일을 생성한다.
mathtools.idl,
dlldata.c
mathtools_i.c
mathtools_p.c
mathtools.tlb
 
앞의 4개 파일은 프록시/스텁 DLL을 만들기 위한 소스파일이고,
mathtools.tlb 파일이 타입 라이브러리이다.
타입라이브러리는 바이너리 파일이다.
Visual C++.NET 도구메뉴의 OLD/COM 개체뷰어를 이용하여 내용을 볼 수 있다.
 
mathtools.idl 파일에 oleautomation 특성이 선언되어 있다.
oleautomation 특성이 선언된 인터페이스는 모든 멤버함수의 매개변수가 Variant 구조체에 정의된 데이터 타입에만 국한됨을 의미한다.
다음은 Variant 구조체에 정의된 데이터 타입
 
LONG lVal;
BYTE bVal;
SHORT iVal;
FLOAT fltVal;
DOUBLE dblVal;
VARIANT_BOOL boolVal;
SCODE scode;
CY cyVal;
DATE date;
BSTR bstrVal;
IUnknown __RPC_FAR *punkVal;
IDispatch __RPC_FAR *pdispVal;
SAFEARRAY __RPC_FAR *parray;
BYTE __RPC_FAR *pbVal;
SHORT __RPC_FAR *piVal;
LONG __RPC_FAR *plVal;
FLOAT __RPC_FAR *pfltVal;
DOUBLE __RPC_FAR *pdblVal;
VARIANT_BOOL __RPC_FAR *pboolVal;
SCODE __RPC_FAR *pscode;
CY __RPC_FAR *pcyVal;
DATE __RPC_FAR *pdate;
BSTR __RPC_FAR *pbstrVal;
IUnknown __RPC_FAR *__RPC_FAR *ppunkVal;
IDispatch __RPC_FAR *__RPC_FAR *ppdispVal;
SAFEARRAY __RPC_FAR *__RPC_FAR *pparray;
 
Variant 구조체에 정의된 데이터 타입을 일컬어 OLE Automation 호환 데이터 타입 이라 한다. 만약 어떤 인터페이스의 모든 멤버함수가 매개변수로 OLE Automation 호환 데이터 타입만을 사용한다면 IDL 파일에 인터페이스를 정의할 때 oleautomation 특성을 지정하는 것이 좋다. 왜냐하면 oldautomation 특성을 가진 인터페이스는 별도의 프록시/스텁 DLL을 만들 필요가 없기 때문이다. oldautomation 특성을 가진 인터페이스를 위해서 oleaut32.dll 라는 유니버셜 마샬러(프록시/스텁 DLL을 마샬러 라고 부르기도 한다.)가 시스템에 이미 등록되어 존재한다.
 
일반적으로 OLE Automation 호환 인터페이스는 레지스트리의
HKCR\Interface\{IID}\ProxyStubClsid32 키 밑에
oleaut32.dll 유니버셜 마샬러를 의미하는 CLSID가 기록된다.
Oleaut32.dll 유니버셜 마샬러를 사용하는 클라이언트 컴퓨터는 반드시 타입 라이브러리를 가지고 있어야 한다. 왜냐하면 유니버셜 마샬러는 내부적으로 인터페이스의 타입 라이브러리를 사용하여 실질적인 마샬링(IPC를 통해 매개변수를 전달하기 위해 패킷을 만드는 작업을 뜻한다.) 작업을 수행하도록 구현되었기 때문이다.
 
OLE Automation 호환 인터페이스는 oleaut32.dll 유니버셜 마샬러에게 런타임시 타입 라이브러리르 제공하기 위해 HKCR\Interface\{IID} 키 밑에 TypeLib 키를 두고 여기에 타입 라이브러리의 식별자를 기록하게 된다.
레지스트리의 HKCR\TypeLib 아래에는 타입라이브러리에 대한 경로가 들어 있다.
 
OLE Automation 호환 인터페이스를 갖는 컴포넌트를 구현할 때 개발자는 MIDL을 통해 만든 타입 라이브러리를 사용하여 인터페이스와 프록시/스텁에 관련된 정보를 레지스트리에 쉽게 기록할 수 있다.
 
HKCR\Interface, HKCR\TypeLib 아래에 각각 인터페이스와 프로시/스텁에 관한 정보가 기록되어야 한다.
HRESULT DllRegisterServer()
{
//HKCR\CLSID\{CLSID 문자열}키 밑에 정보를 기록하는 코드 생략.
 
OLECHAR wPath[] = Lc:\\mathtools.tlb;
ITypeLib* pTypeLib = NULL;
LoadTypeLibEx(wPath, REGKIND_REGISTER, &pTypeLib);
//REGKIND_REGISTER 플래그를 주는것에 주목하자.
//LoadTypeLibEx가 호출되는 순간 타입라이브러리 상의 정보가 레지스트리에 기록된다.
pTypeLib->Release();
return S_OK;
}
 
OLE Automation 호환 인터페이스의 경우 타입 라이브러리만 있으면 프록시/스텁 DLL을 따로 만들 필요가 없다.
게다가 타입 라이브러리만 있으면 LoadTypeLibEx()를 호출하여 간단히 그 내용을 레지스트리에 기록할 수 있다. OLE Automation 호환 데이터 타입은 우리가 일상적으로 흔히 쓰는 거의 모든 데이터 타입이 포함됐을 뿐만 아니라 각각의 데이터 타입에 대한 배열(Array)까지 지원하기 때문에 아주 특별한 경우(ex. 구조체를 정의하여 매개변수로 사용해야 하는 경우)를 제외하고는 OLE Automation 호환 데이터타입만으로 충분히 인터페이스를 정의할 수 있다.
 
클라이언트 코드 상에서 타입 라이브러리를 사용하는 법.
타입 라이브러리는 언어에 종속되지 않은 헤더파일이라고 했다.
마이크로소프트의 모든 개발툴은 이 원칙을 확실하게 지키고 있다.
Visual C++ import 키워드는 타입 라이브러리를 읽어서 헤더파일로 변환시키고, 그 헤더파일을 소스에 포함시키는 일을 한다.
 
#import mathtools.tlb
Using namespace MathToolsLib;
 
int main(int argc, char* argv[])
{
CoInitialize(NULL);
ICalculator* pCalculator = NULL;
CoCreateInstance(__uuidof(MathTools), NULL, CLSCTS_SERVER, __uuidof(ICalculator), (void**)&pCalculator);
long a = 1, b = 2, total = 0;
pCalculator->Add(a,b,&total);
pCalculator->Release();
CoUninitialize();
return 0;
}
 
위 코드를 컴파일하면 import 키워드에 의해 mathtools.tlh, mathtools.tli 파일이 생성된다. mathtools.tlh 은 타입 라이브러리의 내용이 C++ 문법으로 바뀌어 정의된 파일,
mathtools.tli 은 각종 Wrapper 함수의 구현 코드가 들어있다.
타입 라이브러리만 있으면 클라이언트 어플리케이션을 작성할 때 더 이상 헤더파일이 필요하지 않다.
 
 
 
출처 : Visual C++.Net Programming Bible(삼양출판사)