COM (Component Object Model)
COM은 Microsoft에서 개발한 객체 지향 프로그래밍 기술로, 여러 프로그래밍 언어 및 환경에서 개발된 소프트웨어 컴포넌트들을 상호 운용성(interoperability)을 제공하여 재사용성과 확장성을 높이는 기술이다.
COM의 핵심 개념은 “컴포넌트”이다. 컴포넌트은 재사용 가능한 소프트웨어 모듈로, 다른 컴포넌트와 상호 작용할 수 있는 인터페이스를 제공한다. 이 인터페이스는 COM에서 지정한 표준 인터페이스 규격을 따르며, 이 규격에 따라 작성된 인터페이스는 여러 프로그래밍 언어와 환경에서 사용될 수 있다.
COM 컴포넌트는 DLL(Dynamic Link Library) 형태로 제공된다. DLL은 동적으로 로드되어 메모리에 올라가는 라이브러리 형태의 파일이며, COM 컴포넌트는 DLL 파일 내에 구현된다.
COM은 다른 컴포넌트와 상호 작용을 위해 표준 인터페이스와 함께 객체를 식별하기 위한 고유한 식별자인 “CLSID”를 사용한다. 또한, 컴포넌트의 인터페이스를 통해 메소드를 호출하고 데이터를 주고받는데 사용되는 “MIDL” 이라는 언어를 정의한다.
COM은 이후에 발전된 기술인 Distributed Component Object Model (DCOM)과 istributed Component Object Model (DCOM) 으로 발전하면서, 분산 환경에서 컴포넌트 상호 운용성을 높였고, 보안 및 트랜잭션 처리 등의 기능을 추가했다.
COM(Component Object Model)은 DirectX의 프로그래밍 언어 독립성과 하위 호환성을 가능하게 하는 기술이다. C++ 클래스로 간주하고 사용해도 무방하기 때문에 주로 COM 객체라고 흔히 부른다.
COM 객체는 COM 인터페이스라고도 불린다. 사용자는 COM의 대부분의 세부사항을 볼 수 없다. 그저 사용자가 알아야 할 것은 필요한 COM 객체를 가리키는 포인터를 특별한 함수를 이용해서, 또는 다른 COM 인터페이스의 메서드를 이용해서 얻는 방법뿐이다.
C++처럼 사용하는 방법과 유사하게 COM 객체를 사용할 수 있지만, C++과 다르게 COM 객체는 new나 delete를 이용하여 생성, 삭제를 할 수 없다. 반드시 생성을 위한 별도의 API 함수를 써야하며 그 인터페이스의 Release 메서드를 호출해주어야 한다. (모든 COM 인터페이스는 IUnknown이라는 COM인터페이스의 기능을 상속하는데, 여기에 Release라는 메서드를 제공한다.)
사용자는 COM 인터페이스 포인터를 AddRef 메서드를 사용하여 다른 변수에 복사할 수 있다. 이때 참조 횟수가 1증가한다. 또한 Release()를 호출하면 참조 횟수가 1감소한다. COM 객체는 참조 횟수가 0이 되면 메모리에서 해제된다.
COM 인터페이스들은 이름이 대문자 'I'로 시작한다.
예를 들어 명령 목록(command list)을 나타내는 COM 인터페이스의 이름은 ID3D12GraphicsCommandList이다.
코드로 COM 객체의 생성과 소멸을 간략하게 살펴보자.
ID3D12Device *pd3dDevice = NULL;
D3D12CreateDevice(NULL, ..., &pd3dDevice, ...);
ID3D12Device *pd3dDeviceCopied = pd3dDevice;
pd3dDeviceCopied->AddRef();
pd3dDeviceCopied->Release();
pd3dDevice->Release();
Device COM 객체를 D3D12CreateDevice API함수를 이용하여 생성하고 그것을 pd3dDeviceCopied 변수에 복사하였다. 이때 AddRef() 메서드를 호출해줘야 하며, 역시 더이상 변수를 사용하지 않을때에도 Release() 메서드를 호출해주어야 한다.
COM 객체가 아닌 경우의 API 함수들은 객체 포인터의 주소가 아닌 그냥 주소를 넘겨준다는 점에서 확연한 차이를 보여준다.
GUID (Globally Unique IDentifier)
GUID는 인터페이스 클래스 식별자(ID)를 나타내는 128비트(16바이트) 정수 문자열이다. Microsoft의 COM에서는 인터페이스들을 구별하기 위해 GUID가 사용된다. 서로 호환되지 않을 수 있는 두개의 COM이 동일한 인터페이스 이름을 사용하더라도, 고유한 GUID 덕분에 구별이 가능하다.
__uuidof 연산자와 IID_PPV_ARGS 매크로 중 하나를 사용하여 비교적 쉽게 인터페이스 자료형, 클래스 이름, 인터페이스 포인터에 알맞는 GUID를 얻을 수 있다.
ID3D12Device* pd3dDevice;
// __uuidof 사용시
D3D12CreateDevice(..., __uuidof(ID3D12Device), &pd3dDevice);
// IID_PPV_ARGS 매크로 사용시
D3D12CreateDevice(..., IID_PPV_ARGS(&pd3dDevice));
//...
또한 COM 객체의 수명 관리를 돕기 위해 ComPtr 클래스를 제공한다. 이 클래스는 COM 객체를 위한 스마트 포인터라고 할 수 있다. 범위를 벗어난 ComPtr 인스턴스는 COM 객체에 대해 자동으로 Release를 호출한다. 따라서 사용자는 직접 Release를 호출할 필요가 없다. ComPtr 클래스에서 제공하는 메서드들을 살펴보면 다음과 같다.
template<typename T> class ComPtr;
T* ComPtr::Get() const; // 인터페이스 포인터를 반환
T** ComPtr::GetAddressOf(); // 인터페이스 포인터의 주소를 반환
unsigned long ComPtr::Reset(); // 인터페이스 포인터에 대한 모든 참조를 Release()
ComPtr::operator-> Operator // 인터페이스 포인터를 반환(Get과 동일)
ComPtr::operator& Operator // 인터페이스 포인터의 주소를 반환(GetAddressOf와 동일)
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](<https://docs.microsoft.com/en-us/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx>)의 값에서 사용되며, 새로 만들어진 객체를 사용할 코드가 어디에서 실행될 지를 알려준다.
인터페이스
모든 COM 컴포넌트는 인터페이스를 가진다. 다른 컴포넌트들과 상호작용을 할 때 이 인터페이스를 통해서 한다. 중요한 표준 인터페이스에는 IUnknown과 IClassFactory가 있다. 모든 인터페이스들은 기본 인터페이스인 IUnknown인터페이스를 상속 받고, 모든 컴포넌트들도 IUnknown인터페이스를 가지고 있다.
IUnknown
IUnknown은 COM의 가장 기본 인터페이스로 세가지 함수를 갖는다.
- QueryInterface() 함수를 이용해서 특정 개체의 인터페이스에 대한 포인터를 받아올 수 있다.
- AddRef()는 인터페이스 포인터를 생성/복사 할 때 호출하며, 호출하면 특정 인터페이스의 참조 횟수가 증가한다.
- Release()는 AddRef()와는 반대로 인터페이스의 참조 횟수를 감소 시킨다.
IClassFactory
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 클라이언트와 서버
COM에서 서버와 클라이언트로 나눈다면, 클라이언트는 서버로부터 포인터를 받아서 인터페이스의 함수를 호출하는 객체를 말하고, 서버는 이러한 클라이언트 객체에게 인터페이스를 제공함으로써 서비스를 제공하는 객체를 말한다. 서버의 종류는 CoCreateInstance()를 호출할 때 dwClsContext의 값으로 알 수 있다.
In-Process
In-Process 서버는 DLL 형태로 구성된다. DLL의 경우, 사용하는 프로세스가 라이브러리를 로딩해서 실행 하기 때문에 말 그래도 "In Process"로 작동 하는 서버이다.
- COM Library가 CoGetClassObject()를 호출하면 Service Control Manager(SCM)이 레지스트리의 HKEY_CLASSES_ROOT\CLSID 에서 원하는 모듈의 경로(InProcServer32)를 반환해주고, COM Library가 DLL을 로드한다.
- 로딩 후, 모듈(DLL)안에 있을 DllGetClassObject() 함수를 호출하면 클래스 팩토리를 생성하고, 인터페이스 포인터를 반환한다.
Out-of-Process
Out-of-Process 서버는 EXE 형태로 구성되며, 클라이언트와 다른 주소공간에서 로드되기 때문에 Marshaling, Proxy, Stub 등의 기술을 이용해서 프로세스의 경계를 넘어서 필요한 데이터를 전송한다.
Out-of-process 서버의 경우, 로컬과 원격, 두 가지의 위치에 존재 할 수 있다. COM Library가 CoGetClassObject()를 호출하면 SCM이 레지스트리의 HKEY_CLASSES_ROOT\CLSID 에서 원하는 모듈의 경로(LocalServer32)를 가져와 실행시킨다.
- 로컬 서버의 경우, SCM이 서버를 실행시키면 서버가 클래스 팩토리를 생성하고, CoRegisterClassObject()를 호출해서 클래스 테이블에 등록한다. 클래스 테이블에 등록 되면 클라이언트가 CoGetClassObject()를 호출하여 사용할 수 있다.
- 원격의 경우, 로컬 SCM이 서버가 있는 컴퓨터의 SCM으로 부터 클래스 팩토리 인터페이스에 대한 포인터를 받아서 사용한다. DCOM을 사용하며, LPC(Local Procedure Call) 대신에 RPC (Remote Prodcedure Call)을 사용한다는 차이점이 있다. 원격의 경우, 다음 구간에서 구체적으로 설명한다.
위치 투명성과 마샬링
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) 이라고 한다.
IDispatch 인터페이스(Dispinterface)와 오토메이션
COM은 언어 독립적으로 표준만 지킨다면 객체들 끼리 서로 소통할 수 있다. 한편 COM 인터페이스는 사실상 가상 함수 테이블로, 함수의 주소 값을 가져다 쓰는 것이다. 하지만 가상 함수 테이블을 지원하지 않는 언어들이 있는데, 이 언어들의 경우에도 객체들 끼리 소통하기 위해서 생긴 것이 IDispatch 인터페이스다. 이 외에도, 언어들 간의 데이터 타입 처리 방식에 차이가 있기 때문에, 변수 유형을 통일시키기 위해서도 IDispatch 인터페이스가 필요하다. 이 문제를 위해 IDispatch 인터페이스에서는 Variant 데이터 유형을 사용한다.
IDispatch 인터페이스에는 4개의 함수가 추가된다.
- GetTypeInfoCount()는 객체가 타입 정보를 제공하는지를 알려준다.
- GetTypeInfo()는 객체의 타입정보를 알려준다.
- GetIDsOfNames()는 특정 함수 혹은 객체의 속성 이름에 해당하는 DISPID 값을 반환한다.
- Invoke()은 GetIDsOfNames()에서 받아온 DISPID로 원하는 함수 혹은 속성을 사용할 수 있게 해준다.
COM의 Thread Model (STA와 MTA)
COM에서는 Thread Model에 아파트먼트(Apartments)르 사용한다. 아파트먼트는 COM 클라이언트 그리고 객체들을 가상의 공간으로 분리해 놓는다고 생각할 수 있다. 아파트먼트는 Thread-Safe 하지 않은 함수 혹은 객체들을 사용하기 위해서이다. 만약 객체가 Thread-Safe 하다고 명시 하지 않는다면, COM은 이 객체를 한번에 한 곳에서만 호출 할 수 있도록 한다. Thread-Safe하다고 명시를 한다면, 이 한 객체를 여러 곳에서 호출이 가능해진다.
아파트먼트에는 두가지 STA (Single Threaded Apartmnet)와 MTA(Multi-Threaded Apartment) 가 있다. STA의 경우, STA에 있는 객체는 하나의 스레드만 동시에 호출이 가능하고, MTA의 경우 여러 스레드가 동시에 호출 할 수 있다. 다른 아파트먼트에 있는 객체를 호출 하는 경우, RPC와 마샬링을 이용해서 호출을 하는데, STA의 경우 이 RPC 메세지들을 메세지 큐에 넣고, 해당 아파트먼트에 있는 스레드가 이를 차례대로 처리한다.
MTA의 경우에는 프로세스 당 한개만 가질 수 있지만, 이 MTA 한개에 들어갈 수 잇는 스레드와 객체의 갯수에 제한이 없다. 또한, 메세지 큐가 없고, 요청들이 RPC 스레드 풀에서 무작위로 선택되서 처리 되기 때문에, Thread-Safe하지 않다면 MTA에 있어서는 안된다.
References
[1] COM (Component Object Model)에 대해서 (velog.io)
[2] 컴포넌트 오브젝트 모델 - 위키백과, 우리 모두의 백과사전 (wikipedia.org)
[3] Component Object Model - Wikipedia
[4] [DirectX 12] 기본지식 - COM(Component Object Model) (tistory.com)