윈도우 개발을 하면서 COM에 대한 개념이 필요하다고 느껴 MSDN, 박성규님의 블로그 등을 읽어보며 간략한게 정리한 내용이다. 워낙 오래된 기술이기도 하고, 이제 쓸 사람도 없겠지만, 그래도 만약 나처럼 이걸 공부해야하는 안타까운 사람이 있다면, 조금이라도 도움이 되고자 글을 남긴다. COM을 전부 다루는건 아니지만, 기본적인 내용과 몇가지 주제를 다룬다.
COM은 객체들을 생성하고 파괴, 그리고 객체들의 상호작용을 표준화한 기준이다. COM은 언어 독립적이기 때문에 이 표준만 지킨다면, 가상 함수 테이블을 지원하는 언어라면 뭐든 사용해서 COM 컴포넌트를 만들 수 있다.
COM을 이용해서 객체를 생성할 때 필요한 두 가지 함수가 있다. CoInitialize()
와 CoUninitialize()
의 두 개인데, COM 컴포넌트를 사용하는 인스턴스의 시작과 끝에 사용한다; push
와 pop
, OpenHandle()
과 CloseHandle()
처럼 이 두 개의 함수도 함께 사용된다.
CoInitialize()
를 한 후에는 CoCreateInstance()
함수를 이용해서 객체를 생성한다.
CoCreateInstance( REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID *ppv
)
여기서 rclsid
와 riid
의 경우, 컴포넌트 식별자인 GUID를 사용한다. rclsid
는 생성하려는 객체의 고유식별자, riid
는 해당 객체와 소통하는데 사용할 인터페이스의 식별자이다. riid
에서 명시한 인터페이스의 포인터는 ppv
에 반환된다.
pUnkOuter
는 해당 객체가 Aggregation의 일부로 생성되는 지를 알려준다. Aggregation의 목적이 아니라면 NULL, 맞다면 Aggregate 하는 객체의 IUnknown
인터페이스에 대한 포인터를 넣는다.
wClsContext
의 경우 CLSCTX
의 값에서 사용되며, 새로 만들어진 객체를 사용할 코드가 어디에서 실행될 지를 알려준다.
모든 COM 컴포넌트는 인터페이스를 가진다. 다른 컴포넌트들과 상호작용을 할 때 이 인터페이스를 통해서 한다. 중요한 표준 인터페이스에는 IUnknown
과 IClassFactory
가 있다. 모든 인터페이스들은 기본 인터페이스인 IUnknown
인터페이스를 상속 받고, 모든 컴포넌트들도 IUnknown
인터페이스를 가지고 있다.
IUnknown
은 COM의 가장 기본 인터페이스로 세가지 함수를 갖는다.
QueryInterface()
함수를 이용해서 특정 개체의 인터페이스에 대한 포인터를 받아올 수 있다. AddRef()
는 인터페이스 포인터를 생성/복사 할 때 호출하며, 호출하면 특정 인터페이스의 참조 횟수가 증가한다.Release()
는 AddRef()
와는 반대로 인터페이스의 참조 횟수를 감소 시킨다. IClassFactory
는 COM 컴포넌트를 생성할 때 사용되는 보조 컴포넌트다. 이 보조 컴포넌트는 객체를 생성하는 CoCreateInstance()
에서 사용된다.
CoCreateInstance(REFCLSID rclsid, LPUNKNOWN punkOuter, DWORD dwClsContext, REFIID riid, LPVOID *ppv){
*ppv = NULL;
IClassFactory *plFactory = NULL;
HRESULT hr = CoGetClassObject(rclsid, dwClsContext, NULL, IID_ICLASSFACTORY, (LPVOID*)&plFactory);
if(SUCCEEDED(hr)){
hr = plFactory -> CreateInstance(pUnkOuter, riid, ppv);
plFactory -> Release();
}
return (hr);
}
IClassFactory
는 IUnknown
에 추가로 2개의 함수를 갖는다.
CreateInstance()
는 주어진 riid
에 해당하는 인터페이스를 만들어서 ppv
에 반환한다.LockServer()
는 AddRef()
와 Release()
와 같이 해당 서버를 사용 중인 객체의 숫자를 관리하는 역할을 한다.COM에서 서버와 클라이언트로 나눈다면, 클라이언트는 서버로부터 포인터를 받아서 인터페이스의 함수를 호출하는 객체를 말하고, 서버는 이러한 클라이언트 객체에게 인터페이스를 제공함으로써 서비스를 제공하는 객체를 말한다. 서버의 종류는 CoCreateInstance()
를 호출할 때 dwClsContext
의 값으로 알 수 있다.
In-Process 서버는 DLL 형태로 구성된다. DLL의 경우, 사용하는 프로세스가 라이브러리를 로딩해서 실행 하기 때문에 말 그래도 "In Process"로 작동 하는 서버이다.
CoGetClassObject()
를 호출하면 Service Control Manager(SCM)이 레지스트리의 HKEY_CLASSES_ROOT\CLSID 에서 원하는 모듈의 경로(InProcServer32)를 반환해주고, COM Library가 DLL을 로드한다.DllGetClassObject()
함수를 호출하면 클래스 팩토리를 생성하고, 인터페이스 포인터를 반환한다.Out-of-Process 서버는 EXE 형태로 구성되며, 클라이언트와 다른 주소공간에서 로드되기 때문에 Marshaling, Proxy, Stub 등의 기술을 이용해서 프로세스의 경계를 넘어서 필요한 데이터를 전송한다.
Out-of-process 서버의 경우, 로컬과 원격, 두 가지의 위치에 존재 할 수 있다. COM Library가 CoGetClassObject()
를 호출하면 SCM이 레지스트리의 HKEY_CLASSES_ROOT\CLSID 에서 원하는 모듈의 경로(LocalServer32)를 가져와 실행시킨다.
CoRegisterClassObject()
를 호출해서 클래스 테이블에 등록한다. 클래스 테이블에 등록 되면 클라이언트가 CoGetClassObject()
를 호출하여 사용할 수 있다.COM 서버는 위치 투명성을 제공한다. 이 말은 COM 서버가 In-Process 이든, Out-of-Process 이든 클라이언트는 크게 신경을 쓰지 않아도 된다는 뜻이다. IClassFactory
가 이 위치 투명성을 제공해주는 좋은 예시이다. In-Process의 경우, 클라이언트와 같은 주소 공간에 로드가 되어있기 때문에 클래스 팩토리를 사용하지 않고 new 연산자로 객체를 만들 수도 있지만, 굳이 클래스 팩토리를 사용하는 이유는 COM 서버가 In-Process이든, Out-of-Process 이든 구분 없이 같은 방법을 사용할 수 있게 하기 위함이다. 그러기 위해서는 Out-of-Process 서버에서도 In-Process와 마찬가지로 자신의 함수를 호출하듯이 호출 할 수 있어야 한다. 하지만 Out-of-Process 서버의 경우에는 추가적인 메커니즘이 필요한데, 이것이 마샬링(Marshaling) 이다.
위치 투명성을 위해서 클라이언트는 In-Process와 같은 방식으로 처리를 하려고 한다. 그러기 위해서 마샬링 (Marshaling) 은 원격 서버의 경우에, 로컬에 있는 클라이언트가 원격에 있는 서버를 마치 로컬에 있는 것 처럼 사용하기 위해서 필요한 메커니즘이다. 이 과정에서 COM 라이브러리가 로컬에서는 Proxy를, 원격에서는 Stub를 생성한다. 클라이언트는 Proxy를 서버라 COM 객체라고 생각하고, 원격의 COM 객체는 Stub를 클라이언트라고 생각하는 것이다. 실제로 Stub과 Proxy 사이에 각 시스템의 COM 라이브러리가 RPC를 이용해서 통신을 한다. 이때 Proxy가 함수를 호출하는 호출과 인자를 원격으로 전송가능한 패킷으로 포장해서 전송하는 과정을 마샬링 이라고 하고, 이렇게 패킷으로 포장 된 데이터를 다시 COM라이브러리가 인식하기 위한 데이터로 바꾸는 것을 언마샬링 (Unmarshalling) 이라고 한다.
COM은 언어 독립적으로 표준만 지킨다면 객체들 끼리 서로 소통할 수 있다. 한편 COM 인터페이스는 사실상 가상 함수 테이블로, 함수의 주소 값을 가져다 쓰는 것이다. 하지만 가상 함수 테이블을 지원하지 않는 언어들이 있는데, 이 언어들의 경우에도 객체들 끼리 소통하기 위해서 생긴 것이 IDispatch 인터페이스다. 이 외에도, 언어들 간의 데이터 타입 처리 방식에 차이가 있기 때문에, 변수 유형을 통일시키기 위해서도 IDispatch 인터페이스가 필요하다. 이 문제를 위해 IDispatch 인터페이스에서는 Variant 데이터 유형을 사용한다.
IDispatch 인터페이스에는 4개의 함수가 추가된다.
GetTypeInfoCount()
는 객체가 타입 정보를 제공하는지를 알려준다. GetTypeInfo()
는 객체의 타입정보를 알려준다.GetIDsOfNames()
는 특정 함수 혹은 객체의 속성 이름에 해당하는 DISPID 값을 반환한다.Invoke()
은 GetIDsOfNames()
에서 받아온 DISPID로 원하는 함수 혹은 속성을 사용할 수 있게 해준다.COM에서는 Thread Model에 아파트먼트(Apartments)르 사용한다. 아파트먼트는 COM 클라이언트 그리고 객체들을 가상의 공간으로 분리해 놓는다고 생각할 수 있다. 아파트먼트는 Thread-Safe 하지 않은 함수 혹은 객체들을 사용하기 위해서이다. 만약 객체가 Thread-Safe 하다고 명시 하지 않는다면, COM은 이 객체를 한번에 한 곳에서만 호출 할 수 있도록 한다. Thread-Safe하다고 명시를 한다면, 이 한 객체를 여러 곳에서 호출이 가능해진다.
이미지 1. 다른 아파트먼트의 객체에 요청을 할때는 원격 서버와 같이 proxy와 stub를 사용한다.
아파트먼트에는 두가지 STA (Single Threaded Apartmnet)와 MTA(Multi-Threaded Apartment)가 있다. STA의 경우, STA에 있는 객체는 하나의 스레드만 동시에 호출이 가능하고, MTA의 경우 여러 스레드가 동시에 호출 할 수 있다. 다른 아파트먼트에 있는 객체를 호출 하는 경우, RPC와 마샬링을 이용해서 호출을 하는데, STA의 경우 이 RPC 메세지들을 메세지 큐에 넣고, 해당 아파트먼트에 있는 스레드가 이를 차례대로 처리한다.
이미지 2. 위에처럼 STA의 경우, 요청들을 메세지 큐에 넣어서 하나씩 처리한다.
MTA의 경우에는 프로세스 당 한개만 가질 수 있지만, 이 MTA 한개에 들어갈 수 잇는 스레드와 객체의 갯수에 제한이 없다. 또한, 메세지 큐가 없고, 요청들이 RPC 스레드 풀에서 무작위로 선택되서 처리 되기 때문에, Thread-Safe하지 않다면 MTA에 있어서는 안된다.
이미지 3. MTA의 경우, 여러 요청을 모두 아파트먼트안의 객체에 직접적으로 요청한다.
출처
이미지 1, 2, 3