2.COM (OLE Component Object Model)

본장은 OLE의 기본형태인 COM에 대해서 설명합니다. COM은 OLE Component Object Model의 약자입니다. 즉 OLE에서 가장 기본적으로 만든 모델이 COM 인것입니다. 실제 프로그램에서는 COM을 사용하지는 않으나 OLE가 기본적으로 어떤형태로 구성되었는가를 이해하는데 도움이 되는 내용입니다.

대상자: 중급

Chapter 등급: 선택

공부방법:

본장의 내용은 그냥 어떤내용이 있는가 읽어보시는것이 좋을듯합니다. 그냥 넘어가지 마시고 한번정도는 읽어보세요.

1.COM이 무엇인가?

COM은 OLE의 가장 기본이 되는 컴포넌트를 의미합니다. OLE는 객체간에 서로 연결하여 필요한 부분을 상호 교환하며 프로그램을 구동하는 것을 의미합니다. OLE가 만들어진 큰목적은 객체지향과 재사용(resuse)입니다. 객체 지향적인 프로그램은  프로그램에서 하나의 모듈을 객체로 정의하는 것입니다. 이렇게 객체로 정의하고 객체에서 구동되는 문제는 객체 내부에 한한것으로 정의하는 방법입니다. 우리가 테레비젼을 보고자 할때 간단하게 리모콘 조작으로 테레비젼을 볼수가 있습니다. 그러나 테레비젼 안에는 전파를 받아서  신호를 해석하고 이것을 다시 영상으로 전환시키는 복잡한 여러 회로들이 가동되고 있습니다. 테레비젼을 보는 우리들은 테레비젼 안에서 발생하는 모든일에 대해서는 알지 못하고 또한 알필요가 없습니다. 다만 테레비젼이라는 객체를 통해서 방송을 보는 목적한 성취하면 되는 것입니다. 객체지향 프로그램이라는것이 바로 이부분입니다. COM은 여기서 부터 출발합니다. 필요한 객체를 만들고 이객체에서 우리가 필요한 부분만 호출하고 나머지 부분은 객체 내부에서 알아서 처리하고자 하는 것입니다. C++ 에서도 이런 객체지향의 의미가 통용됩니다. C++에서의 객체지향과 COM과의 차이는 무엇일까요? 재사용의 관점에서 차이가 있습니다. C++에서는 소스레벨에서의 재사용입니다. 즉 라이브러리나 소스위치에서 제사용을 할수 있는데 반하여 COM에서는 따로 독립적인 상태에서 재사용이라는 것입니다. 자동차 제작을 예로 설명을 드리자면 자동차에 물건을 실을수 있는 기능을 부과 하고자 할때 C++에서의 개념은 소스 레벨이기 때문에 엔진객체,차체객체 등등을 다시 조립하여 트럭이라는 새로운 자동차를 만드는 반면, COM 입장에서는 현재 자동차뒤에 고리를 달아서 물건을 싫을수 있는 바퀴달린 컨테이너를 연결시킨 형태입니다.

이런 의미에서 COM을 이용한다는 의미는 어떤 완성된 제품을 자신이 필요에 의해서 구입하여 조립하거나 부착시켜서 사용한다는 의미입니다.

이것은 소프트웨어가 산업으로 계속적으로 발전하면서 하드웨어를 제작하는 방법과 같이 소프트웨어도 발전하자는 의미에서 시작된것입니다.

용산 전사상가에서 컴퓨터를 구입한다고 했을때 용산전자상가에서는 컴퓨터를 조립하여 판매합니다. 즉 보드,메모리,HDD,모니터 등등의 각부품을 조립하여 구매자에서 판매하는 것입니다. COM의 개념이 이와 유사합니다. 필요한 어떤 소프트웨어를 제작하고자 할때 일일이 만들지 말고 필요한 부품들을 COM이나 ActiveX,OLE automation등으로 제작된 프로그램을 구입하여 이것을 조립하자는 의미입니다.

요즘 소프트웨어 엔지니어링 측면에서 본다면 소프트웨어를 조립하여 사용한다는것이 최대 이슈가 되어 있습니다. 이부분이 완벽하게 이루어 진다면 소프트웨어 산업은 엄청나게 발전되기 때문입니다. 윈도우 하에서 이런 방법을 지원하는것이 바로 OLE와 ActiveX인것입니다. 아직까지 이런 조립으로의 소프트웨어의 완성부분은 초기 단계에 있습니다. 그러나 언젠가는 이런쪽으로 발전하게 될것입니다. 만약 이 조립적인 소프트웨어 제작이 일반화 된다면 그때는 Visual C++를 이용하여 GUI나 또는 컨트롤 클래스를 매우 잘쓴다고 프로그래머로 곽광을 받던 사람은 몰락하게 되고, 저처럼 Visual C++ 프로그래밍 책을 써서 쪼금 유명해지는 사람 도 없어질것입니다. 살아 남는 프로그래머는 구현하기 힘든 알고리듬을 프로그래밍 하는 사람입니다.  예를 들어서 그래픽 에디터 프로그램을 만든다고 한다면 여러 그래픽 파일을 출력하는 프로그램, 그림 데이터를 수정하는 프로그램, 특수 효과를 주는 프로그램 등등을 조립하여 하나의 프로그램을 만드는 날이 올것이라는 것입니다. 이렇게 되면 용산전자상가에 가서 □그래픽 프로그램을 살려구요□ 라고 하면 □그래픽 파일을 출력하는 것은 만득사의 천득Show를 사용하시고요, 특수효과는 천순사의 만순 효과를 부착시키세요 만순효과는 그림의 효과가 만가지 기능이 됩니다.□ 하는 날이 올것입니다. 즉 프로그램의 모듈 하나하나를 조립할수 있기 때문에 작은 프로그램이면서 알고리듬이 좋은 프로그램은 다른 프로그램의 부품으로 많이 팔리게 됩니다. 그러나 전체적 프로그램 구성은 용산전자상가에서 컴퓨터 조립하듯이 되게됩니다,이렇게 되면 전체적으로 프로그램을 잘작성하는 사람들보다 알고리듬을 알고 있는 사람이 대우가 좋아지게 되는 것입니다. 또 예를 들어보겠습니다.어떤 프로그래머가 동영상을 MPEG 로 저장할때 정말 빠르게 인코딩하고 출력할때 정말 빠르게 디코딩하는 기술을 가지고 있습니다. 헌데 이사람은 이부분만 프로그래밍 할줄알지 다른 부분은 모릅니다. (MPEG를 저장하고 출력하는 것을 빠르게 만드는데도 정말 많은 시간이 걸리니까 다른 부분의 프로그램을 배울 시간이 없게 될수도 있습니다.) 요즘 같으면 이런 분들이 별로 대우가 좋지 않습니다. 그러나 조립식 소프트웨어 시대가 오면 이런분들은 살아남고 MFC라이브러리를 잘써서 프로그래밍을 하는 분들은 죄송하지만 용산전자상가에서 프로그램 조립하는 수준으로 전락할것입니다. 지금 이글을 보시는 분들중에 □무슨 소리를 하는것이냐!□ 라고 하시는 분들도 있을것입니다. 필자도 이렇게 되면 타격이 클것입니다. 누가 이렇게 두꺼운  Visual C++ 프로그래밍 책을 구입하여 보겠습니까? 그시간에 알고리듬 공부하지... 저도 프로그래머이기 때문에 이런 이야기를 하면 반감을 갖습니다. 허지만 요즘 AcitveX,OLE나 자바 등이 인기가 높아지는것은 무슨의미이겠습니까?  이것은 조립으로 가자는 의미가 됩니다.이제는 프로그래머 한사람이 몇일 밤새워 만든 프로그램은 상업성 가치가 없어지게 됩니다. 기존에 있는 기능을 합쳐서 멋지게 꾸몄다 하더라도 이런 프로그램은 조립시대에서는 누구나 할수 있는 것입니다. 조립시대가 앞으로 몇십년 후에 오는게 아니라 몇년이 안되어서 일반화 될것입니다. 이렇게 되면 또한번의 물결이 일어나고 (도스 시대에서 윈도우시대로 물결이 일어나듯) 프로그래머들의 위치가 바뀌게 될것입니다. 필자의 책에 알고리듬을 중요하다. MFC는 노우하우가 없다! 이런 이야기를 외치는 이유가 바로 이런 앞날이 있을것을 예상하기 때문입니다. 그렇다고 걱정을 할필요는 없습니다. 오히려 이때가 기회인것입니다. 조립시대가 들어서기 전이 과도기입니다. 과도기때 빨리 자리를 잡으면 빨리 발전할수가 있습니다.

자신만의 중요한 알고리듬을 구현하기 위해서는 결국 툴보다는 언어를 이용하여야 하며 언어를 이용하면서도 프로그램 결과는 GUI 환경에서 보기가 좋게 만드는 것이 좋습니다. 이런 컴파일러가 바로 Visual C++이며,이컴파일러를 이용하여 어떤 알고리듬을 구현할때, 툴같은 개념으로 화면을 디자인하면서도, C++이라는 언어로 알고리듬을 구축하기 때문에 다른 OS나 환경에 이식하기가 편리하다는 것입니다. 그래서 필자가 자꾸 본책을 공부할때 필요한 부분만 보면서 그리고 그냥 왜그런지 알고자하지 말고 예제에서 복사하여 사용하면서 남은 시간에 알고리듬 개발에 주력하라는 것입니다. 필자는 초기 XT때 부터 프로그래밍을 하여 지금 까지 온사람입니다. 그 기간동안 여러 변화를 거쳤지만 아직까지도 프로그래머로 인정을 받고 있습니다. 전 앞으로도 프로그래머로 인정을 받을겁니다. 그렇게 하기 위해서 저또한 지금은 알고리듬 개발에 노력하고 있습니다. 많은 변화를 거치면서도 프로그래머로 살아있다면 그리고 그런 프로그래머가 앞으로 살아남기 위해서 어떤 길을 정했다면 믿을만한 이야기가 아닐까요?

이번책에는 정말로 잔소리를 왠만하면 안할려고 하였는데 결국은 COM분야에서 하게 되었군요. 허지만 너무 중요한 내용이라 안할수가 없게 됩니다. 지금이 호기이기 때문에 국내의 프로그래머들이 세계를 주름 잡을수도 있을것 같기 때문입니다.

COM의 개념으로 다시 넘어가서, COM은 바로 이런 조립적인 프로그램을 만들자는 취지에서 나온 기본 모델입니다.  COM으로 프로그램을 제작하면 구동을 하고자 할때는 두개의 프로그램이 필요합니다. 서버와 클라이언트가 이것입니다. 그림 4-2-1은 COM이 구동되는 기본 형태입니다.

(그림 파일)

(그림 4-2-1)COM의 구동 형태

그림 4-2-1의 ⓐ는 COM을 이용하고자 하는 클라이언트 프로그램입니다.

ⓑ는 COM으로 제작된  서버 프로그램입니다. 이 서버 프로그램 내부에 ①과 ②는 외부에서 사용하고자하는 구동 객체이며 ③는 외부에서 사용하지 않는 내부적인 프로그램입니다. 앞에서 예를 든 테레비전에서 보면 ①과 ②는 조작 스위치나 또는 테레비젼 화면에 해당할것이며 내부의 복잡한 회로가 ③에 해당할것입니다. COM은 기본적으로 하나의 부속 프로그램이 있고 이부속프로그램을 외부에서 조립식으로 사용하는 형태이니다. COM에서는 클라이언트와 서버가 통신하고자 할때 하나의 표준을 정해 놓았습니다. 이 표준은 그림 4-2-2와 같습니다.

(그림 파일)

(그림 4-2-2)COM의 표준 형태

COM으로 프로그램을 제작하고자 할때는 3개의 기본 함수가 있어야 합니다. 그림 4-2-2의 에서 보듯이 QueryInterface ,AddRef,Release 이 3개가 바로 기본함수입니다. QueryInterface는 현재 COM프로그램이 클라이언트 프로그램과 연결이 되어 가동이 가능하다는 의미입니다. AddRef는 현재 서버에 연결된 클라이언트의 수를 증가시키거나 접속한 클라이언트 레퍼런스를 등록시킬때 사용하는 함수입니다. Release 는 현재 클라이언트와 서버의 접속이 종료되었다는 의미입니다. COM으로 프로그램을 작성하게 되면 그림 4-2-2의 3개의 항목을 수행하는 함수가 있어야 합니다. 이 3개의 함수는 연결과 종료를 처리하는 부분입니다. QueryInterfac와 AddRef는 연결이 되었을때 가동되는 함수이며 Release는 종료 되었을때 가동되는 함수입니다. 이 3개의 함수 다음으로  필동한 함수들 또는 구동객체들이 연결되어 있습니다. 3개의 함수에 필요한 기능적인 함수가 연결된다는 의미입니다. 그림 4-2-2와 같은 형태로 구조체가 정의되어 있습니다. 이구조체가 IUnknown 입니다. 이구조체는 다음과 같습니다.

struct IUnknown

{

QueryInterface() // 서로의 메시지를 연결시키는 함수

AddRef() //클라이언트가 증가될 때마다 번호를 증가시키는 함수

Release()//클라이언트가 감소될 때마다 해제를 하는 함수

}

IUnknown은 COM을 사용할때 기본적으로 사용하는 구조체입니다. 일반적으로 사용할때는 IUnknown을 바로사용하지 않습니다. 이것을 상속받은 또하나의 구조체를 이용합니다. 예를 들어서 덧셈을 하고자 하는 내용을 COM으로 만들고자 할경우 제일먼저 만들어야 할것이 IUnknown을 상속받은 구조체를 만드는 것입니다. 다음은 IUnknown의 구조체를 상속받은 덧셈 구조체 예입니다.

struct IAdd : public IUnknown

{

int m_nFirstNum;

int m_nSecondNum;

STDMETHOD_(void, AddData) (int first,int second)=0;

STDMETHOD_(int, GetValue) ()=0;

};

IAdd구조체는 IUnknown을 상속받은 구조체이기 때문에 기본적으로 QueryInterface와 AddRef,Release함수는 내장되어 있습니다. 이것과 함께 구동함수로 AddData와 GetValue함수가 있는 것입니다. STD_METHOD_ 형으로 사용한것은 이 두개의 함수가 외부에서 호출할수 있는 구동 함수이라는 선언입니다. COM으로 프로그램을 제작하고자할때는 다소 복잡한 내용을 수반합니다. 그림 4-2-3은 COM을 이용하여 프로그램을 제작할때의 전체 구성도입니다.

(그림 파일)

(그림 4-2-3)COM을 이용한 프로그램 구성

COM 으로 프로그램을 제작할때는 3가지 형태를 만들어주어야 합니다. 첫번째는 IUnknown구조체를 상속받은 새로운 객체를 만들고 이구조체를 클래스 내부에 인터페이스로 연결합니다. 이 것이 그림 4-2-3의 ⓐ항목입니다. 먼저 우리가 만들고자 하는 COM의 구조체를 그림 4-2-3의 ①과 같이 설정한것입니다. 이렇게 설정한후에 ①에서 설정한 IAdd구조체를 클래스 내부에 선언해야 합니다. 지금까지는 구조체를 선언하면 이구조체의 형을 가진 변수를 설정하는것이 일반적이었으나 COM을 만드는경우는 다릅니다. COM이란 조립의 개념입니다. COM을 하나의 형으로 선언하는 방법은 일반방법과 다릅니다. 먼저 그림 4-2-3에서 선언한 IAdd를 선언하는 방법을 보겠습니다.

IAdd Add;

위와 같이 하면 IAdd라는 형을 가진 Add라는 변수가 됩니다. 이런 형태로 클래스내부에서 IAdd 컴포턴트를  연결시키기 위해서는 BEGIN_INTERFACE_PART 매크로에 정의를 해주어야 합니다. 이내용이 그림 4-2-3의 ②항목입니다. BEGIN_INTERFACE_PART 매크로의 첫번째는 IAdd의 형을 선언하는 변수의 의미이며 두번째는 형의 의미입니다.  이렇게 선언한후에 IAdd에 선언되어 있는 구동 함수들을 기록하여 줍니다. 기록하는 형태는 그림 4-2-3의 ②항목과 같습니다. 구조체에 설정한 내용을 그대로 기입하되 □=0□의값을 삭제한 형태입니다. 이렇게 선언을 하고난후에 END_INTERFACE_PART(Add) 를 기록하면 매크로가 종료 됩니다. 이렇게 되면 내부적으로는 객체를 COM 형태로 정의한것입니다. 그러나 외부에서는 이것을 호출할때 객체의 이름을 이용하지 않습니다. 외부에서는  ID 즉 번호를 호출합니다.  그림 4-2-3 과같은 형태로 Ocomex1.dll이라는 프로그램을 만들었을경우 이 서버를 호출하고자 할때 "Ocomex1.dll"이라는 찾아서 프로그램을 구동하지 않고 윈도우 레지스트리에 있는 CLSID를 검색하고 해당 ID를 찾은다음 이 ID와 연결된 프로그램을 구동하는 형태가 됩니다. 그림 4-2-4는 윈도우 레지스트리에 설정되어 있는 CLSID입니다.

(그림 파일)

(그림 4-2-4) 윈도우 레지스트리

COM프로그램을 만들면 이프로그램을 윈도우 레지스트리에 등록을 해주어야 합니다. 등록을 하지 않으면 클라이언트에서 아무리 호출을 하고자 하여도 프로그램이 구동되지 않습니다. 다시 그림 4-2-3 으로 넘어가서 CLSID에 등록할 ID를 설정해 주어야 합니다. 이 ID를 그림 4-2-3에서는 ③과 같이 설정한것입니다. 이렇게 CLSID를 설정한후에  BEGIN_INTERFACE_MAP 에 INTERFACE_PART 수행매크로를 설정합니다. 이렇게 수행매크로를 설정할때 첫번째는 현재 프로그램의 클래스명과 다음에는 CLSID 그리고 3번째는 헤더에 구조체 형으로 선언한 IAdd의 변수를 설정합니다.

ⓐ와 ⓑ 두개의 항목을 설정한후에 ⓒ와 같이 함수를 선언하면 됩니다. ⓒ와 같이 함수를 선언하고 내부에서 필요한 내용을 설정하면 됩니다.

2.COM 프로그램 만들기

OCOMEX1 프로그램은 덧셈과 뺄셈을 계산해 주는 프로그램이고 OCOMEX2는 OCOMEX1을 호출하는 클라이언트 프로그램입니다.

APPWIZARD를 이용하여 새로운 프로젝트를 만듭니다. 프로젝트명은 Ocomex1이며 Mfc AppWizard(dll)를 선택합니다. 이렇게 한후 AppWizard Step1 에서 Automation항목을 체크해야 합니다. 체크를 하지않으면 COM또는 OLE를 구동할수 없습니다.

실질적으로 덧셈과 뺄셈을 하는 클래스를 만들어야 하는데 COM을 지원하는 기본 클래스는 CCmdTarget 클래스입니다. 이 클래스는 MFC 메시지를 서로 전달하는 기본 클래스이면서 OLE 서버의 각종 함수를 내장하고 있습니다.

CCmdTarget 클래스를 상속받은 CCalcul이라는 클래스를 하나 만들어야 합니다. 클래스 위자드를 실행시킨 후 AddClass->new를 선택하면 새로운 클래스를 만드는 창이 그림 4-2-2와 같이 나타납니다. 이 때 그림 4-2-2와 같이 입력하면 CCmdTarget 클래스를 상속받은 CCalcul 클래스가 생깁니다.

그림 4-2-2 에서서 Ole Automation 박스 안에 있는 라디오 버튼 세 개 중에서 본 프로그램은 Createable by type ID를 선택하고 활성화되어 있는 창 안에 calcul이라고 등록하였는데 클라이언트 프로그램에서 이 프로그램을 호출할 때 calcul명을 부르면 호출하게끔 설정한 것입니다.

(그림 파일)

(그림 4-2-5)CCalcul 클래스 만들때의 화면

Ocomex1 프로그램 안에는 두 가지 모듈이 설정됩니다. 첫번째는 덧셈을 하는 모듈이고 두 번째는 뺄셈을 하는 모듈입니다. 이 모듈들이 따로따로 객체를 가지기 위해서 하나의 구조체로 만듭니다. 덧셈을 하는 구조체는 IAdd라고 설정하고 뺄셈을 하는 구조체는 ISubtract라고 설정합니다. 이 안에는 더하거나 뺄 정수형 데이터 두 개와 데이터를 설정하는 함수, 계산하는 함수가 들어가게 됩니다.

정수형 데이타 두 개를 int m_nFirstNum, int m_nSecondNum이라고 설정하고 두 개의 정수형에 데이터를 기록하는 함수를 AddData라고 하며 더하거나 뺀 값을 알려고 하는 함수를 GetValue라고 설정한다면 덧셈을 하는 구조체는 다음과 같습니다.

struct IAdd {

int m_nFirstNum;

int m_nSecondNum;

void AddData(int,int);

int GetValue()

};

위와 같은 구조체에서 m_nFirstNum=1,m_nSecondNum=2를 설정하려면

IAdd->AddData(1,2);

라고 실행하면 되고 다음 두 개를 더한 값을 알려고 한다면

num=IAdd->GetValue();

와 같이 만들어 줍니다. 그럼 num에는 3이 기입될 것입니다. 그런데 이 구조체에서 우리가 덧붙여야 할 것이 있습니다. 우리가 이 구조체를 모듈화하는 것은 다른 프로그램에서 호출을 할 때 실행하게 만들기 위해서, 즉 서로 통신을 하기 위해서입니다. 통신이란 한쪽만 하는 것이 아니라 서로 대화를 하는 것이죠. 전화로 통신을 한다면 갑이 먼저 □여보세요.□하면 을이 □예, 말씀하세요.□ 이렇게 이쪽에서 묻고 물은 답을 저쪽에서 대답하고 하는 것이 통신이잖아요. 이런 것을 인터페이스라고 하는데 서로 인터페이스를 이쪽에서 저쪽으로 수행해 주는 장치가 전화기와 전화선이죠. 마찬가지로 윈도우에서 서로의 인터페이스를 수행하는 일을 해주는 클래스가 IUnknown이라는 클래스입니다. 이 클래스 안에는 QueryInterface라는 함수가 있습니다. 이것은 이쪽과 저쪽을 연결시켜 주는 함수입니다. 이쪽에서 전화로 □여보세요.□라고 하면 QueryInterface는 □여보세요.□를 그대로 상대방 프로그램에 전송시켜 주는 일을 합니다. 그리고 □예, 말씀하세요.□라고 하는 말을 그대로 받아서 다시 전달해 주는 일을 합니다.

전화에 한 라인에 3명 또는 4명이 한꺼번에 대화하는 3자 통신, 4자 통신이라는 것이 있지요. 덧셈을 하는 모듈은 하나지만 이것을 호출하는 클라이언트가 2개 또는 3개 이상일 수도 있습니다. 이 때 각각 다른 클라이언트들을 적절하게 매치하려면 처음에 요구하는 클라이언트에게 1번을 배정하고 또 다시 다른 어떤 클라이언트가 요구를 하면 번호를 하나 증가시켜서 2번을 배정하고 또 증가시켜 3번을 배정하면 서로가 혼선이 없겠죠. 이렇게 인터페이스가 설정될 때마다 하나씩 증가시키는 함수가 AddRef입니다. 1번을 배정받은 클라이언트가 □이제 연결을 끊겠소!□하고 연결을 끊어 버리면 현재 번은 비워져 있겠지요. 그럼 이쪽에서도 1번을 해제해야죠. 이렇게 해제하는 함수가 Release입니다.

즉 인터페이스를 실행하는 IUnknown 구조체 안에는 다음과 같은 함수가 있는 것입니다.

struct IUnknown

{

QueryInterface() // 서로의 메시지를 연결시키는 함수

AddRef() //클라이언트가 증가될 때마다 번호를 증가시키는 함수

Release()//클라이언트가 감소될 때마다 해제를 하는 함수

:

}

그렇다면 우리가 만든 IAdd 구조체는 IUnknown이란 구조체를 상속받아야겠지요. 그래서 위의 IAdd 구조체를 다음과 같이 변형합니다.

struct IAdd : public IUnknown

{

int m_nFirstNum;

int m_nSecondNum;

void AddData(int,int);

int GetValue()

};

위와 같이 만들었다 해도 완성된 것은 아닙니다. AddData이나 GetValue를 실행시키는 것은 현재 프로그램이 아니라 클라이언트 프로그램입니다. 클라이언트는 서버에게 어떤 일을 요구할 때 방법을 제시합니다.

□1과 2를 데이터로 가져가라.□라는 방법을 제시하는 것이 AddData이고 □계산을 해서 답을 알려다오.□라는 방법 제시가 GetValue입니다. 즉 AddData와 GetValue가 방법, 즉 메소드 입니다. 그렇기 때문에 메소드라고 알려 주어야 합니다. 메소드라고 설정하는 함수가  STDMETHOD_입니다.

이제 위 구조체를 다시 변형시키겠습니다.

struct IAdd : public IUnknown

{

int m_nFirstNum;

int m_nSecondNum;

STDMETHOD_(void, AddData) (int first,int second);

STDMETHOD_(int, GetValue) ();

};

위와 같이 한 후 만약 컴파일한다면 link에서 에러가 날 것입니다. AddData 함수를 찾을 수 없다고요. STDMETHOD_를 실행시키면서 이 함수를 모두 0으로 설정해 주어야 합니다. 이제 위의 구조체를 다시 변형하면

struct IAdd : public IUnknown

{

int m_nFirstNum;

int m_nSecondNum;

STDMETHOD_(void, AddData) (int first,int second)=0;

STDMETHOD_(int, GetValue) ()=0;

};

같이 됩니다. 마찬가지로 뺄셈을 하는 모듈도 다음과 같이 변하게 됩니다.

struct ISubtract : public IUnknown

{

int m_nFirstNum;

int m_nSecondNum;

STDMETHOD_(void, AddData) (int first,int second)=0;

STDMETHOD_(int, GetValue) ()=0;

};

MFC에서는 메시지를 매핑하기 위해서 BEGIN_MESSAGE_MAP을 사용한다는 것을 전에 설명했습니다. 한 개의 프로그램 안에 모듈이 여러 개라면 여러 개 모듈의 인터페이스로 매핑을 해주어야 합니다. 이렇게 하기 위한 매크로가 BEGIN_INTERFACE_MAP입니다. CCalcul 클래스 안에 다음과 같이 설정해 주어야 합니다.

//인터페이스를 매핑시킨다.

BEGIN_INTERFACE_MAP(CCalcul, CCmdTarget)

INTERFACE_PART(CCalcul, IID_IAdd, Add)

INTERFACE_PART(CCalcul, IID_ISubtract,Subtract)

END_INTERFACE_MAP()

여기에서 INTERFACE_PART(CCalcul,IID_IAdd,Add)란 말은 ID가 IID_IAdd이고 이것의 모듈은 Add로 연결되어 메시지가 매핑된다는 말입니다. 그런데 IID_IAdd라는 것을 설정해 준 적이 없지요. 따라서, 이것을 설정해 주어야 하는데 CCalcul.cpp 안에 다음과 같은 ID가 있을 겁니다.

// {49FC3C85-FC34-11CF-8CE0-444553540000}

static const IID IID_ICalcul =

{ 0x49fc3c85, 0xfc34, 0x11cf, { 0x8c, 0xe0, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };

우리는 이것을 사용하지 않고 위와 같은 ID를 IID_IAdd로 사용하여 변형시킵니다.

// {49FC3C85-FC34-11CF-8CE0-444553540000}

//덧셈 연산 함수 ID

static const IID IID_IAdd =

{ 0x49fc3c85, 0xfc34, 0x11cf, { 0x8c, 0xe0, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };

다음은 INTERFACE_PART(CCalcul,IID_IAdd,Add)에서 Add라는 것도 설정해 주어야 하는데 이것은 CCalcul.h에 정의합니다.

BEGIN_INTERFACE_PART(Add, IAdd)

STDMETHOD_(void, AddData) (int,int);

STDMETHOD_(int, GetValue) ();

END_INTERFACE_PART(Add)

BEGIN_INTERFACE_PART(Add, IAdd)란 Add가 IAdd 구조체를 갖는 형이라는 말이고 IAdd 구조체 안에 있는 메소드를 다음과 같이 설정한다는 말입니다.

STDMETHOD_(void, AddData) (int,int);

STDMETHOD_(int, GetValue) ();

정리를 하자면 다음과 같습니다.

① IUnknown을 상속받은 구조체를 만들고

② ID를 만들어서 BEGIN_INTERFACE_MAP에 INTERFACE_PART로 등록시키고

③ 헤더의 BEGIN_INTERFACE_PART에 METHOD를 설정한다.

마찬가지로 뺄셈도 만들어야겠지요. 구조체는 만들었으므로 ID를 만듭니다.

static const IID IID_ISubtract =

{ 0x49fc3c86, 0xfc34, 0x11cf, { 0x8c, 0xe0, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };

IID_ISubtract는 IID_IAdd 0x49fc3c85보다 번호를 하나 증가시켜 0x49fc3c86으로 설정합니다. 다음 BEGIN_INTERFACE_PART를 다음과 같이 헤더에 정의 합니다.

BEGIN_INTERFACE_PART(Subtract, ISubtract)

STDMETHOD_(void, AddData) (int,int);

STDMETHOD_(int, GetValue) ();

END_INTERFACE_PART(Subtract)

인터페이스를 실행할 수 있는 모든 것을 다 정의했으므로 이제는 실행하는 함수만 만들면 됩니다.

몇 가지 함수가 필요할까요? 다음과 같습니다.

1. Addref  2. QueryInterface  3. Release  4. AddData  5. GetValue

기본적으로 5개가 필요합니다. 그런데 우리는 모듈이 덧셈, 뺄셈 2개이므로 10개가 필요하죠. CCalcul 클래스 안에 Add로 설정된 IAdd 모듈의 Addref 함수를 만들려면 다음과 같이 씁니다.

STDMETHODIMP_(ULONG) CCalcul::XAdd::AddRef()

중요한 것은 Add 앞에 X가 붙었는데 BEGIN_INTERFACE_PART(Add, IAdd)로 설정해 주면 멤버는 자동적으로 m_x 함수는 X를 첫번째 파라미터로 추가합니다.

STDMETHODIMP_(ULONG) CCalcul::XAdd::AddRef()

{

METHOD_PROLOGUE(CCalcul, Add)

return pThis->ExternalAddRef();

}

AddRef에는 아무 일도 안 하고 그냥 증가만 하는 ExternalAddRef를 수행합니다. QueryInterface나 Release도 내장된 함수를 그대로 호출합니다.

STDMETHODIMP_(ULONG) CCalcul::XAdd::Release()

{

METHOD_PROLOGUE(CCalcul, Add)

return pThis->ExternalRelease();

}

STDMETHODIMP CCalcul::XAdd::QueryInterface(

REFIID iid, LPVOID* ppvObj)

{

char message[80];

sprintf(message,"Add 함수의 QueryInterface가 실행됩니다.");

AfxMessageBox(message);

METHOD_PROLOGUE(CCalcul, Add)

return pThis->ExternalQueryInterface(&iid, ppvObj);

}

다음은 AddData입니다.

STDMETHODIMP_(void) CCalcul::XAdd::AddData(int first,int second)

{

m_nFirstNum=first;

m_nSecondNum=second;

return;

}

클라이언트에서 보내진 first와 second를 IAdd 구조체의 m_nFirstNum과 m_nSecondNum에 넣습니다. 이제 GetValue를 실행하면 두 개의 수를 더해서 리턴합니다. GetValu 함수는 다음과 같습니다.

STDMETHODIMP_(int) CCalcul::XAdd::GetValue()

{

return m_nFirstNum+m_nSecondNum;

}

뺄셈의 모듈도 같은 방법으로 합니다. 본항목에서 제작한 CCalcul클래스의 전체 소스는 다음과 같습니다.

(프로그램 소스)

// Calcul.h : header file

//

/////////////////////////////////////////////////////////////////////////////

// CCalcul command target

struct IAdd : public IUnknown

{

int m_nFirstNum;

int m_nSecondNum;

STDMETHOD_(void, AddData) (int first,int second)=0;

STDMETHOD_(int, GetValue) ()=0;

};

struct ISubtract : public IUnknown

{

int m_nFirstNum;

int m_nSecondNum;

STDMETHOD_(void, AddData) (int first,int second)=0;

STDMETHOD_(int, GetValue) ()=0;

};

class CCalcul : public CCmdTarget

{

DECLARE_DYNCREATE(CCalcul)

CCalcul();           // protected constructor used by dynamic creation

// Attributes

public:

private:

// Operations

public:

// Overrides

// ClassWizard generated virtual function overrides

//{{AFX_VIRTUAL(CCalcul)

public:

virtual void OnFinalRelease();

//}}AFX_VIRTUAL

// Implementation

protected:

virtual ~CCalcul();

// Generated message map functions

//{{AFX_MSG(CCalcul)

// NOTE - the ClassWizard will add and remove member functions here.

//}}AFX_MSG

DECLARE_MESSAGE_MAP()

DECLARE_OLECREATE(CCalcul)

BEGIN_INTERFACE_PART(Add, IAdd)

STDMETHOD_(void, AddData) (int,int);

STDMETHOD_(int, GetValue) ();

END_INTERFACE_PART(Add)

BEGIN_INTERFACE_PART(Subtract, ISubtract)

STDMETHOD_(void, AddData) (int,int);

STDMETHOD_(int, GetValue) ();

END_INTERFACE_PART(Subtract)

DECLARE_DISPATCH_MAP()

DECLARE_INTERFACE_MAP()

};

/////////////////////////////////////////////////////////////////////////////

// Calcul.cpp : implementation file

//

#include "stdafx.h"

#include "Ocomex1.h"

#include "Calcul.h"

#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif

/////////////////////////////////////////////////////////////////////////////

// CCalcul

// {49FC3C85-FC34-11CF-8CE0-444553540000}

//덧셈 연산함수 아이디

static const IID IID_IAdd =

{ 0x49fc3c85, 0xfc34, 0x11cf, { 0x8c, 0xe0, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };

// {49FC3C86-FC34-11CF-8CE0-444553540000}

//뺄셈 연산함수 아이디

static const IID IID_ISubtract =

{ 0x49fc3c86, 0xfc34, 0x11cf, { 0x8c, 0xe0, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };

IMPLEMENT_DYNCREATE(CCalcul, CCmdTarget)

CCalcul::CCalcul()

{

EnableAutomation();

AfxOleLockApp();

}

CCalcul::~CCalcul()

{

// To terminate the application when all objects created with

//	with OLE automation, the destructor calls AfxOleUnlockApp.

AfxOleUnlockApp();

}

void CCalcul::OnFinalRelease()

{

// When the last reference for an automation object is released

// OnFinalRelease is called.  The base class will automatically

// deletes the object.  Add additional cleanup required for your

// object before calling the base class.

CCmdTarget::OnFinalRelease();

}

BEGIN_MESSAGE_MAP(CCalcul, CCmdTarget)

//{{AFX_MSG_MAP(CCalcul)

// NOTE - the ClassWizard will add and remove mapping macros here.

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

BEGIN_DISPATCH_MAP(CCalcul, CCmdTarget)

//{{AFX_DISPATCH_MAP(CCalcul)

// NOTE - the ClassWizard will add and remove mapping macros here.

//}}AFX_DISPATCH_MAP

END_DISPATCH_MAP()

// Note: we add support for IID_ICalcul to support typesafe binding

//  from VBA.  This IID must match the GUID that is attached to the

//  dispinterface in the .ODL file.

//인터페이스를 맵핑시킨다.

BEGIN_INTERFACE_MAP(CCalcul, CCmdTarget)

INTERFACE_PART(CCalcul, IID_IAdd, Add)

INTERFACE_PART(CCalcul, IID_ISubtract,Subtract)

END_INTERFACE_MAP()

// {49FC3C85-FC34-11CF-8CE0-444553540000}

IMPLEMENT_OLECREATE(CCalcul, "calcul", 0x49fc3c85, 0xfc34, 0x11cf, 0x8c, 0xe0, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0)

/////////////////////////////////////////////////////////////////////////////

// CCalcul message handlers

// IUnknown::AddRef   함수는 본 인터페이스를 호출할때마다 하나씩 증가시킵니다.

// 여러게의 클라이언트가 본 Add 를 호출하면 서로다른 포인터를 넘겨줍니다.

STDMETHODIMP_(ULONG) CCalcul::XAdd::AddRef()

{

METHOD_PROLOGUE(CCalcul, Add)

return pThis->ExternalAddRef();

}

//Decrements the reference count for the calling interface on a object. If the reference

//count on the object falls to 0, the object is freed from memory.

STDMETHODIMP_(ULONG) CCalcul::XAdd::Release()

{

METHOD_PROLOGUE(CCalcul, Add)

return pThis->ExternalRelease();

}

//Returns a pointer to a specified interface on a component to which a client

//currently holds an interface pointer. This function must call IUnknown::AddRef

//on the pointer it returns.

STDMETHODIMP CCalcul::XAdd::QueryInterface(

REFIID iid, LPVOID* ppvObj)

{

char message[80];

sprintf(message,"Add 함수의 QueryInterface가 실행됩니다. ");

AfxMessageBox(message);

METHOD_PROLOGUE(CCalcul, Add)

return pThis->ExternalQueryInterface(&iid, ppvObj);

}

STDMETHODIMP_(void) CCalcul::XAdd::AddData(int first,int second)

{

m_nFirstNum=first;

m_nSecondNum=second;

return;

}

STDMETHODIMP_(int) CCalcul::XAdd::GetValue()

{

return m_nFirstNum+m_nSecondNum;

}

// IUnknown::AddRef   함수는 본 인터페이스를 호출할때마다 하나씩 증가시킵니다.

// 여러게의 클라이언트가 본 Add 를 호출하면 서로다른 포인터를 넘겨줍니다.

STDMETHODIMP_(ULONG) CCalcul::XSubtract::AddRef()

{

METHOD_PROLOGUE(CCalcul, Subtract)

return pThis->ExternalAddRef();

}

//Decrements the reference count for the calling interface on a object. If the reference

//count on the object falls to 0, the object is freed from memory.

STDMETHODIMP_(ULONG) CCalcul::XSubtract::Release()

{

METHOD_PROLOGUE(CCalcul, Subtract)

return pThis->ExternalRelease();

}

//Returns a pointer to a specified interface on a component to which a client

//currently holds an interface pointer. This function must call IUnknown::AddRef

//on the pointer it returns.

STDMETHODIMP CCalcul::XSubtract::QueryInterface(

REFIID iid, LPVOID* ppvObj)

{

char message[80];

sprintf(message,"Subtract 함수의 QueryInterface가 실행됩니다. ");

AfxMessageBox(message);

METHOD_PROLOGUE(CCalcul, Subtract)

return pThis->ExternalQueryInterface(&iid, ppvObj);

}

STDMETHODIMP_(void) CCalcul::XSubtract::AddData(int first,int second)

{

m_nFirstNum=first;

m_nSecondNum=second;

return;

}

STDMETHODIMP_(int) CCalcul::XSubtract::GetValue()

{

return m_nFirstNum-m_nSecondNum;

}

(프로그램 소스끝)

3.클라이언트 프로그램 만들기

클라이언트 프로그램의 제작은 매우 간단합니다. Ocomex2가 클라이언트 프로그램입니다. AppWizard로 SDI형 EXE 프로젝트를 Ocomex2라는 이름으로 만든 다음 메뉴판에서 모든 것을 지우고 □계산기 부르기□라는 항목만 하나 만들어 놓습니다. 프로젝트를 만들 때 OLE OPTION에서 아무것도 링크시키지 말고 StdAfx.h에 다음과 같이 선언을 한 후

#include <afxole.h>

BOOL COcomex2App::InitInstance() 안에

AfxOleInit();        함수만 등록시킨다.

메뉴판에서 □계산기 부르기□를 선택하였을 때 다음과 같은 방법으로 실행하면 됩니다.

① 기본적인 ID와 팩토리 클래스 IUnknown 구조체 다음에 덧셈 구조체, 뺄셈 구조체 변수를 정의합니다.

CLSID clsid;

LPCLASSFACTORY pClf;

LPUNKNOWN pUnk;

IAdd* pAdd;

ISubtract* pSub;

② ID로부터 128비트의 ids를 얻습니다.

::CLSIDFromProgID(L"calcul", &clsid);

CCmdTarget으로 만든 CCalcul 클래스의 ID를 calcul이라고 정의했기 때문에 그림 1과 같이 윈도우 레지스터리에 calcul에 해당하는 CLSID가 우측에 나타나 있습니다. □49FC3C85-FC34-11CF-8CE0-444553540000□ 번호를 얻어서 이 번호를 clsid라는 변수에 저장하자는 말입니다.

③ clsid로부터 서버 프로그램을 찾습니다.

::CoGetClassObject(clsid, CLSCTX_INPROC_SERVER,

NULL, IID_IClassFactory, (void **) &pClf);

2번에 의해서 얻어진 clsid를 가지고 CoGetClassObject 함수를 이용하여 서버 프로그램을 얻습니다. 서버 프로그램의 포인터는 pClf와 연결됩니다.

④ 서로가 통신할 수 있도록 서버와 클라이언트의 IUnknown을 연결시킵니다.

pClf->CreateInstance(NULL, IID_IUnknown, (void**) &pUnk);

IUnknown형인 pUnk에 서버와 연결되어 있다.

⑤ pUnk의 QueryInterface를 실행시킵니다.

pUnk->QueryInterface(IID_IAdd, (void**) &pAdd); // 덧셈쪽

pUnk->QueryInterface(IID_ISubtract, (void**) &pSub); // 뺄셈쪽

⑥ 덧셈 모듈에 1과 2를 넣습니다.

pAdd->AddData(1,2);

⑦ 1／2의 답을 n1에 넣습니다.

int n1;

n1=pAdd->GetValue();

⑧ 뺄셈 모듈에 2와 1을 넣습니다.

pSub->AddData(2,1);

⑨ 2１1의 답을 n2에 넣습니다.

int n2;

n2=pSub->GetValue();

⑩ 이것을 메시지 다이얼로그로 화면에 출력합니다.

char temp[80];

sprintf(temp,"1 + 2 = %d \r\n2 - 1 =%d",n1,n2);

AfxMessageBox(temp);

⑪ 이제 모든 것을 해제합니다.

pClf->Release();

pUnk->Release();

pAdd->Release();

pSub->Release();

COM 클라이언트가 DLL 서버를 부르는 루틴은 위와 같습니다. 새로운 프로그램을 만들 때도 6, 7, 8, 9, 10번의 내용만 변경하면 될 것입니다.

4.프로그램 실행

프로그램을 실행하기 이전에 서버프로그램을 윈도우 레지스트리에 저장해야 합니다. 제공되는 CD에 보면 Part4Chapter 에 RegeAdd프로그램이 있습니다. 이프로그램을 이용하여 COM파일을 레지스트리에 저장할수 있습니다. 이프로그램은 간단하게 파일 대화상자가 실행되고 이파일대화상자에서 설정한 파일을 레지스트리에 등록합니다. InitInstance함수에서 다음과 같이 코딩함으로 레지스트리에 DLL이나 OCX파일을 등록하는 부분이 끝나게 됩니다.

CFileDialog dlgFile(TRUE,NULL, NULL, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,

"DLL files (*.dll)|*.dll|OCX files (*.ocx)|*.ocx||");

dlgFile.m_ofn.lpstrTitle = "DLL 를 OLE서버로 등록하기";

if(dlgFile.DoModal() != IDOK)  return FALSE;

CString strDllPath = dlgFile.GetPathName();

HINSTANCE h = ::LoadLibrary(strDllPath);

if(h == NULL) {

CString msg;

msg.Format("서버를 찾는데 실패했습니다. %s", strDllPath);

AfxMessageBox(msg);

return FALSE;

}

FARPROC pFunc = ::GetProcAddress((HMODULE) h, "DllRegisterServer");

if(pFunc == NULL) {

AfxMessageBox("서버 펑션을 등록하는데 실패했습니다.");

return FALSE;

}

(*pFunc)();	// call the function to register the server

AfxMessageBox("서버를 등록하였습니다.");

레스트리에 등록하는 방법은 Explict 링킹방법으로 DLL를 로드하고 □DLLRegisterServer"이라는 함수를 찾은 다음이것을 실행시키면 됩니다. 모든 DLL이 이함수가 있는것이 아닙니다. COM이나 OLE automation등이 이 함수가 내장되어 있습니다. 이함수를 실행시키면 자동적으로 레지스트리에 등록이 됩니다. DLL를 Explict로 로드하는 방법은 Part2의 1장에서 설명하였습니다.