지난 회에서는 COM에 대해 개략적인 설명을 했고 이번 회에는 실제로 간단한 COM 컴포넌트를 하나 만들어 보도록 하자. 따라서 당연히
지난 회의 내용을 잘 알고 있어야 한다. 만들어 볼 컴포넌트는 간단하게 두 개의 메소드를 갖는 컴포넌트가 되겠다. COM 인터페이스 중에서는
IUnknown과 IClassFactory만 지원할 것이다. IDispatch는 지원하지 않기 때문에 이번 회에서 만든 컴포넌트는 비주얼
베이직이나 델파이 같은 RAD 개발 환경에서는 사용할 수 없다. 이 것을 지원하려면 COM 컴포넌트 구현시 IDL(Interface
Definition Language)이라는 것을 사용해야 하는데 이걸 사용하게 되면 내용이 너무 복잡해져서 작은 지면으로는 도저히 설명할 분량이
안 되기 때문인데 이에 대해서는 다음 회에서 살펴보겠다. 다음 회에서는 이번 회에서 만든 컴포넌트를 IDL로 구현해보고 여기에 IDispatch
인터페이스에 대한 구현도 추가해보도록 하겠다. 내용에 들어가기에 앞서 한 가지만 말하자면 이번 회의 내용은 결코 쉬운 내용이 아니라는 점이
아니라는 것이다. 즉 열심히 보고 다른 자료도 찾아보고 해야 알 수 있는 내용이란 점이다.
——————————————————————————–
참고
IDispatch 인터페이스와 타입 라이브러리
COM 컴포넌트를 만들 때 IDispatch 인터페이스를 지원하지 않으면 그 컴포넌트는
포인터가 지원되지 않는 개발 환경(비주얼 베이직, 파워빌더 등)하에서 사용할 수 없다. 실제로 COM이 처음 나왔을 때만 해도 COM으로 만드는
컴포넌트는 C++나 C와 같이 포인터의 개념이 있는 개발 환경에서만 사용할 수 있었다. 그 문제점을 보완하기 위해서 추가된 인터페이스가 바로
IDispatch이다. 원래 COM에서 컴포넌트의 메소드들은 함수 테이블의 형태로 존재하는데 이를 접근하려면 포인터를 쓰는 수 밖에 없다.
뒤에서 예제 프로그램을 보면 확연히 알 수 있을 것이다. 그래서 포인터를 쓰지 않고 컴포넌트의 메소드를 호출할 수 있도록 해주는 표준적인
방법으로 제공된 것이 바로 IDispatch 인터페이스이다. 즉, 개발 환경에서는 직접 인터페이스 포인터로 건드리는 것이 아니라
IDispatch를 이용하여 인터페이스를 조작할 수 있게 된다. 또, IDispatch 인터페이스를 이용하면 컴포넌트의 메소드 호출시에 late
binding을 수행할 수 있다. 이 특성을 이용하면 굉장히 융통성이 있는 프로그래밍을 할 수 있는데 이에 대해서도 다음 회에 알아보겠다.
COM 컴포넌트를 RAD 개발 환경에서 사용함에 있어 한 가지 더 언급할 것이 있는데 그것은 바로 타입 라이브러리라는 것이다. 어떤
컴포넌트를 개발에 사용하려면 그것이 포함하고 있는 메소드가 무엇이 있는지를 개발 환경에 알려야 한다. C++ 같으면 사용할 컴포넌트의 클래스
정의를 include하여 사용하면 된다. 그러면 비주얼 베이직에서는 어떻게 해야할까 ? 이런 목적으로 나온 것이 바로 타입 라이브러리(Type
Library)이다. 이는 COM 컴포넌트에 들어있는 메소드와 프로퍼티(나중에 언급하겠지만 프로퍼티도 결국은 두 개의 메소드로 구현된다)와
이벤트가 무엇이 있는지 알려 주는 역할을 한다. 타입 라이브러리는 COM 컴포넌트에 포함되기도 하고 별개의 모듈로 존재하기도 한다. 별개의
모듈로 존재할 경우에는 확장자로 .tlb가 주어진다. 비주얼 베이직과 같은 개발 환경 하에서는 이 타입 라이브러리를 인식하여 컴포넌트의 메소드
호출 시에 문법 에러 체크를 수행할 수 있다. 그럼, 타입 라이브러리는 어떻게 만들어지는가 ? 이 것을 만들려면 컴포넌트를 만들 때 IDL이라는
언어로 인터페이스 정의를 하고 그걸 MIDL(Microsoft Interface Definition Language)로 컴파일하는 작업을 해야
한다. 이에 대해서는 다음 회에 살펴보도록 하겠다.
——————————————————————————–
COM 컴포넌트 만들기
그럼 COM 컴포넌트를 만드는 작업에 바로 들어가도록 하자. 먼저 비주얼 C++를 실행하고 File 메뉴의
New 명령을 선택한다. 그래서 Projects 탭의 Win32 Dynamic-Link Library를 선택한다. 프로젝트 이름으로는
MyCom을 지정한다. Step 1에서는 A Simple DLL Project를 선택한다. DLL 타입을 선택했기 때문에 만들어 볼 COM
컴포넌트는 인프로세스 컴포넌트가 된다. 여기서 소스의 모든 부분을 자세히 설명할 수는 없는 노릇이고 세부적인 코드 내용은 자료실의 예제
프로그램을 다운받아 참고하기 바란다.
만들어 보고자 하는 COM 컴포넌트는 DisplayCPUType과 DisplayMemorySize라는 두 개의 메소드를 지원한다. 그리고
컴포넌트의 CLSID는 CLSID_MyCOM이고 여기에서 지원하는 인터페이스의 이름은 IMyInterface이고 IID는
IID_IMyInterface이다.
COM 클라이언트 코드부터 보기
구체적인 컴포넌트 만들기에 들어가기에 앞서 COM 컴포넌트를 사용하는 클라이언트 프로그램의 소스를
먼저 살펴보는 것이 이해에 도움이 될 것 같아서 먼저 사용 코드를 보기로 하겠다. 이 코드 역시 비주얼 C++로 작성했으며 보면 알겠지만 C
코드만 사용하였다. 테스트 클라이언트 코드는 Win32 Application 타입의 프로젝트로 구성되었다.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR
lpCmdLine, int nCmdShow)
{
IClassFactory *pFactory = NULL;
IMyInterface *pMyInf = NULL;
// COM 라이브러리를 초기화한다.
CoInitialize(NULL);
// MyCOM 객체의 IClassFactory 인터페이스를 얻어 낸다.
if
(CoGetClassObject(CLSID_MyCOM, CLSCTX_ALL, NULL, IID_IClassFactory, (LPVOID
*)&pFactory) != S_OK)
{
CoUninitialize();
MessageBox(NULL,
“Cannot get My Com object”, “Error”, MB_OK);
return 0;
}
// IClassFactory의 CreateInstance를 통해 IMyInterface 인터페이스의 포인터를 얻어낸다.
if
(pFactory->CreateInstance(NULL, IID_IMyInterface, (void **)&pMyInf) !=
S_OK)
{
pFactory->Release();
CoUninitialize();
return
0;
}
// pFactory 인터페이스를 제거한다.
pFactory->Release();
// IMyInterface의 메소드를 호출한다.
pMyInf->DisplayMemorySize();
pMyInf->DisplayOSType();
pMyInf->Release();
CoUninitialize();
return 0;
}
위의 코드에서 제일 중요한 부분은 바로 굵은 체로 표시한 DisplayMemorySize와 DisplayCPUType 메소드를 호출하는
부분이다. 이 것을 호출하기 위해 그 앞 단에서 많은 일을 한 것이다. 그걸 좀 살펴보면 먼저 COM 라이브러리를 초기화하기 위해
CoInitialize를 호출하였다. COM 라이브러리를 다 사용하였으면 마지막에 CoUninitialize를 호출해야 한다. 다음으로 이제
MyCOM 컴포넌트를 하나 만들고 나서 그것의 두 개의 메소드를 호출해야 한다. 그런데 MyCOM 컴포넌트를 만드는 방법이 약간 복잡하다.
MyCOM 객체의 IClassFactory 인터페이스를 얻어서 그것의 CreateInstance 메소드 호출을 통해 만들어 내게 된다.
IClassFactory 인터페이스를 얻기 위해서는 CoGetClassObject라는 API를 호출한다. 인자를 보면 알 수 있지만 첫 번째
인자로는 대상 컴포넌트의 CLSID를 지정한다. 네 번째 인자로는 필요한 인터페이스인 IClassFactory를 나타내는
IID_IClassFactory를 지정한다. 마지막 인자로는 IClassFactory 인터페이스에 대한 포인터를 받아 올 변수를 지정한다.
if (CoGetClassObject(CLSID_MyCOM, CLSCTX_ALL, NULL,
IID_IClassFactory,
(LPVOID *)&pFactory) != S_OK)
CoGetClassObject를 부를 때 일어나는 내부 동작을 좀 더 이해할 필요가 있다. 이를 호출하면 COM 런타임(운영체제에서
COM을 담당하는 부분)은 레지스트리를 보고 해당 CLSID를 갖는 파일이 무엇인지 알아낸 다음에 그걸 메모리로 로드한다(이미 로드되어 있었다면
물론 그걸 사용한다). 그리고 나서 IClassFactory 인터페이스에 대한 포인터를 얻는 일을 한다. 인프로세스 컴포넌트의 경우에는 그
컴포넌트의 DllGetClassObject라는 함수를 호출하면 포인터를 얻을 수 있다. 전 회에서 모든 인프로세스 컴포넌트가 구현해야 하는
4가지 함수가 있었다고 하였다. 그 중의 하나가 바로 DllGetClassObject였다. 그러면 아웃오브프로세스 컴포넌트는 ?
아웃오브프로세스는 DLL이 아니기 때문에 남이 부를 수 있는 함수를 제공할 수 없다. 그래서 아웃오브프로세스 컴포넌트는 처음 로드될 때 자신의
IClassFactory 인터페이스를 명시적으로 COM 런타임에 알려주어야 한다. 이 때 사용되는 API가 바로
CoRegisterClassObject라는 것이다. 즉 COM 런타임은 이 정보들을 내부적으로 보관했다가 사용하게 된다. 아웃오브프로세스
컴포넌트는 종료될 때 CoRevokeClassObject라는 API를 호출하여 자신이 등록했던 IClassFactory 인터페이스의 포인터를
취소해야 한다.
성공적으로 IClassFactory에 대한 인터페이스를 얻었으면 그것의 CreateInstance 메소드를 호출하여 MyCOM 컴포넌트를
하나 만들면서 IMyInterface에 대한 포인터를 요구한다.
if (pFactory->CreateInstance(NULL, IID_IMyInterface, (void
**)&pMyInf) != S_OK)
그 다음에는 그냥 IMyInterface 인터페이스의 메소드들을 호출한다.
pMyInf->DisplayMemorySize();
pMyInf->DisplayOSType();
전역 함수의 구현
그러면 다시 컴포넌트의 구현으로 돌아가자. 구현해야 될 것은 IClassFactory 인터페이스와
IMyInterface 인터페이스이다. IClassFactory 인터페이스는 CClassFactory라는 클래스로 구현해볼 것이고
IMyInterface 인터페이스는 CMyInterface라는 클래스로 구현해볼 것이다. 이 밖에도 전 회에서 이야기했던 것처럼 DLL 타입의
컴포넌트들이 구현해야 하는 함수가 4가지가 있다. 여기서는 편의상 DllUnregisterServer를 제외한 3가지만 구현해보도록 할 것인데
그 중에서 DllCanUnloadNow와 DllGetClassObject의 내용만 살펴 보자.
// 이 컴포넌트를 사용하는 곳이 있는지 판단하는데 사용된다.
STDAPI DllCanUnloadNow(void)
{
return (g_DllRefCount ? S_FALSE : S_OK);
}
// 앞서 설명한 것처럼 COM 런타임에서 컴포넌트의 IClassFactory 인터페이스 포인터를 얻는데 사용한다.
STDAPI
DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppReturn)
{
*ppReturn = NULL;
// 지원되지 않는 CLSID를 요청하면 에러를 낸다.
if
(!IsEqualCLSID(rclsid, CLSID_MyCOM))
return CLASS_E_CLASSNOTAVAILABLE;
// 클래스팩토리 인터페이스를 생성한다.
CClassFactory *pClassFactory = new
CClassFactory(rclsid);
if(NULL == pClassFactory)
return
E_OUTOFMEMORY;
// 클래스팩토리의 QueryInterface 함수를 통해 요청된 인터페이스를 넘기도록 한다.
HRESULT hResult =
pClassFactory->QueryInterface(riid, ppReturn);
// 생성한 클래스팩토리의 레퍼런스 카운트를 하나 감소시킨다.
pClassFactory->Release();
return hResult;
}
DllCanUnloadNow는 COM 런타임이 주기적으로 호출하는 함수이다. 이 함수에서 S_OK를 리턴하면 COM은 이 모듈을 사용하는
곳이 없다고 생각하고 메모리에서 내려 버린다. 판단 기준은 바로 전역 변수로 유지되는 g_DllRefCount라는 정수 타입의 변수이다. 이
변수는 이 컴포넌트를 사용하는 곳이 생길 때마다 하나씩 증가되고 없어질 때마다 하나씩 감소된다. 이를 통해서 0이 되면 사용하는 곳이 없다고
판단하는 것이다. 이런 방식은 뒤에서 IUnknown의 Release 메소드 구현시에도 볼 수 있을 것이다. DllGetClassObject는
위의 소스 주석 그대로이다. 앞서 설명과 연관해서 보기 바란다.
IClassFactory의 구현
DllGetClassObject에서는 IClassFactory 인터페이스를 구현한
CClassFactory라는 클래스를 사용하고 있는데 이 것의 정의는 다음과 같다. 모든 인터페이스가 그렇듯이 IUnknown으로부터 계승된다는
점을 명심하기 바란다.
class CClassFactory : public IClassFactory
{
protected:
DWORD m_ObjRefCount; // 이 컴포넌트를 사용하는 곳의 수를 유지한다.
public:
CClassFactory(CLSID);
~CClassFactory();
//IUnknown methods
STDMETHODIMP QueryInterface(REFIID, LPVOID*);
STDMETHODIMP_(DWORD)
AddRef();
STDMETHODIMP_(DWORD) Release();
//IClassFactory methods
STDMETHODIMP CreateInstance(LPUNKNOWN, REFIID,
LPVOID*);
STDMETHODIMP LockServer(BOOL);
private:
CLSID
m_clsidObject;
};
위의 클래스 구현은 다음과 같다.
STDMETHODIMP CClassFactory::QueryInterface(REFIID riid, LPVOID *ppReturn)
{
*ppReturn = NULL;
if(IsEqualIID(riid, IID_IUnknown)) // IUnknown
인터페이스가 요구된 경우
*ppReturn = this;
else if(IsEqualIID(riid,
IID_IClassFactory)) // IClassFactory 인터페이스가 요구된 경우
*ppReturn =
(IClassFactory*)this;
if (*ppReturn) // 요구된 인터페이스가 있는 경우에는
{
(*(LPUNKNOWN*)ppReturn)->AddRef(); // 레퍼런스 카운트를 1 증가시킨다.
return
S_OK;
}
return E_NOINTERFACE;
}
STDMETHODIMP_(DWORD) CClassFactory::AddRef()
{
return
++m_ObjRefCount;
}
STDMETHODIMP_(DWORD) CClassFactory::Release()
{
if (–m_ObjRefCount
== 0) // 사용하는 곳이 없어지면 자살(?)한다.
{
delete this;
return 0;
}
return m_ObjRefCount;
}
STDMETHODIMP CClassFactory::CreateInstance(LPUNKNOWN pUnknown, REFIID riid,
LPVOID *ppObject)
{
HRESULT hResult = E_FAIL;
LPVOID pTemp = NULL;
*ppObject = NULL;
if (pUnknown != NULL)
return
CLASS_E_NOAGGREGATION;
if (IsEqualCLSID(m_clsidObject, CLSID_MyCOM)) // 요구된 컴포넌트가 MyCOM이면
{
CMyInterface *pMyInt = new CMyInterface(); // MyCOM에 해당하는 CMyInterface 객체를
생성한다.
if (NULL == pMyInt)
return E_OUTOFMEMORY;
pTemp =
pMyInt;
}
if(pTemp)
{
// 요구하는 인터페이스가 CMyInterface 내에
있는지 물어본다.
hResult = ((LPUNKNOWN)pTemp)->QueryInterface(riid, ppObject);
((LPUNKNOWN)pTemp)->Release();
}
return hResult;
}
// 여기서는 구현하지 않았다.
STDMETHODIMP CClassFactory::LockServer(BOOL)
{
return E_NOTIMPL;
}
위의 CreateInstance 함수의 구현을 보면 CLSID_MyCOM이 요구되면 CMyInterface 타입의 객체를 하나 만들고
그것의 QueryInterface를 호출하여 요청된 인터페이스의 IID와 포인터를 넘긴다. 사실 IClassFactory의 구현은 누가 해도
비슷하다. 그렇기 때문에 ATL이나 MFC, VB 등을 이용해서 만들 경우에는 코드가 아예 제공이 된다.
IMyInterface의 구현
이제 CMyInterface 클래스의 정의와 구현을 보자.
class CMyInterface : public IMyInterface
{
protected:
DWORD
m_ObjRefCount; // 사용 중인 곳의 수를 유지한다.
public:
CMyInterface();
~CMyInterface();
// IUnknown methods
STDMETHODIMP
QueryInterface(REFIID, LPVOID*);
STDMETHODIMP_(DWORD) AddRef();
STDMETHODIMP_(DWORD) Release();
// IMyInterface methods
STDMETHODIMP DisplayOSType();
STDMETHODIMP DisplayMemorySize();
};
여기서는 구현 내용을 간단히 하기 위해 DisplayOSType과 DisplayMemorySize 함수의 구현 내용을 메시지 박스를
출력하는 것으로 대체하였다. 아래에서 QueryInterface, AddRef, Release의 구현 내용을 보면 대충 앞서
IClassFactory의 QueryInterface 구현 내용과 흡사하다는 것을 느낄 수 있을 것이다. 그래서 COM 개발 툴을 이용할
경우에는 IUnknown을 직접 구현할 필요가 없다. 제공되는 것을 그냥 사용하면 된다.
STDMETHODIMP CMyInterface::QueryInterface(REFIID riid, LPVOID *ppReturn)
{
*ppReturn = NULL;
if(IsEqualIID(riid, IID_IUnknown))
*ppReturn = this;
else if(IsEqualIID(riid, IID_IMyInterface))
*ppReturn = (IOleWindow*)this;
if(*ppReturn)
{
(*(LPUNKNOWN*)ppReturn)->AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
STDMETHODIMP_(DWORD)CMyInterface::AddRef()
{
return ++m_ObjRefCount;
}
STDMETHODIMP_(DWORD)CMyInterface::Release()
{
if (–m_ObjRefCount ==
0)
{
delete this;
return 0;
}
return m_ObjRefCount;
}
STDMETHODIMP CMyInterface::DisplayOSType()
{
MessageBox(NULL, “It is
Windows 98”, “OS-Type”, MB_OK);
return S_OK;
}
STDMETHODIMP CMyInterface::DisplayMemorySize()
{
MessageBox(NULL,
“It is 128MB”, “Mem-Size”, MB_OK);
return S_OK;
}
COM 컴포넌트를 다 만들었으면 테스트 클라이언트 프로그램을 실행하기에 앞서 COM 컴포넌트를 레지스트리에 등록하는 작업을 해주어야 한다.
MyCom.dll이 생성된 디렉토리로 가서 Regsvr32 MyCOM.DLL을 수행하던가 MyCOM 프로젝트가 선택된 상태에서 Tools 메뉴의
Register Control 명령을 선택해 주어야 한다.
이 것으로 이번 회의 내용은 끝이 났다. 어쩌면 도대체 무슨 말을 하는 것인지 이해 못 하겠다고 말하는 독자들도 있을 것 같다. 처음에도
이야기했지만 COM에 대한 지식이 전무한 상태라면 본 내용은 아마 이해하기 힘들었을 것이다. COM에 대해 처음부터 차근차근 공부해보고 싶은
이라면 정보문화사에서 번역한 Beginning ATL COM Programming이란 책을 참고하기 바란다. 다음 회에서는 이 컴포넌트를 비주얼
베이직에서 사용할 수 있도록 수정해보도록 하겠다.
——————————————————————————–
Copyright
2000ⓒ 한기용 Last updated: 04/07/2005 14:14:46 Designed By 한기용