남의 글 도배 다.

com 에 관한글 10회를 올리고 또 올린다.

3회 정도에 걸쳐 COM(Component Object Model)에 대해 자세히 알아보고자 한다. 그 동안 본인을 비롯한 여러 필자들이
COM에 대한 이야기를 해왔지만 자세하게 다룬 경우는 드물었다. 사실 COM을 자세하게 밑바닥부터 이해할 필요는 없다. 개념적으로만 충실히
이해했으면 프로그래밍은 COM에 대한 세부적인 지식 없이도 얼마든지 잘 할 수 있다. MFC나 비주얼 베이직, ATL(ActiveX
Template Library), 델파이 등의 개발 도구로 ActiveX 컨트롤이나 ActiveX 코드 컴포넌트 등을 만들어 본 이라면 필자의
말에 공감할 수 있을 것이다. COM에 대해 잘 몰라도 얼마든지 COM 기반의 컴포넌트를 만들어 사용할 수 있다(MFC 보다는 ATL이 COM에
대한 지식을 필요로 하며 비주얼 베이직은 COM에 대해 거의 몰라도 관계없다). 필자가 설명하고자 하는 것은 COM 컴포넌트를 위에서 열거한
MFC나 ATL의 도움을 받지 않고 C++ 언어만을 이용해 만드는 방법이다. 이 과정을 한 번 거쳐 보면 COM기반의 컴포넌트가 어떤 구조를
갖는지 가장 분명하게 이해할 수 있다. 하지만 그만큼 어렵다는 것도 체험할 수 있을 것이다. 본 회에서는 COM에 대한 개념적인 이야기를
하고(프로그램 세계 98년 1월호에 필자가 연재했던 내용을 정리, 요약했다고 보면 될 것이다) 다음 회부터는 실제로 C++ 언어만을 이용해
COM 컴포넌트를 만드는 작업을 설명하도록 하겠다.

——————————————————————————–
참고
: COM 개발 도구 : ATL, MFC, 비주얼 베이직

COM 관련 개발을 하는데 사용할 수 있는 개발 도구로는 ATL, MFC, 비주얼 베이직(델파이)이 있다. 그렇다면 언제 어떤 개발 도구를
사용하는 것이 좋을까 ? 필자의 개인적인 견해이긴 하지만 데이터베이스 관련 컴포넌트를 만드는 것이라면 비주얼 베이직이나 델파이가 제일 좋다.
아주 쉽고 빠르게 만들어낼 수 있다. 그 이외의 기능을 하는 컴포넌트를 만드는 것이라면 자신이 익숙한 환경을 사용하는 것이 좋다. 즉 비주얼
C++를 주요 개발 환경으로 사용하는 사람이라면 ATL이나 MFC를 사용하면 되고 비주얼 베이직을 많이 사용하는 사람이라면 비주얼 베이직을
사용하면 된다는 이야기이다. 다만 비주얼 베이직에서 사용할 수 없는 기능이 필요한 경우에는 어쩔 수 없이 ATL이나 MFC를 사용하면 된다.
ATL과 MFC는 어떤 차이가 있을까 ? 일단 난이도는 ATL이 더 높다. 어려운 대신에 빠르고 크기가 작은 모듈을 만들어 낼 수 있다는 장점이
존재한다. COM 컴포넌트 프로그래밍에 관한한 MFC는 비주얼 베이직과 ATL 사이에 끼어 굉장히 어쩡쩡한 위치를 차지하고 있다.

——————————————————————————–

편리한 라이브러리나 개발 도구를 사용하지 않고 C++ 언어만을 사용하여 COM 컴포넌트를 만들어 보는 것은 어디까지나 설명을 위한 것이다.
여러분이 진짜로 COM 컴포넌트를 만들 때는 MFC나 ATL 등의 편리한 개발 도구를 사용하는 것이 좋다. 아니 사용해야할 것이다.

COM, OLE, ActiveX

책이나 웹을 보다보면 COM, OLE(Object Linking and Embedding), ActiveX 등의 용어가 쏟아져 나온다.
비슷한 것 같긴 한데 이름이 다른 걸 보면 다른 게 분명히 있을 것 같기도 하고 굉장히 혼란스러운 것이 사실이다. 결론부터 이야기하자면 거의
혼용되고 있다고 보면 될 것 같다. 다만 OLE라는 말은 사장되는 추세이고 COM과 ActiveX라는 말이 사용되고 있다는 점은 기억해두기
바란다.

COM은 마이크로소프트가 만든 컴포넌트 모델이다. 즉 컴포넌트의 모듈 형태와 그러한 컴포넌트를 사용하기 위한 방법을 나름대로 표준화한
것이다. S/W의 재사용성을 증대하기 위해 만들어진 것이 컴포넌트였는데 이것 자체도 표준화가 안 되어 문제가 많았다. 이를 해결하기 위해
마이크로소프트가 만든 표준 컴포넌트 방안이 바로 COM인 것이다. COM인 윈도우 운영체제의 기본 모듈로 포함되며 OLE나 ActiveX의
근간이 된다. OLE는 COM을 바탕으로 마이크로소프트가 만들어낸 컴포넌트 기술이다. OLE 컨트롤이나 OLE 복합 문서(Compound
Document), OLE 자동화(Automation) 등이 이에 포함된다. 이를 인터넷 환경하에서 동작하도록 수정한 것이 바로
ActiveX이다. 여기에는 액티브X 스크립팅 등의 기술이 추가되었다. 따지고 보면 ActiveX는 마이크로소프트가 마케팅적인 이유로 마치
새로운 것처럼 만들어낸 OLE의 새로운 이름 정도 밖에 안 된다.

COM 컴포넌트 – 모듈 형태

개념적으로 볼 때 COM 컴포넌트는 메소드와 프로퍼티(경우에 따라서는 이벤트까지)를 갖는 하나의 객체이다. VB나 델파이와 같은 RAD
프로그래밍 경험이 있는 사람이라면 잘 이해할 수 있을 것이다. 컴포넌트 프로그래밍 이전에 사용하던 함수 라이브러리에 비해 사용하기 쉽다는 것이
가장 큰 장점이 된다. 또한 윈도우 환경이라면 거의 모든 프로그래밍 언어와 데스크탑 뿐만 아니라 인터넷 환경에서도 사용할 수 있다는 것도 큰
장점이 된다.

이러한 COM 컴포넌트는 실행 파일이나 DLL의 형태를 갖게 된다. 실행 파일의 형태를 가지면 이를 아웃프로세스(Out-Process)
컴포넌트라고 하며 DLL의 형태를 가지면 이를 인프로세스(In-Process) 컴포넌트라고 한다. 아웃프로세스는 속도는 느리지만 안정성이 좋고
인프로세스는 속도는 빠르지만 안정성이 떨어진다. 이러한 이유는 인프로세스 컴포넌트는 DLL이기 때문에 자신을 사용하는 프로그램과 같은 주소 공간
내에 놓이게 되기 때문이다. 호출시 주소 변환 과정이 필요없기 때문에 속도는 빠르지만 한 지붕내의 가족이기 때문에 컴포넌트에서 발생한 오류로
인해 사용자 프로그램까지 다운되어 버리는 폐단이 있을 수 있다. 반면에 아웃프로세스 컴포넌트는 실행 파일이기 때문에 자신을 사용하는 프로그램과
별도의 주소 공간에 놓인다. 따라서 호출시 주소 변환 절차가 들어가므로 속도가 느리지만 주소 공간이 다르므로 다른 집에 사는 가족이기 때문에 한
쪽의 에러가 다른 쪽에 심각한 영향을 끼치지는 못 한다. 아웃프로세스 컴포넌트를 호출하면 호출시 주소 공간의 차이로 인한 변환 과정이 끼어
드는데 이를 마샬링(Marshaling)이라고 한다. 마샬링은 COM에서 알아서 수행해주기 때문에 아웃프로세스 컴포넌트를 사용하느냐 아니면
인프로세스 컴포넌트를 사용하느냐에 따라 사용 프로그램 쪽의 코딩이 달라지지는 않는다. 또한 사용 프로그램의 입장에서도 입장에서 볼 때 사용 중인
컴포넌트가 DLL이냐 실행 파일이냐를 구분할 필요가 없다는 것이다. 아웃프로세스 컴포넌트를 사용할 경우에는 COM에서 컴포넌트 쪽에는
스텁(Stub)이라는 모듈을 올리고 사용 프로그램 쪽에는 프록시(Proxy)라는 것을 올려서 마치 인프로세스 컴포넌트를 사용하는 것 같은 환경을
만들어 준다. 마샬링은 프록시와 스텁이 알아서 수행해준다. 인프로세스로 컴포넌트의 모듈 형태를 선택하는 것이 요즘의 추세이다.

그리고 하나의 모듈 안에 둘 이상의 컴포넌트가 존재해도 무방하다. 예를 들어 MyCom.dll이라는 인프로세스 컴포넌트가 있다면 그 안에
꼭 하나의 컴포넌트만 존재해야 하는 것은 아니라는 이야기이다.

COM 컴포넌트 – 레지스트리

세상에는 수많은 컴포넌트들이 존재한다. 사람도 태어나면 이름을 주듯이 이들을 구별할 수 있는 이름이 있어야 할 것이다. 사람은 문자로
이름을 부여하지만 COM 컴포넌트에는 128비트의 숫자 이름을 부여한다. 이런 숫자 이름을 클래스 ID(CLSID)라고 부른다. 이 값으로
전세계적으로 유일한 값이어야 한다. 유일성의 보장은 CLSID 생성 도구에서 보장해준다. 사실 100% 보장이 된다고 보기는 힘들지만
128비트라는 숫자의 크기가 엄청나기 때문에 아무나 임의로 만든다고 해도 같은 생길 확률은 거의 제로에 가깝다. 참고로 VC에서 COM
컴포넌트를 만들면 Guidgen.exe라는 유틸리티가 이 것의 생성을 담당한다. 그런데 128비트씩이나 되는 숫자는 사람이 기억하기 어렵기
때문에 이를 문자열화하여 기억하기 쉽도록 만든 이름도 있는데 이를 ProgID라고 한다. CLSID와 ProgID는 모두 레지스트리의
HKEY_CLASSED_ROOT 밑에 등록된다. 예를 들어 HTML 도움말 컴포넌트에 해당하는 HHCtrl.ocx라는 것이 있다. 이 것의
CLSID와 ProgID를 알아보자. 먼저 레지스트리 편집기를 실행한다. 그래서 HKEY_CLASSED_ROOT 밑을 뒤져보면
Internet.HHCtrl이란 것을 찾을 수 있을 것이다(인터넷 익스플로어 4.0 이상이 설치되어 있지 않은 시스템에서는 찾을 수 없을
것이다). 이 것이 바로 ProgID라는 것이다. 요 놈을 펼쳐보면 서브키로 CLSID라는 것이 있을 텐데 여기에 이 ProgID에 해당하는
실제 CLSID가 존재한다. 참고로 Internet.HHCtrl의 CLSID는
{ADB880A6-D8FF-11CF-9377-00AA003B7A11}이다. HKEY_CLASSES_ROOT 밑에 보면 CLSID라는 서브키도
있는데 이 밑에는 이 시스템에 설치된 모든 컴포넌트의 CLSID가 놓인다. 여기서 앞의 Internet.HHCtrl의 CLSID인
{ADB880…..}을 찾아보면 이 컴포넌트의 위치 등의 보다 더 자세한 정보를 알아낼 수 있다. 아웃프로세스 컴포넌트의 경우에는
LocalServer32라는 서브 키에 컴포넌트의 디스크 상에서의 절대 경로가 위치한다. 인프로세스 컴포넌트의 경우에는
InProcServer32라는 서브 키에 컴포넌트의 경로명이 놓인다.

여기서 알 수 있듯이 특정 컴포넌트를 사용하려면 이것이 디스크에 존재하는 것만으로는 불충분하다. 이를 레지스트리에 등록해야 한다. 그래야만
COM에서 이를 보고 사용할 수 있기 때문이다. 모든 COM 컴포넌트는 자신을 레지스트리에 등록하고 등록을 취소하는 함수를 자체적으로 제공해야
한다. 인프로세스 컴포넌트의 경우에 그 함수들의 이름은 다음과 같이 정해져 있다.

DllRegisterServer : 컴포넌트를 레지스트리에 등록하는데 사용된다.
DllUnregisterServer :
레지스트리에 등록된 컴포넌트를 레지스트리에서 제거한다.
COM 컴포넌트를 많이 사용해본 사람이라면 Regsvr32라는 컴포넌트 등록
유틸리티를 많이 사용해봤을 것이다. 이 유틸리티는 인프로세스 컴포넌트를 레지스트리에 등록하고 제거하는 역할을 담당한다. 이 유틸리티는 바로 위의
두 함수를 이용하는 아주 간단한 구조로 이루어져 있다. 아웃프로세스 컴포넌트는 명령행 인자를 분석하여 자신을 레지스트리에 등록하고 제거하는
역할을 수행한다. /RegServer라는 것을 주고 아웃프로세스 컴포넌트를 실행하면 레지스트리에 등록이 되고 /RegUnServer라는 것을
주면 레지스트리에서 제거한다.

COM 컴포넌트 – 인터페이스
이제 이야기를 조금 더 구체적으로 전개해보자. 앞에서 이야기한 것처럼 COM 컴포넌트는 사용하는
개발자의 입장에서 보면 메소드, 프로퍼티(경우에 따라서 이벤트)를 사용하여 제어할 수 있는 객체에 해당한다. 이러한 메소드와 프로퍼티는 모두
인터페이스(Interface)라는 것으로 구성된다. 자바를 공부해본 사람이라면 인터페이스에 대해 들어봤을 것이다. 그 인터페이스와 같은 것이다.
즉, 멤버 함수로만 구성되는 클래스이다. 프로퍼티의 경우에 우리가 쓸 때는 마치 변수처럼 쓰지만 이 것도 결국은 함수 호출로 변경된다는 점을
미리 알아두기 바란다. 예를 들어 A라는 객체에 long 타입의 Value라는 프로퍼티가 있다면 이는 COM 인터페이스에서 두 개의 함수로
구현된다. 예를 들어 get_Value와 put_Value와 같은 함수로 구현이 되는데 Value라는 프로퍼티에 어떤 값을 대입하는 코드를
사용하면 put_Value 함수가 호출이 되고 Value 프로퍼티의 값을 사용하면 get_Value 함수가 호출된다. VB에서 다음과 같은
코드를 사용했다고 하자.

A.Value = 30
MsgBox CStr(A.Value)

위의 코드는 실제 COM 컴포넌트 호출시에는 다음과 같은 식으로 호출된다고 생각하면 된다.

A.put_Value(30) ‘ A.Value = 30
long lValue = A.get_Value()

MsgBox(CStr(lValue))

아무튼 COM 컴포넌트는 하나 이상의 인터페이스로 구현되며 각 인터페이스는 관련있는 함수들의 집합이다. 모든 COM 컴포넌트는
IUnknown이라는 최상위 선조 인터페이스로부터 계승되어야 한다. 이 인터페이스에는 3개의 멤버 함수(AddRef, Release,
QueryInterface)가 존재한다. 따라서 모든 COM 컴포넌트는 이 3개의 함수는 기본적으로 갖고 있다. 인터페이스도 구별을 위해 ID를
갖는다. 이 역시 CLSID처럼 128비트의 숫자로 구성된다. 이를 인터페이스 ID(IID)라고 부른다.

AddRef와 Release는 레퍼런스 카운팅을 통한 컴포넌트의 활성화 여부를 결정하기 위해 사용한다. 이 컴포넌트에 대한 레퍼런스가 생길
때마다 사용자 프로그램에서는 그 컴포넌트의 AddRef 메소드를 호출하여 레퍼런스 카운트를 하나 증가시켜야 한다. 더 이상 그 컴포넌트를 사용할
일이 없어지면 Release 메소드를 호출해야 한다. Release 메소드에서는 내부적으로 유지하는 레퍼런스 카운트가 0이 되면 자신을
메모리에서 제거해야 한다. QueryInterface는 컴포넌트에게 갖고 있는 인터페이스를 달라고 요청하는데 사용된다. 첫 번째 인자로 원하는
인터페이스의 ID를 지정하고 두 번째 인자로 그 인터페이스에 대한 포인터를 받을 변수의 주소를 지정해야 한다(말이 좀 어려운데 포인터의 포인터를
지정해야 한다). 즉, 이 메소드는 실제로 그 컴포넌트에서 자신이 사용하고자 하는 인터페이스를 받아오는데 사용된다. 이 메소드는 여러 모로
유용하게 사용될 수 있다. 흔히 버전닝(Versioning)이라고 하는 기능을 수행하는데 쓰일 수 있는데 이에 대한 이야기는 다음 회에서 조금
더 자세히 하도록 하겠다.

조금 더 구체적인 예를 들어보도록 하자. GetMemorySize라는 메소드를 갖는 ISystemInfo라는 인터페이스를 구현한다고
해보자. 일단 모든 인터페이스는 앞에 I를 붙여 표시하는 것이 일반적이다. 당연히 IUnknown으로부터 계승이 되어야할 것이다. 이를 C++로
표현하면 다음과 같은 식이 될 것이다. 아래 클래스의 자세한 구현 방법에 대해서는 다음 회에서 언급하도록 하겠다.

class ISystemInfo : public IUnknown
{
AddRef…
Release…

QueryInterface…
// ….
GetMemorySize…
};

IUnknown 이외에도 모든 컴포넌트는 COM에서 자신을 쉽게 생성할 수 있도록 하는 용도로 IClassFactory라는 인터페이스를
지원해야 한다. 이 것 역시 IUnknown 인터페이스로부터 계승되어야함은 물론이다. 이 인터페이스의 목적은 요청된 컴포넌트를 COM에서 실제로
생성하는데 있다. 이 인터페이스에는 CreateInstance와 LockServer 두 개의 멤버 함수가 존재한다. CreateInstance가
실제로 컴포넌트를 생성하는데 사용된다. 그러면 잠깐 컴포넌트와 컴포넌트 사용자간의 상호 동작을 한 번 살펴보도록 하자. 그림 1과 함께 보기
바란다.

1&gt 어떤 프로그램에서 특정 컴포넌트를 사용하려면 그 컴포넌트의 ProgID나 CLSID를 지정해야 한다. 예를 들어
MyCom.MyObj라는 이름을 갖는 컴포넌트(ISystemInfo라는 인터페이스)가 있고 이를 사용하는 클라이언트가 VB로 작성되었다고 하자.

Set MyObj = CreateObject(“MyCom.MyObj”)

위의 코드에서 명시적으로 보이지는 않지만 프로그램이 시작할 때 COM 라이브러리를 초기화하기 위해 CoInitialize라는 초기화 함수가
호출된 상태이다. 종료시에는 CoUninitialize라는 함수를 호출해야 한다.

2&gt 위에서처럼 ProgID를 지정하면 COM에서 이를 CLSID로 변경하여 사용한다. 그 다음으로 COM은 레지스트리에서 해당
CLSID 정보를 뒤져 컴포넌트의 위치를 찾는다. 찾아서 이를 메모리로 로드한다. 이 작업은 SCMService Control
Manager)라는 COM내의 모듈이 담당한다.

3&gt 컴포넌트가 들어있는 파일이 메모리에 로드되었으면 거기서 IClassFactory 인터페이스에 대한 포인터를 얻는다. 그럴
목적으로 모든 컴포넌트는 자신의 익스포트 함수로 DllGetClassObject라는 함수를 구현해야 한다.

DllGetClassObject : 운영체제가 컴포넌트를 생성하기 위해 해당 모듈의 IClassFactory 인터페이스에 대한 포인터를
얻어내는데 사용된다.
COM에서는 이를 통해 그 컴포넌트의 IClassFactory 인터페이스를 얻어낸 다음에 그것의 멤버 함수인
CreateInstance를 호출한다. 그래서 실제로 컴포넌트의 객체를 생성하고 포인터를 얻어낸 다음에 그 포인터를 리턴한다.

4&gt 해당 컴포넌트의 원하는 메소드나 프로퍼티를 사용한다.

Dim iMem As Long
iMem = MyObj.GetMemorySize()

&lt 그림 1. 컴포넌트 사용 절차 &gt

위에서처럼 COM 컴포넌트를 비주얼 베이직과 같은 RAD 개발 도구에서 사용할 수 있으려면 사실 그 컴포넌트에서 IDispatch라는
인터페이스도 구현해야 한다. 요즘처럼 컴포넌트들이 대부분 RAD 환경하에서 사용되는 세상에서는 반드시 구현해주어야 할 인터페이스인 셈이다. 이
인터페이스에 대해서는 다다음 회에서 자세히 알아볼 것이다. 사실 COM 인터페이스 자체는 포인터라는 개념하에 동작하도록 되어 있기 때문에
포인터를 모르는 대부분의 RAD 환경하에서는 사용할 수가 없다. 그래서 COM에 추가된 것이 바로 IDispatch라는 인터페이스이다.

위의 절차를 다시 보면 내가 만들어야 하는 직접적인 ISystemInfo라는 인터페이스 이외에도 IUnknown,
IClassFactory, IDispatch 인터페이스를 별도로 구현해주어야 한다. 사실 이 인터페이스들은 누가 구현해도 내용이 비슷할 수 밖에
없는 부분들이기 때문에 ATL이나 MFC, VB 등의 상위 레벨의 개발 도구를 사용하면 알아서(?) 구현해준다. 하지만 직접 COM 컴포넌트를
만들려 한다면 일일이 다 코딩해주어야 한다. 이런 노가다를 다음 회부터 해보겠다는 것이다. 이는 COM에 대해 자세히 알아보자는 목적으로 하는
것이지 COM 컴포넌트는 이렇게 구현하자는 의미는 아니므로 오해없기 바란다.

다음 회에는 실제로 C++로 COM 컴포넌트를 구현해보도록 하자.

——————————————————————————–
Copyright
2000ⓒ 한기용  Designed By 한기용

Leave a Reply

Your email address will not be published. Required fields are marked *