COM의 소개(파트 2) – COM 서버의 이면
본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.
원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.
또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.
- COM의 소개(파트 2) – COM 서버의 이면 (1)
- COM의 소개(파트 2) – COM 서버의 이면 (2)
- COM의 소개(파트 2) – COM 서버의 이면 (3)
- COM의 소개(파트 2) – COM 서버의 이면 (4)
- COM의 소개(파트 2) – COM 서버의 이면 (5)
- COM의 소개(파트 2) – COM 서버의 이면 (6)
- COM의 소개(파트 2) – COM 서버의 이면 (7)
- COM의 소개(파트 2) – COM 서버의 이면 (8)
- COM의 소개(파트 2) – COM 서버의 이면 (9) [完]
간단한 사용자 인터페이스
실제로 클래스팩토리의 예제를 확인하기 위하여, 본 글에 수록된 샘플 프로젝트를 살펴보는 것부터 시작하겠습니다. 이 프로젝트는 CSimpleMsgBoxImpl
이라는 이름의 coclass에서 ISimpleMsgBox
인터페이스를 구현하고 있는 DLL 서버입니다.
인터페이스 정의
우리가 만든 새로운 인터페이스의 이름을 ISimpleMsgBox
로 정하겠습니다. 모든 인터페이스들과 마찬가지로 이 인터페이스도 IUnknown
에서 파생되어야 합니다. 이 인터페이스에는 단 하나의 메소드인 DoSimpleMsgBox
가 있습니다. 이 메소드는 표준 반환형인 HRESULT
를 반환함을 숙지하시기 바랍니다. 여러분이 작성하는 모든 메소드들은 HRESULT
를 반환형으로 삼아야 합니다. 그 외 다른 형식으로 호출자에게 값을 반환하고자 할 때는 포인터 파라미터를 사용해야 합니다.
struct ISimpleMsgBox : public IUnknown {
/* IUnknown에서 정의된 메소드들 */
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface(REFIID riid, void ** ppv);
/* ISimpleMsgBox 메소드 */
HRESULT DoSimpleMsgBox(HWND hWndParent, BSTR bsMessageText);
};
struct declspec(uuid(“{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}”)) ISimpleMsgBox;
__declspec
가 적혀있는 줄은 ISimpleMsgBox
심볼(symbol)에 GUID
를 부여하고 있습니다. 이렇게 하면 GUID
는 나중에 __uuidof
연산자를 통해 얻을 수 있습니다. __declspec
와 __uuidof
는 Microsoft C++ 확장입니다.
DoSimpleMsgBox
의 두 번째 파라미터는 BSTR
형입니다. BSTR
은 “바이너리 문자열(binary string)”을 의미하며, 고정 크기 바이트들의 시퀸스를 나타냅니다(역자: 즉 한 글자당 2바이트짜리가 계속 이어져 있다는 뜻입니다). BSTR
은 주로 Visual Basic이나 Windows Scripting Host와 같은 스크립팅 클라이언트에서 사용됩니다.
이 인터페이스는 CSimpleMsgBoxImpl
이라는 이름의 C++ 클래스로 구현됩니다. 클래스의 선언은 다음과 같습니다.
class CSimpleMsgBoxImpl : public ISimpleMsgBox {
public:
CSimpleMsgBoxImpl();
virtual ~CSimpleMsgBoxImpl();
// IUnknown 메소드
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// ISimpleMsgBox methods
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
protected:
ULONG m_uRefCount;
};
class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsgBoxImpl;
COM 클라이언트가 SimpleMsgBox
형식의 COM 객체를 생성하고자 할 때 코드는 다음과 같이 작성합니다.
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl), // coclass의 CLSID
NULL, // 객체 통합하지 않음
CLSCTX_INPROC_SERVER, // 인 프로세스 DLL 서버
__uuidof(ISimpleMsgBox), // 우리가 원하는 인터페이스에 대한 IID
(void**) &pIMsgBox); // 인터페이스 포인터의 주소
클래스팩토리
클래스팩토리 구현
SimpleMsgBox
팩토리는 C++ 클래스로 구현되어 있으며, 예상하다시피 CSimpleMsgBoxClassFactory
로 명명합니다.
class CSimpleMsgBoxClassFactory : public IClassFactory {
public:
CSimpleMsgBoxClassFactory();
virtual ~CSimpleMsgBoxClassFactory();
// IUnknown 메소드
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface(REFIID riid, void** ppv);
// IClassFactory 메소드
HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID riid, void ** ppv);
HRESULT LockServer(BOOL fLock);
protected:
ULONG m_uRefCount;
};
생성자, 소멸자 및 IUnknown
서 보았던 예제들과 거의 같게 작동됩니다. 여러분이 예상하듯이 IClassFactory
의 메소드이며 새롭게 추가된 LockServer
는 비교적 단순합니다.
HRESULT CSimpleMsgBoxClassFactory::LockServer(BOOL fLock) {
fLock ? g_uDllLockCount++ : g_uDllLockCount--;
return S_OK;
}
지금부터는 흥미로운 내용인 CreateInstance
에 대해 살펴보겠습니다. 이 메소드는 새로운 CSimpleMsgBoxImpl
객체를 만드는 역할을 합니다. 이 메소드의 원형과 파라미터에 대해 자세히 보겠습니다.
HRESULT CSimpleMsgBoxClassFactory::CreateInstance(IUnknown * pUnkOuter, REFIID riid, void ** ppv);
pUnkOuter
는 새 객체가 결합될 때에 한하여 사용됩니다. 그리고 이는 “외부(outer)” COM 객체를 가리키는 포인터입니다. 즉, 새로운 객체를 포함하게 될 기존의 객체입니다. 객체 결함은 본 글의 범위를 벗어나는 주제이므로 우리의 예제 객체는 결합을 지원하지 않을 것입니다.
riid
와 ppv
는 QueryInterface
에서와 동일하게 사용됩니다. 각각 COM 클라이언트가 요청하고 있는 인터페이스에 대한 IID
이고, 인터페이스 포인터를 보관할 수 있는 포인터 크기의 버퍼입니다.
여기 CreateInstance
구현 예가 있습니다. 먼저 파라미터 유효성 검증과 초기화부터 시작합니다.
HRESULT CSimpleMsgBoxClassFactory::CreateInstance(IUnknown * pUnkOuter, REFIID riid, void ** ppv) {
// 객체 결합을 사용하지 않을 것이므로 pUnkOuter는 항상 NULL이어야 합니다.
if (pUnkOuter != NULL)
return CLASS_E_NOAGGREGATION;
// ppv가 실제 void * 형 포인터를 가리키고 있는지 검사합니다.
if (IsBadWritePtr(ppv, sizeof(void *)))
return E_POINTER;
*ppv = NULL;
파라미터가 유효함을 확인하였기 때문에 우리는 새로운 객체를 생성할 수 있습니다.
CSimpleMsgBoxImpl* pMsgbox;
// 새로운 COM 객체를 생성합니다.
pMsgbox = new CSimpleMsgBoxImpl;
if (NULL == pMsgbox) return E_OUTOFMEMORY;
마지막으로 우리는 새롭게 생성되는 객체에 대해 클라이언트가 요청한 형식의 인터페이스로 QI를 합니다. QI가 실패하면 객체는 사용할 수 없는 것이므로 우리는 이를 해제합니다.
HRESULT hrRet;
// 클라이언트가 요청한 형식의 인터페이스로 새로 만든 객체에 대해 QI합니다.
hrRet = pMsgbox->QueryInterface(riid, ppv);
// QI가 실패하면 클라이언트가 사용할 수 없는 객체이므로 이를 해제합니다.
// 왜냐하면 객체로부터 원하는 인터페이스 형식을 얻을 수 없기 때문입니다.
if (FAILED(hrRet)) delete pMsgbox;
return hrRet;
DllGetClassObject
DllGetClassObject
의 내부를 좀 더 자세히 들여다 보겠습니다. 원형은 다음과 같습니다.
HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void ** ppv);
rclsid
는 COM 클라이언트가 필요로 하는 coclass의 CLSID
입니다. 이 함수는 해당 coclass에 대한 클래스팩토리를 반드시 반환해야 합니다.
riid
와 ppv
는 QI에서 언급한 파라미터와 같습니다. 이 경우 riid
는 COM 라이브러리가 요청하는 클래스팩토리 객체에 대한 IID
, 즉 IID_IClassFactory
입니다.
DllGetClassObject
가 새로운 COM 객체로서 클래스 팩토리를 반환하기 때문에, 이 코드는 IClassFactory::CreateInstance
와 비슷해 보입니다. 유효성 검사와 초기화로 시작하는 부분을 살펴보겠습니다.
HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void ** ppv) {
// 클라이언트가 CSimpleMsgBoxImpl에 대한 팩토리를 요청하고 있는지 검사합니다.
if (!InlineIsEqualGUID(rclsid, __uuidof(CSimpleMsgBoxImpl)))
return CLASS_E_CLASSNOTAVAILABLE;
// ppv가 실제 void * 형 버퍼를 가리키고 있는지 검사합니다.
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
*ppv = NULL;
먼저 if
구문이 rclsid
파라미터를 검사합니다. 우리의 COM 서버는 단 하나의 coclass만을 가지고 있기 때문에, rclsid
는 반드시 CSimpleMsgBoxImpl
에 대한 CLSID
여야 합니다. __uuidof
연산자는 앞서 __deslcped(uuid())
선언으로 CSimpleMsgBoxImpl
클래스에 부여된 GUID
를 가져오는 작동을 합니다. InlineIsEqualsGUID
는 두 개의 GUID
가 서로 같은지 여부를 확인하는 인라인 함수입니다.
다음 단계는 클래스팩토리 생성 단계입니다.
CSimpleMsgBoxClassFactory* pFactory;
// 새로운 클래스팩토리를 생성합니다.
pFactory = new CSimpleMsgBoxClassFactory;
if (pFactory == NULL)
return E_OUTOFMEMORY;
이 부분이 CreateInstance
와 다소 다른 부분입니다. CreateInstance
로 돌아가서, 우리는 QI의 호출이 실패하였을 때 COM 객체를 할당 해제하였습니다. 하지만 이 부분은 그렇지 않습니다.
우리는 스스로를 우리가 만든 COM 객체(클래스팩토리)의 클라이언트라고 생각할 수 있습니다. 그래서 우리는 이 객체의 레퍼런스 카운트를 1
로 만들기 위해 AddRef
를 호출합니다. 그 다음 우리는 QI
를 호출합니다. QI가 성공하면 이 객체에 한번 더 AddRef
가 이루어져서 레퍼런스 카운트가 2
가 됩니다. QI
가 실패하면 레퍼런스 카운트는 1
로 감소합니다.
QI
가 호출된 후 우리는 클래스팩토리 객체의 사용을 더 이상 안 할 것이기 때문에 Release
를 호출합니다. QI
가 실패하면 이 클래스팩토리의 레퍼런스카운트가 0
이 되어 스스로 할당 해제합니다. 결국 결과는 같습니다.
// 클래스팩토리를 사용하고 있는 동안에는 AddRef()를 한 번 해줍니다.
pFactory->AddRef();
HRESULT hrRet;
// 클래스팩토리에 대해 COM 클라이언트가 요청한 형태의 인터페이스가 있는지 QI합니다.
hrRet = pFactory->QueryInterface(riid, ppv);
// 클래스팩토리의 사용을 끝냈으므로 Release합니다.
pFactory->Release();
return hrRet;
QueryInterface 다시 살펴보기
필자는 이전에 QI
의 구현을 보인 바 있습니다. 그러나 실전에서는 COM 객체가 꼭 IUnknown
만을 구현하지 않기 때문에 클래스팩토리의 QI
는 다시 살펴보는 것이 좋습니다. 먼저 우리는 ppv
버퍼가 유효한지 검사하고 이를 초기화합니다.
HRESULT CSimpleMsgBoxClassFactory::QueryInterface(REFIID riid, void ** ppv) {
HRESULT hrRet = S_OK;
// ppv가 실제로 void * 형 버퍼를 참조하고 있는 지 검사합니다.
if (IsBadWritePtr(ppv, sizeof(void *)))
return E_POINTER;
// 표준 QI 초기화: *ppv를 NULL로 설정합니다.
*ppv = NULL;
그 다음 우리는 riid
를 검사하여 해당 클래스팩토리에서 구현하고 있는 IUnknown
또는 IClassFactory
중 하나의 것인지를 확인합니다.
// COM 클라이언트가 우리자 지원할 수 있는 형태의 인터페이스를 요청하고 있다면 *ppv 설정
if (InlineIsEqualGUID(riid, IID_IUnknown)) {
*ppv = (IUnknown*) this;
} else if (InlineIsEqualGUID(riid, IID_IClassFactory)) {
*ppv = (IClassFactory *) this;
} else {
hrRet = E_NOINTERFACE;
}
마지막으로 riid
가 가리키는 인터페이스가 우리가 지원할 수 있는 인터페이스일 때 우리는 AddRef
를 호출하여 인터페이스 포인터의 레퍼런스 카운트를 1
만큼 증가시킵니다. 그 다음 인터페이스 포인터를 반환합니다.
// 인터페이스 포인터를 반환할 수 있게 되었다면, AddRef 호출
if (hrRet == S_OK) {
((IUnknown *) *ppv)->AddRef();
}
return hrRet;
ISimpleMsgBox 구현
우리의 소스 코드는 ISimpleMsgBox
가 가진 유일한 메소드인 DoSimpleMsgBox
를 가지고 있습니다. 우리는 bsMessageText
를 TCHAR
로 변환하기 위해 마이크로소프트의 확장 클래스인 _bstr_t
를 처음으로 사용해 봅니다.
HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText) {
_bstr_t bsMsg = bsMessageText;
LPCTSTR szMsg = (TCHAR *) bsMsg; // 필요하다면 _bstr_t 문자열을 TCHSR로 변환합니다.
// 변환 후에 우리는 메시지 박스를 보여주고 값을 반환합니다.
MessageBox(hwndParent, szMsg, _T("Simple Message Box"), MB_OK);
return S_OK;
}
계속 읽기
이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (7)
다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (9) [完]