드디어 마지막입니다.
너무 오랫동안 잠수를 하였군요. ^^;
확실히 생각보다 글을 쓴다는 것은 힘든 일입니다. 뒤로 가면 갈수록 내용은 부실해지고 날림공사가 되어 가는
기분입니다. 아무래도 빨리 끝내야겠다는 맘이 이런 결과를 가져오지 않았나 싶습니다. 이게 생각보다 큰 부담감으로 작용하더군요. 이런 강좌 아닌
강좌가 처음이라서 그럴 수도 있을 것 같기는 한데, 다음에는 좀 여유를 가지고 천천히 일관 되도록 써봐야겠습니다. COM 경험담의 마지막인 만큼
유종의 미를 거둬야 겠죠. ^^;
그럼 시작하겠습니다.
오늘은 커넥션 포인트(Connection Point)에 대해 중점적으로 다루고 나머지 다루지 못했던 부분인 타입
정보와 코드 재사용을 위한 통합과 포함에 대한 간단한 이해와 지금까지 해왔던 내용에 대한 요약을 해 보도록 하자.
먼저 커넥션 포인트다. 이 커넥션 포인트와 함께 등장하는 싱크라는 개념 역시 중요하다. 천천히 알아보자. 여기서
잠시 인터페이스를 보자면, IConnectionPointContainer, IEnumConnectionPoints,
IConnectionPoint, IEnumConnections 이 네 개의 인터페이스가 커넥션 포인트를 지원하기 위한 인터페이스이다.(음..
여기서 자세하게 알 필요가 없다. 이 글 끝에 첨부한 내용을 참고하면 되겠다.)
그럼 도대체 커넥션 포인트가 무엇이란 말인가? 대충 짐작이 가능하듯이 뭘 연결해준다는 의미 같아 보인다.
간단하다. COM 서버에서 어떤 일이 발생했음을 클라이언트에게 알릴 수 있도록 해주는 놈이 바로 이 커넥션 포인트이다. (이놈이 연결하고 무슨
상관이냐고? 좀더 지켜 보도록 하자.) 지금까지 우리는 클라이언트에서 일방적으로 서버에게 서비스를 요청하는 얘기만 해왔다. 하지만, 이제는
서버에서도 클라이언트에게 어떤 이벤트의 전달이나 서비스를 요청할 수도 있다는 걸 알아야 한다.
아직 무슨 말인지 잘 모르겠다고? 간단하다. 지금까지의 내용을 다시 떠 올려보자. 우리는 클라이언트에서 COM
개체를 생성해서 그 인터페이스를 얻어내고 그 인터페이스를 통해 메서드를 호출을 통해 그 서비스를 사용하였다. 하지만, COM 개체도 자존심은
있다. 왜 맨날 주기만 해야만 하는가? 나도 좀 받아보자라고 충분히 생각할 수 있다. 그렇다고 해서 클라이언트의 메서드를 호출할 수는 없는
일이다. 왜냐? 클라이언트가 어떤 메서드를 구현할지 어떻게 알겠는가? 알 방법이 없다. 즉, 그 메서드의 메모리 번지가 어디에 생성될지는
컴파일러만이 알 것이다. 결국 우리는 이 메모리 번지를 알 수만 있다면 특정 메서드를 호출하는 것이 가능하겠지만, 지금은 불가능하다. 그리고
만드는 사람 맘이니 어쩔 수 없다. 그래서 생각한 것이 이벤트라는 놈이다. 그냥 나한테 이런 일이 생겼어라고 클라이언트에게 알려주기만 하면
된다. 여기서 이 이벤트라는 놈이 바로 커넥션포인트라는 메커니즘으로 구현된다. 이제 알았겠지? (이 설명에서 약간의 문제점이 있다. 나중에
설명하겠지만, 클라이언트의 메서드를 호출한다는 말이 맞을 수도 있다. 이것은 조금 있다가 알아보자.)
여기서 또 많은 용어가 나오기 시작한다. 이걸 하기 위해서는 앞에서 잠시 언급한 싱크라는 놈도 알아야 하고 소스
인터페이스라는 놈도 알아야 한다. 항상 그랬듯이, 그럼 알아봐야지. 결국 이 두 놈 역시 떼어놓고는 말할 수 없다. 특이하게도, 이 두 놈의
메서드는 서로 일치한다.
(여기서 어느 정도 이제 감을 잡으신 분들도 있을 것이다. 그리고, 말하지 않고 넘어온 중요한 전제가 있다.
이것은 모두 자동화와 관련된 내용임을 알고 있어야 한다. 그래서 앞으로는 클라이언트 서버의 개념보다는 자동화 컨트롤러와 자동화 개체라는 말을
사용하겠다. 어느것이 서버이고 클라이언트인지는 말하지 않겠다.)
그럼, 커넥션포인트의 핵심인 소스 인터페이스와 싱크라는 놈이 하는 역할이 무엇인가? 간단하게 말하자면, 이벤트를
발생시키는 놈과 이벤트를 받아들이는 놈이라 표현해야 할 것 같다. 따라서 소스 인터페이스는 자동화 개체에서 구현될 것이고 싱크라는 놈은 자동화
컨트롤러(클라이언트)에서 구형되는 놈이다. 여기서 문제점이 발생한다. 싱크 역시 인터페이스로 구현된다. 그런데 자동화 클라이언트라는 놈은
일반적인 우리가 자주 만들던 MFC 프로그램들이다. 꼭 MFC로 만들진 않겠지만, VC 사용자가 대부분 MFC 사용자라는 것을 감안할 때,
난감한 일이 아닐 수 없다. 왜 난감하냐구? COM 개체를 만드는 것도 아닌데, 단지 사용하기 위해서 이벤트라는 놈을 받아들이기 위해서도 COM
개체의 구현방법을 알아야 하기 때문이다.(싱크라는 놈이 COM 개체라고 해도 무방하기 때문이다.) 결국 우리는 자동화 컨트롤러에서 COM 개체의
일종인 싱크 개체를 구현해야 한다는 말이다. 그러면, 자동화 개체에서 이벤트가 발생하게 되고 이 자동화 컨트롤러에서 구현한 싱크 개체의
메서드들을 호출하는 것이다. 이것으로 앞에서 언급한 메서드를 호출한다는 말이 맞을 수도 있다는 말이 무슨말인지 조금은 보완이 되었으리라
생각한다.
잠시 정리해 보도록 하자.
이 부분은 생각보다 아주 어려운 부분이다. 그러면서도 아주 중요하게 취급되는 부분이기도 하다. COM의 중급자라면
이 부분이 아주 유용하게 쓰이는 부분이기 때문에 잘들 사용하고 계실 것이다. 하지만, 이 강좌를 보고 있는 대다수의 초보자분들은 도대체가 무슨
말을 하는 건지도 잘 이해하지 못하시는 분들이 많을 것이다. 항상 그랬듯이 간단하게 이해할 수 있는 방법을 생각해 봐야 한다. 내가 코드를
가지고 또 설명을 하면 포기하는 사람을 늘일 뿐일 것 같다. 간단히 개념을 설명하는 것도 만만치 않은 작업이긴 하지만, 나름대로 간단하게 정리
해 보도록 하자.
여기서부터 좀 주의 깊게 들어주길 바란다.
자동화 개체에서 먼저 어떤 이벤트가 있는지, 이런 이벤트가 발생할 때 이런 메서드를 호출할 것이다라고 먼저
정의한다.(당연히 IDL에서 하겠지…) 이것이 소스 개체이다. 이 말은 소스 인터페이스에서 정의 한다는 말이다.
그 다음은 자동화 컨트롤러에서 싱크 개체를 구현하는데 이 싱크 개체의 구현은 자동화 개체에서 정의한 소스
인터페이스의 메서드들을 싱크 개체에 맞도록 구현한다. 왜냐하면 결국 자동화 개체에서 이 싱크 개체의 메서드를 호출하기 때문이다.
그러면 여기서 커넥션 포인트가 해주는 역할은 무엇일까? 그것은 바로 이 두 놈을 연결해주는 역할을 하는 것이 이
커넥션 포인트가 해주는 일이다. 이것은 또 어떻게 가능할까? 이것은 커넥션포인트의 메서드들을 살펴보면 답이 나온다.
이 두 놈들을 연결시키고 해제 시키는 역할을 하는 메서드가 IConnectionPoint::Advice 메서드와
IConnectionPoint::Unadvice 메서드이다. 사실 내부는 간단하다 앞장에서 했듯이 QueryInterface 메서드로 가능하게
된다. 즉, IOutGoing 인터페이스를 받아옴으로써 가능해지는 일인 것이다. 즉, 앞에서 말한 메서드의 메모리 번지를 얻어 온다고 해야
할까? 하여튼 그런 개념일 것이다. 그리고 싱크에서 중요한 것이 IOutGoing 인터페이스인데 이 IOutGoing에 대해서도 알아 봐야
한다.
싱크 개체의 구현은 IUnknown 과 IOutGoing 인터페이스로 구현되는데 IUnknwon은 언급할 필요가
없고 IOutGoing 에 대해서만 조금 알고 넘어가자. 이 인터페이스의 메서드는 GotMessage 라는 메서드가 있다. 즉, 메시지를 받았을
때 작동하는 놈인 것 같다는 생각이 들것이다. 여기서 이벤트가 발생할 때 자동화 컨트롤러에 알리면 자동화컨트롤러에서 구현한 싱크 개체의 이
메서드가 호출됨을 알 수 있다.
자, 지금까지 숨가쁘게 넘어왔다. 잠시 숨 좀 돌리자.
결국은 여러분 대부분이 ATL을 사용해서 프로그래밍을 하게 될 것이다. 말리진 않겠다. 사실 나 조차도 ATL이
편한 것은 사실이다. 하지만, 프로그래밍을 하다 보면 ATL에서 제공하는 메크로에 대해 황당한 생각이 자주 드는 것이 사실이다. 왜냐하면 어떻게
돌아가는지 내부를 전혀 모르기 때문이다. 조금은 개념을 생각하면서 프로그래밍을 하는 것이 나중을 생각해서는 더욱 시간을 아끼는 일이라고 말하고
싶다. 비주얼 베이직에서든, 자바에서든 싱크를 구현하는 방법이 다양하게 지원되고 있다. 비주얼 베이직의 경우 아주 간단하다. 자바역시
C프로그래밍보다 훨씬 간단하다. 가장 힘든 사람은 C 프로그래머들이다. 하지만, 어렵다고 생각할 이유는 없다. 간단한 샘플만 하나 가지고 있다면
그대로 적용만 시키면 되기 때문이다.
앞에서 IConnectionPointContainer, IEnumConnectionPoints,
IConnectionPoint, IenumConnections의 인터페이스들이 있다고 언급했었다. 사실 중요한 것은
IconnectionPointContainer, IconnectionPoint 라고 생각한다. 커넥션 포인트컨테이너의 경우는 커넥션 포인트를
얻기 위해 필요한 인터페이스라 생각하면 되겠다. 메서드로는 IconnectionPointContainer::FindConnectonPoing와
IconnectionPointContainer::EnumConnectionPoints를 지원한다. 메서드 이름에서 짐작할 수 있듯이 하나를
받아오느냐 여러 개를 받아오느냐의 차이일 뿐이다. 이 커넥션 포인트의 경우 멀티캐스트를 지원하기 때문에, 다시 말하면 하나의 자동화 개체가 여러
개의 싱크들에게 이벤트를 전달 할 수도 있기 때문에 여러 개의 커넥션포인트를 가질 때도 있다. 여기서 약간의 문제점은 멀티캐스트라고 해서 네트웍
프로그래밍의 멀티캐스트를 생각하면 곤란하겠다. 이유는 한번의 이벤트를 발생시키는 것이 아니라 각각의 싱크들에게 이벤트를 여러 번 전달해야 하는
이유 때문이다.(차라리 브로드캐스트라는 말이 어울릴 듯 하다.)
참고로, 이 이벤트에 대한 자세한 내용은 앞에서 잠시 언급했듯이 마이크로소프트의 기술문서를 참조하기 바란다.
찾아보라는 얘기는 아니다. 여러분들을 위해 이 글 마지막 부분에 추가 시켜 놓았다. 처음에는 내가 써보려고 했지만, 내용이 중복되는 것이 너무
많은 것이 아닌가? 거기다 신뢰할 수 있는 자료라는 점에서 나의 설명과는 구분된다. 참고이긴 하지만 꼭 읽어보길 바란다.
앞에서 소스 인터페이스에서 정의한 메서드와 싱크 인터페이스의 메서드들이 일치한다고 언급했었다. 따라서 소스
인터페이스의 정보를 모르는 상태에서는 싱크 인터페이스를 만들 수 없다는 얘기도 된다. 이 소스 인터페이스에 대한 정보를 얻기 위해 사용되는
인터페이스가 IProvideClassInfo 인터페이스인데 여기서는 언급하지 않겠다. 단지 이런 것이 있다는 것을 알면 된다. 사실, 대부분은
IDL에서 이 정보를 알 수 있기 때문에 자주 사용하지는 않을 것 같다.
전반적인 과정을 정리해보도록 하자.
먼저 자동화 컨트롤러는 자동화 서버의 IConnectionPointContainer를 요청한다. 그러면 자동화
서버에서 이 인터페이스를 넘겨 줄 것이다. 그리고 우리는 이 인터페이스의 메서드인 FindConnectionPoint를 호출해서
IConnectionPoint 인터페이스를 얻어올 것이다. 그리고 다시 IConnectionPoint 인터페이스의 Advice 메서드를
호출함으로 인해 반대로 자동화 서버에서 자동화 컨트롤러에서 구현된 싱크 인터페이스를 받아오게 된다. 그 다음은 자동화 서버에서 이벤트가 발생할
때 싱크 인터페이스의 메서드들을 자유롭게 호출할 수 있게 되는 것이다.
지금까지 대충 커넥션 포인트에 대해 살펴 보았다. 자세한 코드 내용은 앞에서 말했듯이 이글 마지막에 첨부된 내용을
참조하면 되겠다.
이제는 타입 정보에 대해서 간단히 알아보도록 하자. 여기에 대해서 그렇게 자세하게 알 필요가 있을까 하는 것이 내
생각이라서 대충 알고 넘어가는 수준에서 끝내도록 하겠다.
타입 라이브러리라는 말은 앞에서 몇 번 들어 봤을 것이다. 쉽게 생각하자면 IDL 파일의 이진 버전이라고 생각하는
것이 가장 쉽다. COM 개체의 각종 정보가 들어 있는데 주로 인터페이스의 이진 설명들이라 할 수 있다. 여기에는 메서드와 인자, 리턴값들에
대한 정의가 있을 것이다. 그래야만 많은 다른 언어에서 이 정보에 접근하는 것이 가능해지기 때문이 아닐까라고 생각 한다. 이 타입 라이브러리를
만들기 위해 ICreateTypeLib와 ICreateTypeInfo 인터페이스를 지원한다. 앞에서 타입 라이브러리를 만들기 위해 IDL 파일을
MIDL로 컴파일 하는 것을 봤었다. 이 때 MIDL이 사용하는 두 주요 인터페이스가 앞의 두개 이다. 다른말로 하자면 MIDL 이 없이도
IDL 파일에서 우리는 MIDL이 만드는 타입라이브러리와 똑 같은 타입라이브러리를 생성할 수가 있다. 여기서 의문점이 생길 수 있다.
MIDL에서 다 해주는데 우리가 이 타입정보를 굳이 알 필요가 있을까? 사실, 외부 툴(MIDL)을 사용한다는 자체가 맘에 안들 수가 있다.
COM 자체에서 모든 것이 해결가능한데 왜 MIDL을 써야 하냐고 의문인 사람들, 특히나 고집스러운 프로그래머들은 대부분 그렇게 생각한다. 이때
유용하게 사용할 수 있는 COM 인터페이스가 앞의 두 인터페이스이다.
마지막으로 포함(Contanment) 과 통합(Aggregation)에 대해 알아보자.
COM에서는 개체 상속을 지원하지 않는다. 일반적인 C++ 프로그래밍에 익숙한 프로그래머들에게는 조금은 생소할
수가 있다. 결국 COM에서는 구현상속이 아닌 인터페이스 상속으로 문제를 해결하고 있다. 하지만, 잘 생각해 보자. 인터페이스가 순수 가장함수
테이블임을 가만할 때 인터페이스 상속이라 해봐야 우리는 구현을 새로 다 해야 한다. 그렇다면 구현코드를 가져와서 사용할 방법은 없다는 말인가?
재사용의 이점을 버린 것인가? 그렇지가 않다. COM에서도 구현상속과 비슷한 방법이 있다. 그것이 바로 포함과 통합이라는 방법이다.
차이점을 굳이 들자면 코드 차원의 재사용이 아닌 이진레벨의 재상용성이라는 더 큰 이점이 있다는 것이 가장
중요하다. 조금은 생소할 수 있지만, 살펴보면 그다지 어렵지는 않을 것이다.
간단하게 알아보도록 하자.
포함(Contanment) 과 통합(Aggregation)은 조금은 헷갈리는 부분이다. 둘 다 하나의 개체를
구현하면서 이미 구현된 개체를 내부에 가지면서 그 개체의 기능을 활용하는 방법이다. 포함의 경우는 하나의 개체에서 다른 개체의 인터페이스를
완전히 다시 구현하는 것이고, 포함의 경우는 그 인터페이스는 그대로 사용하고 새로운 인터페이스를 추가로 구현하는 방법이라 이해하면 되겠다.
좀더 자세히 알아보면 포함의 경우 말 그대로 C++에서 class 안에 class을 선언한 경우와 비슷하다.
차이점은 이 과정이 인터페이스 레벨에서 이루어진다는 것이다. 예로 OutsideCOM 개체와 InsideCOM 개체가 있다고 가정하자. 그리고
InsideCOM의 인터페이스로 IAdd 라는 인터페이스가 있다고 가정한다면 OutsideCOM에도 IAdd 인터페이스가 있어서 이
OutsideCOM 개체의 IAdd 인터페이스가 InsideCOM 개체의 IAdd 인터페이스의 역할을 대신하도록 할 수 있다는 것이다.
내부적으로 InsideCOM 개체의 IAdd 인터페이스의 메서드를 그대로 호출해서 똑 같은 동작을 하게 할 수도 있다.
통합의 경우는 말그대로 InsideCOM 개체의 IAdd 인터페이스를 OutsideCOM 개체의 인터페이스인양
바로 밖으로 노출시키는 방법이다. 대신 추가적인 기능보다는 내부 개체의 기능을 그대로 사용하는데 중점을 두는 방식이라 하겠다.
지금까지 COM의 대략적인 개념들을 살펴보았다.
내용상으로 많이 부족한 것도 사실이다. 하지만, 이 부분에 대해서는 책을 참조하길 권하고 싶다. 내가 할 수 있는
일은 앞장에서 잠시 언급했듯이 개념을 잡는데 조금이라도 도움을 줄 수 있다면 그것으로 만족한다. 처음의 각오는 상세한 부분까지 다 하고
싶었지만, 너무 막대한 분량인 것 같았다. 그래서 이 정도가 나의 한계이다.
이제 끝이다. COM+ 에서 다시 보게 될지는 솔직히 의문이다.
잡담으로 좀 넘어가보자.
COM 자체를 중점적으로 공부하고자 하는 사람이 있다면 말리진 않겠다. 중요한 것이 사실이고, 고급 프로그래밍
기법에 속하는 분야라서 취직도 그만큼 잘 되리라 생각한다. 한 예로 ActiveX, COM 가능자라는 구인란을 봐도 그런 것 같다. 하지만,
이것 또한 얼마나 갈지 모르겠다. 곧 .NET 시대로 갈 것이기 때문일 것이다. 기본 개념만 확실히 잡혀있다면 별 문제가 되지 않을 것이라
생각되지만 결국은 새로운 언어를 배워야 하는 부담 역시 남아있다.
COM이 .net 컴포넌트로 대체될 것이고 DCOM역시 .net 리모팅으로 기타 COM+등등도 .net에서 새로운
기능으로 전환되고 있는 시점이다. 이전에 COM 라리브러리를 사용해서 복잡하게 COM 개체를 생성하는 방법 역시 .net 컴포넌트에서는 일반적인
프로그래밍 처럼 new 하나로 바뀐다. 메모리 해제는 가비지 컬렉션이라는 놈이 알아서 해주게 되고 점점 프로그래밍이 쉬워진다는 느낌이지만,
공부해야 할 내용은 더욱 많아지고 있다. 당장은 아닐 것이다. 장담은 못하겠지만 개인적인 생각으로는 적어도 5년 정도는 COM 프로그래머가
먹고 사는 데는 지장 없을 것이란 생각이지만 설 자리가 5년 동안 천천히 줄어들 것임은 자명한 일이다. 결국 내가 하고 싶은 말은 COM을 주로
팔 것인지, 아니면 개념상 공부만 할 것인지를 여기서 분명히 판단할 필요가 있다고 생각한다. (어디까지나, 이 글을 읽고 있는 초보들을 위한
말이다.) 아직 공부할 시기인 학생들이라면 COM의 개념을 중점으로 공부하고 간단한 샘플하나만의 작성으로 충분하리라 생각한다. 그리고 앞으로의
대세인 .net 프로그래밍 쪽으로 권하고 싶다. 그렇다고 COM에서 배운 지식 중에서 버릴 것은 거의 없을 거라 생각한다. 모든 개념은 거의
대부분 그대로 사용되고 있다. 그리고 당장 취업이 눈앞인 사람이 .net을 공부한다는 것은 무모한 일이라 생각한다. 이러한 분들은 오히려
ActiveX 쪽이 훨씬 쉽지 않을까 생각한다. MFC 할 줄 아는데요 하는 것보단 ActiveX 컨트롤 몇 개 만들어 봤습니다가 훨씬 잘 먹혀
들어갈 것이기 때문이다. 그리고 프로그래밍의 열정이 그대로 남아 있다면 집에서 틈틈히 .net을 공부하길 추천한다.
내가 이런 말 할 자격이 없다는 것은 나 자신이 더 잘 알고 있다. 나도 잘 모른다. 할 줄 아는 것도 없다.
하지만, 주위 사람들로부터 듣는 말은 나이만큼이나 나보다 어린 사람들 보다는 더 나을 수도 있다고 생각하기 때문에 이런 말을 하는 것이라
이해해주면 고맙겠다.
더 이상 언급하는 것은 잔소리로 밖에 들리질 않을 것 같다. 그만 하겠다.
네^^, 다들 애 쓰셨습니다.
이제는 이런 말도 끝입니다.^^
지금까지 저의 글을 읽어주신 분들에게 감사의 말씀을 전합니다(너무 부실하게 시작한 것이 끝도 부실하게
만드는군요.)
다들 즐거운 하루 보내시구요. 기회가 되면 COM+에서 만나도록 하겠습니다.
그리고 부가적으로 한 말씀 더 올리자면, 앞으로는 XML이 가장 중요하게 취급되는 시대가 올겁니다. XML을
한번도 보지 않으신 분들(VC++ 사용자라면 더욱 많을 듯 싶은데)은 관심이 있던 없던 무조건 해야 합니다. 이것을 HTML 차원으로 생각하신
분들이 있다면 그것 또한 엄청난 오류입니다. 앞으로의 모든 데이터 처리와 메세징 그리고 웹서비스와 거의 모든 분야에서 XML을 기본으로 발전해
갈겁니다. 다시 말해서 XML을 빼고 프로그래밍을 얘기한다는 자체가 시대의 흐름을 따라가지 못하고 도태되어 간다고 말할 수 있을 것 같습니다.
무슨 말도 안되는 소리냐고 반문하시는 분이 없지 않을 겁니다. 하지만, 한번쯤은 XML관련책이나 .NET 프로그래밍에서 XML 부분을 읽어
보시길 권장합니다. 꼭~입니다. 보다 넓게 보는 시야를 키웁시다.
e-mail : icoddy@hotmail.com
msn id : icoddy@hotmail.com
– 박성규 ?
<마이크로소프트의 기술문서 첨부>
Dr. GUI와 COM 이벤트, 1부
1999년 9월 13일
시작하기에 앞서
현재, COM은 스레드를 완벽하게 처리할 수 있는 기능을 가지고 있고, Dr. GUI 역시 마찬가지여서(그러나,
상당히 많은 추가 작업이 필요함), 이벤트에 대한 기본 적인 개념이 혼동될 정도입니다.
Dr. GUI는 Windows 타이머 SetTimer API를 사용하여 작동되는 단일-스레드 타이머 개체를
만들었습니다. 그러나, 이 타이머 개체는 약간의 문제를 가지고 있으며, 타이머가 종료되기 전에 클라이언트가 타이머를 중단시키려고 하면, 이
문제는 더욱 심각해집니다. 또한, 이 접근 방법의 경우, 메시지와 타이머, 그리고 큐가 아주 복잡하게 얽혀 이벤트의 개념이 혼동되기
시작하였습니다.
그래서 우리는 이벤트가 무엇을 할 수 있는지를 보여주는 더 간단한 개체를 만들어 보기로 하였습니다. 게다가, 이
개체는 코드의 중복을 줄이기 위해 강력한 프로그래밍 기법까지 제공합니다. 물론, Dr. GUI는 다음 칼럼에서 멀티-스레드 문제를 가지고 다시
돌아올 계획입니다.
Windows 2000에서 쉽게 사용할 수 있는 우수한 응용 프로그램
Windows 2000의 날이 얼마 남지 않았습니다. 그 때가 될 때까지 그냥 날짜만 세고 있는 것보다는,
Windows 2000의 새로운 기능을 배워 두는 것도 좋지 않을까요?
Windows 2000은 여러분의 응용 프로그램과 그 응용 프로그램을 실행하는 시스템을 보다 신뢰성 있고,
안정되고, 강력하게 만들 수 있는 풍부한 기능으로 가득 차 있습니다. 또한, 여러분에게 셋업, 트랜잭션, 큐 같은 복잡한 일을 보다 쉽게 처리할
수 있도록 표준 방법을 제공하는 새로운 시스템 서비스가 아주 많습니다.
Windows CE H/PC Pro용 Terminal Server Client
직접 Windows CE H/PC Pro 장치를 구입하신 분 계십니까? 만약 그렇다면, 그리고 NT
Terminal Server를 보유하고 있으시다면, Terminal Server client 를 다운로드하는 것이 좋을 것입니다.
Terminal Server 클라이언트를 설치하면, 거의 모든 NT 응용 프로그램을 H/PC Pro에서 사용할 수 있습니다
H/PC Pro나 다른 NT Terminal Server 클라이언트 하드웨어를 갖고 있지 않더라도, Windows
95/98/NT에서 Terminal Server를 클라이언트로 사용할 수 있습니다. 그 이유는 Terminal Server는 여러분의 하드웨어,
OS, 또는 응용 프로그램 환경과 다른 환경에서 사용이 가능하기 때문입니다. 예컨대, Microsoft사의 Developer Support 팀이
구 버전의 Visual Studio® 에 NT Terminal Server를 설치했습니다. 고객이 구 버전의 개발 환경에 관한 문의를 해오면,
지원 팀의 공학자는 구 버전을 기계에 설치하는 번거로움 없이, 해당 NT Terminal Server 박스를 통해 고객이 경험하고 있는 문제를
재연할 수 있습니다.
구 버전의 Visual Studio로 구축한 프로젝트에 대해서도 동일한 방식으로 적용할 수 있습니다. 단순히
프로젝트와 해당 버전의 Visual Studio(기타 다른 개발 환경)에 적합하게 NT Terminal Server를 설치하면 되고, 해당
라이센스 보유자는 자신의 자리에서 그 프로젝트를 계속 관리할 수 있습니다.
Windows 2000 DDK
Windows 2000 Device Driver를 사용해보십시오. 또한, Windows 2000 DDK
RC1(Release Candidate 1) 버전을 무료로 다운로드하실 수 있습니다. ( http://www.microsoft.com/ddk/
).
초고속 Microsoft 웹 응용 프로그램 서버
여러분은 Microsoft의 웹 서버가 확장성이 떨어진다고 생각할 수도 있습니다. 그러나, PC Week의 확장성
벤치 마크 검사 결과는 그 예상과 다르게 나왔습니다. 이 벤치 마크 검사는 Microsoft 웹 플랫폼이 “지구상의 어떤 비즈니스도 수용할 수
있을 만큼 빠르다”는 결론을 내렸습니다.
현재와 앞으로의 계획
이번 칼럼에서는, COM 이벤트에 관해 다루도록 하겠습니다. 다행히도, 이벤트에 관한 내용은 체계적으로 문서화되어
있으며, 앞으로도 계속해서 설명해 드리겠지만, 약간 생소한 내용의 이벤트도 종종 있으므로, 이벤트에 익숙해지는 것이 좋을 것입니다. 이번
칼럼에서는 이벤트를 파악하는 방법과 이벤트를 받는 원리에 관해 설명하겠습니다.
다음 칼럼에서는, ATL로 이벤트를 구현하는 방법과 이벤트를 받는 Visual Basic 클라이언트를 작성하는
방법을 알아볼 계획입니다. 또한, 이벤트를 사용하여 얼마나 설계를 간단하게 할 수 있는지에 관해서도 설명할 것입니다.
이벤트
이벤트란 무엇인가?
드디어, 이벤트를 설명할 때가 되었습니다. 그렇다면, 이벤트란 무엇일까요?
돌이켜보면, 우리가 지금까지 COM에서 보아온 통신은 매우 단편적인 것이었습니다. 즉, 클라이언트가 개체에 대하여
메서드를 호출하는 것이 전부였죠.
물론, 개체가 반환 값을 재전달하기는 하지만, 그것은 어디까지나 클라이언트가 요청할 때만 발생합니다. 즉,
“필요할 때에만 이야기를 합니다.”
이 방식(“필요할 때에만 이야기한다”)은 단순히 명령에 응답하기만 하는 “dumber” 개체는 물론 다른 여러
개체에도 적용됩니다.
그림 1. IFoo 인터페이스를 사용하는 클라이언트와 그 클라이언트의 개체 간의 단 방향 통신. 이름이 지정되지
않은 인터페이스는 IUnknown입니다.
잠깐만! 클라이언트! 나 할 말이 있어…
개체가 클라이언트에게 뭔가 특별한 일이 발생했음을 알려야 할 때 어떻게 해야 할까요? 예를 들어, 버튼 같은 시각
컨트롤이 클라이언트에게 자신이 언제 클릭되었는지를 알려주어야 할 경우가 있습니다. 또는, 비즈니스 규칙을 구현하는 개체는 클라이언트에게 규칙이
위반되었음을 알려야 할 것입니다. 또는, 개체가 백그라운드에서 어떤 작업을 하고 있고, 클라이언트에게 그 작업이 끝났음을 알려야 하는 경우도
있습니다.(백그라운드의 경우는 이 기사에서 다루지 않습니다.)
폴링
물론, HasButtonBeenClicked, HasRuleBeenViolated,
또는ArentYouDoneYET와 같이 개체 내에서 메서드를 구현할 수도 있습니다. 이 경우, 개체는 자신의 상태를 규정하는 플레그를 유지하고
그것을 메서드 호출에 대한 응답으로 반환합니다. 반면에, 클라이언트는 계속해서 메서드를 폴링해야 합니다. 이 방법은 비효율적이고 프로그래밍하기도
어려우므로 바람직하다고 볼 수는 없습니다.
응답하는 컴포넌트
대신, 개체가 클라이언트에 있는 메서드를 호출할 수 있다면 어떨까요? 이 경우, 개체는 버튼이 클릭되었거나,
규칙이 위반되었거나, 작업이 완료되었을 때처럼, 조건이 허락되면 즉시 메서드를 호출할 수 있을 것입니다. 또한, 클라이언트는 이벤트가 발생했다는
통보를 신속하게 받을 수도 있습니다(지나치게 동시적이지도 않고 비동시적이지도 않음). 이것을 그림으로 나타내면 다음과 같습니다.
그림 2. 클라이언트와 개체 간의 양방향 통신. 즉, IFoo인터페이스를 사용하는 클라이언트로부터 개체로의 통신과
IFooEvents 인터페이스를 사용하는 개체로부터 클라이언트로의 통신
이제 client는 object가 인터페이스에 대해 호출을 할 때 사용할 인터페이스를 구현해야 합니다. 하지만,
인터페이스를 지정하는 것은 여전히 object입니다. 개체는 이 인터페이스에 대한 호출의 소스이므로, 이 인터페이스를 source 인터페이스라고
합니다. 개체를 이벤트의 소스가 되는 것으로 생각하면 기억하기 쉬울 것입니다.
클라이언트는 이 인터페이스 호출에 대한 sink입니다. 지금부터 source와 sink라는 단어는
개체(source)와 클라이언트(sink)를 가리키는 말로 사용하겠습니다. Dr. GUI가 “소스 개체”라고 지칭한 것은 “연결 가능한 개체”를
의미하기도 합니다. 여기서는 편의상 두 단어를 모두 같은 것으로 간주하도록 하겠습니다.
COM의 이벤트 기능
COM 이벤트는 기본적으로 단순합니다. COM 이벤트는 간단히 말해서 개체(source)가 클라이언트(sink)에
대해 메서드를 호출할 수 있는 수단입니다. COM은 이러한 작업을 수행하기 때문에, 이벤트 메서드는 인터페이스 포인터를 통해 호출되며, 이는
다음과 같은 세 가지 의미를 갖습니다.
1. 동일한 이벤트 인터페이스 내에 서로 관련된 여러 개의 이벤트가 들어 있을
수도 있습니다. 예를 들어, 여러 종류의 클릭(한번 클릭, 더블 클릭 등), 여러 가지 위반 사항, 또는 여러 완성 단계(프로세스 내/완료).
2. 클라이언트는 인터페이스를 구현하는 간단한 미니-COM 개체를 구현해야 할
것입니다. (일반적으로 미니-개체는 IUnknown과 이벤트 인터페이스만 구현합니다.) 클라이언트의 개체는 소스 개체로부터 호출을 받기 때문에,
sink 개체라고도 합니다.
3. 클라이언트는 어떻게 해서든 sink 개체의 인터페이스 포인터를 source
개체로 전달해야 합니다. (다소 복잡한 부분임)
이러한 기본적인 내용 외에도, COM의 이벤트 구성은 다음과 같은 여러 가지 특별한 기능을 지원합니다.
· 하나의 개체(또는 source)는 두개 이상의 소스(이벤트) 인터페이스를
지원할 수 있습니다.(이들 인터페이스는 각각 하나 이상의 메서드를 가질 수 있으므로 융통성이 매우 높다고 할 수 있습니다.)
· 여러 sink 개체가 동일한 인터페이스로부터 이벤트를 받을 수
있습니다.(이것을 멀티 캐스팅이라고 함). 이를 위해서는, source 개체가 이벤트를 받고 싶어하는 모든 sink 개체를 기억하고 있어야
합니다.
· 클라이언트 내의 sink 개체는 하나 이상의 개체로부터 이벤트를 받을 수
있습니다.
이벤트를 구현하려면 무엇이 필요한가?
소스 인터페이스
지금까지 이벤트 인터페이스라고 불러온 인터페이스의 또 다른 명칭은 소스 인터페이스입니다. 이 인터페이스가 소스
인터페이스라고 불리는 이유는 인터페이스에 대한 호출의 소스가 되는 이벤트-파이어(event-firing) 개체 안에 선언되어 있기 때문입니다.
이 샘플에 대한 소스 인터페이스는 아주 간단합니다. 다음은 인터페이스 정의 언어(IDL)입니다.
[
uuid(F2F660CF-3ED7-11D3-9C8C-000039714C10),
helpstring(“_IAAAFireLimitEvents Interface”)
]
dispinterface _IAAAFireLimitEvents
{
properties:
methods:
[id(1), helpstring(“method Changed”)]
HRESULT Changed(IDispatch *Obj,
CURRENCY OldValue);
[id(2), helpstring(“method SignChanged”)]
HRESULT
SignChanged(IDispatch *Obj,
CURRENCY OldValue);
};
Changed 이벤트는 개체의 값이 변경될 때마다 파이어되고, SignChanged 이벤트는 개체의 값 부호가
변할 때마다 파이어될 것입니다.
dispinterface?
여러분이 가장 먼저 발견하게 되는 것은 이벤트에 대한 인터페이스의 타입이 dreaded dispinterfece,
즉 디스패치 인터페이스라는 것입니다. 그 이유가 무엇인지 궁금하시겠지요?
하지만, 디스패치 인터페이스는 더 느리고 프로그래밍하기가 지루하다는 단점에도 불구하고, 한가지 중요한 장점을
가지고 있습니다. 그것은 바로 임의의 디스패치 인터페이스로부터의 호출을 정확하게 해석할 수 있는 코드를 작성하기가 비교적 쉽다는 것입니다.
여러분은 IDispatch, 특히 Invoke를 구현하기만 하면 됩니다. 매개 변수는 variant의 배열로 전달되는데, 스택을 통해 실제로
전달된 매개 변수를 찾는 것보다 파싱(구문해석) 작업이 쉽습니다. 클라이언트 개체는 타입 라이브러리를 사용하여 어떤 메서드가 존재하는지(필요에
따라서는, 그 메서드의 매개 변수가 무엇인지)를 알아내야 합니다.
더 중요한 두 번째 이유는 디스패치 인터페이스를 사용할 경우, 호출을 받는 개체가 인터페이스 내의 모든 메서드
구현을 제공할 필요가 없다는 것입니다. 여러분이 정규 사용자 정의 인터페이스를 구현할 때 그 인터페이스에 있는 모든 메서드를 구현해야만 최소한
E_NOTIMPL을 반환할 수 있다는 점을 상기해 보십시오.
디스패치 인터페이스의 경우, IDispatch의 메서드는 모두 구현해야 하지만, Invoke를 구현할 때는
디스패치 인터페이스 내의 모든 메서드를 구현할 필요가 없습니다. Invoke는 자신이 지원하지 않는 메서드(다시 말해서, 처리하고 싶지 않은
이벤트)에 대해 오류(DISP_E_MEMBERNOTFOUND)를 반환합니다.
Visual Basic이나 응용 프로그램용 Visual Basic(VBA)과 같은 언어는 사용자 정의 인터페이스에
대한 이벤트 호출보다 디스패치 인터페이스에 대한 이벤트 호출을 받기가 더 수월하므로, 디스패치 인터페이스에 대한 이벤트 호출만 지원합니다.
C++로 직접 sink를 작성하고 다른 클라이언트에 관여하지 않는 경우에는, 사용자 정의 인터페이스를 향상된 성능으로 사용할 수 있습니다.
그러나 대부분의 경우, 이벤트는 비교적 적기 때문에 성능은 큰 문제가 되지 않습니다. 소스 인터페이스가 듀얼 인터페이스가 되는 것은 아무 의미가
없습니다. 듀얼 인터페이스가 갖는 이중성은 그 인터페이스의 메서드가 직접 호출 가능하고, IDispatch::Invoke를 통해 호출될 수도
있다는 데 있기 때문입니다. 다시 말해서, 듀얼 인터페이스는 호출을 받는 데 관여하는 것이지 호출 생성에는 관여하지 않습니다. Visual
Basic과 호환 가능한 고성능의 이벤트 인터페이스를 만들려면, 서로 다른 인터페이스 ID(IID)를 갖는 두 개의 동등한 소스 인터페이스를
구현하고(하나는 사용자 정의 인터페이스, 다른 하나는 디스패치 인터페이스), Visual Basic에 부합하도록 디스패치 인터페이스를 기본
설정으로 만들어야 합니다.
여러분이 발견하게 될 또 한 가지는 인터페이스 이름이 밑줄(_)로 시작된다는 것입니다. 이것은 Visual
Basic 규칙인데, 인터페이스 이름이 밑줄로 시작되면, Visual Basic 환경은 그 인터페이스를 디스플레이하지 않습니다.
이러한 사항을 제외하면, IDL 코드에는 별로 새로운 것은 없습니다.
소스를 사용하라
그런데 한 가지 빠진 것이 있습니다. 특정 개체 안의 인터페이스가 소스 인터페이스인지 어떻게 알 수 있을까요?
인터페이스는 기본 설정에 의해 들어오는 인터페이스기 때문에 지금까지는 이 부분에 대해 거론한 적이 없었습니다.
인터페이스 자체에 대한 IDL에는 그 인터페이스가 source 인터페이스인지 sink 인터페이스인지를 알려주는
정보가 없습니다. 사실, 인터페이스는 그것이 어떻게 사용되느냐에 따라 source 인터페이스도 될 수 있고 sink 인터페이스도 될 수
있습니다. 인터페이스가 source 인터페이스인지 여부는 특정 개체에 의해 결정되므로, 인터페이스는 IDL의 coclass 섹션 안에서
지정합니다.
coclass AAAFireLimit
{
[default] interface IAAAFireLimit;
[default, source] dispinterface _IAAAFireLimitEvents;
};
주의할 것은 _IAAAFireLimitEvents 인터페이스를 나가는(source) 인터페이스와 기본 설정 소스
인터페이스 두 가지로 모두 지정했다는 점입니다. Visual Basic과 스크립팅 언어는 하나의 COM 개체 당 단 한 개의 소스 인터페이스를
처리하므로, default 속성을 사용하는 것이 가장 바람직합니다(여러 개의 소스 인터페이스가 있는 경우에는 필수적임).
개체에 대한 sink의 인터페이스 포인터 얻기
이와 같이 인터페이스를 규정한 후에는, 그 인터페이스에 대한 인터페이스 포인터를 얻어야 합니다. (여기서는
클라이언트가 그것을 제대로 구현한다고 가정하겠습니다.) 그러면 클라이언트는 소스 인터페이스를 구현하는 sink 개체를 만들고, 그 개체의
인터페이스 포인터를 source 개체로 넘겨줍니다.
물론, 이 작업은 간단하지는 않습니다.
COM은 안정적이긴 하지만 복잡한 솔루션이다: 커넥션 포인트와 컨테이너
COM 설계자는 자주 사용되지 않는 기능을 추가하기도 합니다. 예를 들어, 몇몇 IDispatch 메서드는
인터페이스 ID(IID) 매개 변수를 가지고 있는데, 이것은 여러 개의 디스패치 인터페이스를 지원하기 위한 것입니다. 그러나 그 요소는
구현되지도 않았고, COM 표준의 일부로 생성되지도 않았습니다. 따라서, 디스패치 인터페이스는 COM 개체 당 하나의 인터페이스가 있는 것으로
간주합니다.
이벤트의 경우, COM 설계자가 원한 것은 상기 설명한 요소를 지원하고(개체마다 여러 개의 나가는(또는 발신)
인터페이스 등) 여러분이 개체에 대한 메인 코드를 변경하지 않고도 이벤트 인터페이스를 변경할 수 있게 하는 것이었습니다.
이 문제에 대한 COM의 솔루션은 source 개체가 커넥션 포인트(connection point)라고 하는
미니- COM 개체를 구현하게 하는 것입니다. 소스 개체는 나가는(또는 발신) 인터페이스 각각에 대해 정확히 한 개의 커넥션 포인트 개체를
갖으며, 각 커넥션 포인트는 정확히 하나의 나가는(또는 발신) 인터페이스에 서비스를 제공합니다.
커넥션 포인트는 독립적인 COM 개체이지만, CoCreateInstance에 의해 생성되는 것이 아니라 개체에
의해 생성됩니다.(이에 관해서는 나중에 간단히 설명할 것입니다.) 커넥션 포인트는 일반적으로 단 두 개의 인터페이스, IUnknown과
IConnectionPoint를 구현합니다.
커넥션 포인트와 IConnectionPoint
IConnectionPoint 인터페이스의 메서드를 살펴보면 커넥션 포인트가 어떤 일을 하는지 가장 쉽게 이해할
수 있습니다.
가장 중요한 것은 Advise와 Unadvise입니다. 클라이언트(sink)는 커넥션 포인트 개체에 대해
IConnectionPoint::Advise를 호출하여 연결을 설치합니다. (클라이언트가 커넥션 포인트 개체에 대한 인터페이스를 어떻게 얻는지는
당분간 생각하지 않기로 하겠습니다.) Advise에 사용되는 매개 변수는 이벤트 인터페이스를 구현하는 sink 개체에 대한 IUnknown
포인터입니다. Advise의 구현 부분은 이 인터페이스 포인터를 커넥션 포인트 개체 안에 저장합니다. 그러면, source 개체는 커넥션 포인트
개체로부터 인터페이스 포인터를 얻고 그 포인터에 대해 메서드를 호출하여 이벤트를 파이어할 수 있습니다.
Advise는 이 연결을 식별하는 고유 정수 값인 쿠키를 반환합니다. 연결을 끊으려면, 클라이언트가 동일한 쿠키를
사용하여 Unadvise를 호출해야 합니다. 그러면 커넥션 포인트 개체가 관련된 인터페이스 포인터를 삭제합니다. 클라이언트는 더 이상 이벤트를
받지 말아야 할 경우에(예를 들어, 클라이언트가 종료될 때) 반드시 이 작업을 수행해야 합니다.
멀티케스팅이 지원되므로, 동일한 커넥션 포인트를 사용하여 두개 이상의 연결을 만들 수도 있습니다. 이벤트가
발생하면, source 개체는 각 연결에 대해 적절한 메서드를 호출해야 합니다. 20개의 개체가 Advise를 호출하는 경우, Changed
이벤트가 발생하면 20 개의 Changed 호출이 생성됩니다(Advise로 전달된 각 인터페이스 포인터에 대해 하나씩). 앞에서도 말했듯이,
커넥션 포인트는 모든 인터페이스 포인터를 저장할 책임이 있습니다.
IConnectionPoint 안에 있는 그 다음 메서드는 source 개체의 편의를 위한 것입니다.
EnumConnections는 IEnumConnections를 구현하는 개체(아직은 미니 개체)에 대한 포인터를 반환합니다. 이 경우,
source 개체는 호출 생성에 필요한 인터페이스 포인터를 얻을 수 있습니다.
마지막 두 개의 메서드는 클라이언트나 소스에 의해 사용되며, 그 이름이 의미하는 대로
GetConnectionInterface는 이 커넥션 포인트가 서비스를 제공하는 인터페이스의 IID를 반환하고,
GetConnectionPointContainer는 source 개체의 IConnectionPointContainer 포인터를 반환합니다.(이에
관해서는 곧 설명할 것입니다.)
복습: 클라이언트의 경우, 커넥션 포인터의 함수는 연결을 만들고(Advise) 연결을 파이어하는(Unadvise)
방법을 제공합니다. 이것을 이해하면, 나머지도 부분도 쉽게 이해할 수 있습니다.
커넥션 포인트 컨테이너와 IConnectionPointContainer
이제 우리는 커넥션 포인트를 사용하여 연결을 설치하는 방법을 알게 되었습니다. 미니-개체의 IUnknown
포인터를 커넥션 포인터의 Advise 메서드로 넘겨줍니다.
그런데, 초기에 커넥션 포인트에 대한 포인터를 어떻게 얻으면 될까요?
앞에서 설명했듯이 COM 개체는 하나 이상의 커넥션 포인트를 지원할 수 있습니다. 이를 위해서는, source
개체가 IConnectionPointContainer 인터페이스를 구현해야 합니다. 그러면 클라이언트 개체는 이 인터페이스를 통해 커넥션
포인트를 얻을 수 있습니다.
IConnectionPointContainer 는 단 두 개의 메서드를 갖는 간단한 인터페이스입니다. 먼저
FindConnectionPoint 메서드는 클라이언트 개체에 의해 전달된 IID에 지정된 커넥션 포인트에 대한 포인터를 반환하고,
EnumConnectionPoints는 source 개체에 의해 지원되는 모든 커넥션 포인트를 홀더가 탐색할 수 있게 해주는
IEnumConnectionPoints 열거자를 반환합니다.
결합 방법
Source 개체는 IConnectionPointContainer를 구현하고,
IConnectionPointContainer의 메서드를 통해 검색하고 연결할 수 있는 커넥션 포인트들의 컬렉션을 관리합니다. 각 커넥션
포인트는 활성 연결의 목록을 관리하고, 활성 연결은 커넥션 포인트의 Advise 메서드 호출에 의해 설치되어 커넥션 포인트의 Unadvise
메서드 호출에 의해 파이어됩니다. 이들 연결은 열거가 가능합니다.(커넥션 포인트 개체 이외에도,
IConnectionPointContainer::EnumConnectionPoint에 대하여 IenumConnectionPoints를 구현하는
개체와 IConnectionPoint::EnumConnections에 대하여 IEnumConnections를 구현하는 개체를 만들어야 합니다.)
따라서, 총 세 개의 서로 다른 미니-COM 개체가 생성됩니다.
다소 복잡하기는 하지만 여러 개의 이벤트 인터페이스와 멀티캐스팅을 지원하려면 이 방법이 반드시 필요합니다.
모든 연결 작업이 완료되었을 때 개체의 모습은 다음과 같습니다.
그림 3. 커넥션 포인트 설치(또는 설정). IConnectionPoint에 대한 ICP 표준.
이벤트를 설치를 위한 호출순서
이제 모든 개체와 인터페이스에 대해 살펴보았으므로, 클라이언트가 이벤트를 받는 데 필요한 단계를 설명하겠습니다.
1. 먼저 클라이언트는 IConnectionPointContainer에 대한
source 개체를 질의합니다. 즉, 연결 가능한 모든 개체(source 개체)는 반드시 IConnectionPointContainer를
구현해야 하는데, IConnectionPointContainer를 구현하지 않는 개체는 어떠한 이벤트도 발생할 수 없습니다.
2. QueryInterface가 성공하면, 클라이언트는
IConnectionPointContainer::FindConnectionPoint에 자신이 받고자 하는 이벤트 인터페이스의 IID를
전달합니다. 연결 가능한 개체가 이 인터페이스를 지원하는 경우, 개체는 그 인터페이스에 대한 커넥션 포인트 개체를 가리키는 포인터를 반환합니다.
선택적으로, 클라이언트는 IConnectionPointContainer::EnumConnectionPoints를 호출하여 열거자를 얻을 수
있으므로, 지원되는 모든 커넥션 포인터를 검사하여 지원 가능한 모든 커넥션 포인트를 찾을 수 있습니다.
3. 커넥션 포인트에 대하여 포인터를 얻었다고 가정하여, 클라이언트는 그 포인터에
대한 IConnectionPoint::Advise를 호출하며, 이때 실제로 이벤트를 받게 될 미니-개체를 가리키는 IUnknown 포인터를
전달합니다. 클라이언트는 Advise가 반환하는 쿠키를 저장하는 역할을 합니다.
4. 이 시점에서, 클라이언트는 자신이 전달한 인터페이스 포인터에 대한 이벤트를
받게 됩니다.
5. 클라이언트가 이벤트를 더이상 받고 싶지 않을 때(가령, 클라이언트가 종료 될
때), IConnectionPoint::Unadvise를 호출하면서 단계 3에서 저장했던 쿠키를 넘겨줍니다.
이벤트 발생시키기
일단 연결을 만들고 나면, 이벤트를 파이어하는 일은 비교적 간단합니다. 즉, Source 개체가 연결을 열거하고,
각 연결에 대해 적절한 메서드를 호출하기만 하면 됩니다.
대부분의 이벤트 인터페이스는 디스패치 인터페이스이므로 이 때, 적합한 방법은 어떤 매개 변수를 사용하여 어떤
디스패치 메서드를 호출할 것인지를 나타내는 해당 매개 변수를 갖는 IDispatch::Invoke입니다.
이벤트 받기
이제 sink 미니-개체는 이벤트를 받아 자신이 선택한 대로 이벤트를 처리합니다. 이 작업은 비교적 간단한
편입니다.
이벤트와 스레드
다음은 기본적으로 지켜야 할 규칙입니다.
특별한 경우가 아니라면(나중에 자세히 설명할 것임), 항상 IConnectionPoint::Advise를 호출했던
스레드와 동일한 스레드의 이벤트를 파이어해야 합니다. 일반적으로 또 다른 스레드를 시작하여 이 스레드로부터 이벤트를 파이어할 수 없습니다.
왜냐하면, 이벤트를 파이어한다는 것은 인터페이스 포인터를 사용하여 또 다른 개체를 호출한다는 것을 의미이기 때문입니다.
COM에서는 절대로 스레드 간에 인터페이스 포인터를 전달할 수 없습니다. 대신, 스레드를 마셜링해야 하며, 이를
위해서 아주 긴 이름을 가진 API 즉, CoMarshalInterThreadInterfaceInStream을 호출합니다.
다른 스레드에 대하여 스트림 포인터를 전달하고 새로운 스레드에 있는
CoGetInterfaceAndReleaseStream을 호출하면 마셜링된 인터페이스 포인터를 얻을 수 있습니다. 유감스럽게도, ATL의 이벤트
파이어 구현은 인터페이스 포인터를 마셜링하지 않으므로, 또 다른 스레드로부터 이벤트를 파이어하려면 약간의 추가 작업이 필요합니다.
두 가지의 일반적인 경우에는, 이 것은 별 문제가 되지 않습니다. 즉, 여러분이 마우스 단추 누름 메시지 같은
Windows 메시지에 대한 응답으로 이벤트를 파이어하는 경우에는 별 문제가 생기지 않으며, 메시지를 수신하였을 때 생성되었던 동일한 스레드
상에서 실행됩니다.
아니면, 여기에 나온 예처럼, 메서드 호출에 대한 응답으로 이벤트를 파이어하는 경우를 들 수 있습니다. 여기서도,
메서드는 생성된 것과 동일한 스레드에서 호출되기 때문에 아무런 문제가 없습니다.
여러분이 직접 명시적으로 스레드를 생성한 경우에는(예를 들면, 백그라운드 프로세싱을 위해) 새로 생성된
스레드로부터 이벤트를 파이어하기는 어렸습니다.
발생된 COM+ 이벤트는?
COM+는 이벤트의 영역에서 중요한 변화를 가져옵니다. 즉, COM+는 우리가 여기서 이야기한 COM 이벤트를
여전히 지원할 뿐 아니라 publish/subscribe 스키마가 사용되는 영속 이벤트를 포함하는 이벤트를 보다 쉽게 파이어하고 파괴할 수 있는
새로운 방법을 제공합니다.
하지만, 이 부분에 대한 자세한 설명은 나중으로 미루도록 하겠습니다. 당분간은 Visual Basic과 스크립팅
언어가 이해할 수 있는 “고전적인” COM 이벤트만 생각하기로 합시다.
다음 기사: ATL이 이벤트를 더 쉽게 만든다!
커넥션 포인트에 관한 설명을 보면 알 수 있듯이, 이벤트 파이어에는 수많은 작업이 따릅니다. 먼저, 네 개의 서로
다른 개체(메인 소스 개체, 커넥션 포인트 열거자, 커넥션 포인트, 그리고 커넥션 열거자) 안에 각각 네 개의
인터페이스(IConnectionPointContainer, IEnumConnectionPoints, IConnectionPoint, 그리고
IEnumConnections)를 만들어야 합니다. 또한, 두 개의 컬렉션(커넥션 포인트와 커넥션)을 관리해야 하고, 이벤트를 파이어할 때 모든
연결의 인터페이스 포인터에 대해 적절한 메서드를 호출하는 코드를 구현해야 합니다. 이벤트를 파이어하는 데는 이처럼 많은 작업이 필요한 것입니다!
더 쉬운 방법은 없을까요? Microsoft Foundation Classes (MFC) 또는 Active
Template Libraries (ATL)를 사용하는 것도 좋은 방법입니다. 그 방법에 관해서는 다음에 설명하도록 하겠습니다.
현재와 앞으로의 계획
이번 기사에서는 COM 이벤트와 커넥션 포인트 등에 관해 알아보았습니다. 실제 코드에 관한 내용은 다음 기사에서
다루도록 하고, 코드 다운로드를 원하시는 분은 http://msdn.microsoft.com/voices/drgui0899.zip 를 이용하시기
바랍니다.
Dr. GUI 저
Dr. GUI와 COM 이벤트, 2부
1999년 11월 15일
Dr. GUI의 비트와 바이트
웹에서 향기를 맡아 보십시오
John Waters의 영화 Polyester를 기억하십니까? 이 영화를 상영하는 극장에서는 관객에게 번호를 매긴
향기 카드를 나누어주었지요. 영화 중간에 어떤 숫자가 화면에 나타나면, 관객들은 카드에서 해당되는 숫자를 긁어 스크린에 나타나는 장면의 향기를
맡을 수 있었습니다.
DigiScents라는 회사가 계획한 대로라면, 여러분은 얼마 안 있어 PC에 “Smell-O-Vision”을
부착하고 웹을 탐색하면서 향기를 맡을 수 있을 것입니다. 이 제품에는 다양한 향수 오일이 사용될 것이라 하는군요.
이 회사가 다음에는 어떤 아이디어를 낼지 사뭇 궁금해지는군요.
Microsoft Exchange 개발자 센터
MSDN에는 점점 더 많은 개발자 센터가 생기고 있습니다. 가장 최근에 생긴 것은 Microsoft
Exchange 개발자 를 위한 개발자 센터입니다. 이 곳을 방문하면 곧 발표될 Exchange 2000에 관한 정보와 Exchange
2000의 베타 버전을 받아볼 수 있습니다. 현재 Exchange를 사용하지 않는 개발자라도 유용하고 다양한 정보를 많이 얻을 수 있습니다.
이벤트를 발생시키는 ATL COM 개체
COM 이벤트 : 잠깐 복습
지난 번 기사 에서 다루었던 이벤트에 관한 내용을 복습해 봅시다.
먼저, 대부분의 이벤트 인터페이스는 dispatch 인터페이스라 할 수 있습니다. 그 이유는 클라이언트가
IDispatch를 구현하고 매개 변수를 알아내는 개체를 만드는 작업이 임의의 사용자 정의 인터페이스를 통해 호출된 매개변수를 알아내는 것보다
훨씬 더 간단하기 때문입니다. 이벤트를 발생시키는 개체, 즉 source 개체는 dispatch 인터페이스를 소스 인터페이스로 정의하고
클라이언트 개체가 타입 라이브러리를 통해 그 인터페이스를 사용할 수 있도록 합니다. 이벤트는 인터페이스를 통해 정의되기 때문에, 동일한
인터페이스에 여러 개의 이벤트 메서드가 있을 수도 있습니다.
클라이언트는 개체의 IConnectionPointContainer 구현 여부를 질의하여 개체가 이벤트를
발생시키는지 알아낼 수 있습니다. 개체가 IConnectionPointContainer를 구현한다면 그 개체는 하나 이상의 이벤트 인터페이스를
구현할 수 있다는 것을 뜻합니다. 이 경우, 클라이언트는 IConnectionPointContainer::EnumConnectionPoints를
호출하여 개체가 어떤 이벤트 인터페이스를 구현하는지 알아낼 수도 있고,
IConnectionPointContainer::FindConnectionPoint를 호출하여 특정 커넥션 포인트를 요청할 수도 있습니다.
커넥션 포인트는 그 이벤트 인터페이스에 대한 모든 커넥션을 관리하는 미니 COM 개체로서 소스 개체에 의해
구현됩니다. 커넥션 포인트는 일반적으로 IUnknown과 IConnectionPoint만을 구현합니다.
소스의 커넥션 포인트 중 하나에 대한 인터페이스 포인터를 얻기 위해 클라이언트는 이벤트를 받는 미니 COM 개체를
만든 다음, 미니 COM 개체에 대한 인터페이스 포인터를 IConnectionPoint::Advise에 전달함으로써 이벤트 인터페이스 커넥션을
설치합니다. 이벤트를 받는 미니 개체는 일반적으로 IUnknown과 IDispatch만 구현합니다.
커넥션이 완성되면, 소스 개체는 클라이언트에 의해 전달된 인터페이스 포인터에 대한 메서드를 호출하여 이벤트를
발생시킬 수 있습니다. 여러 개의 클라이언트가 IConnectionPoint::Advise를 호출하는 경우, 소스 개체는 각 클라이언트가
Advise에 전달한 인터페이스 포인터에 대해 적절한 메서드를 호출하는 루프를 실행함으로써 모든 클라이언트 호출에 대해 이벤트를 발생시켜야
합니다.
더 이상 이벤트를 받고 싶지 않을 때, 클라이언트는 IConnectionPoint::Unadvise를 호출하여
커넥션을 해제합니다. 또한, 클라이언트는 종료전 반드시 커넥션을 해제해야 합니다.
이것을 코드로 작성하려면 상당히 복잡할 뿐 아니라 버그가 발생할 수도 있습니다. 우선, 커넥션 포인트 개체는
커넥션 목록을 관리해야 하고, 이벤트를 발생시키는 과정에서 커넥션 목록을 열거하여 각 커넥션에 대해 적절한 메서드를 호출해야 합니다. 아 참!
깜박 잊을 뻔했군요. VARIANT 매개 변수로 구성된 배열을 만들고, 그 배열을 많은 매개 변수를 갖는 IDispatch::Invoke
메서드에 전달하여 dispatch 인터페이스 메서드 호출도 생성시켜야 합니다. 작업이 복잡하지요? 하지만, 우리에게는 ATL이라는 좋은 친구가
있습니다.
ATL로 구현해보자.
커넥션 포인트에 대한 설명에 나와 있듯이, 이벤트를 발생시키기 위해서는 많은 작업이 필요합니다. 우선, 네 개의
서로 다른 개체(메인 소스 개체, 커넥션 포인트 열거자, 커넥션 포인트, 그리고 커넥션 열거자) 안에 각각 네 개의
인터페이스(IConnectionPointContainer, IEnumConnectionPoints, IConnectionPoint,
IEnumConnections)를 만들어야 합니다. 또한, 두 개의 컬렉션(커넥션 포인트와 커넥션)을 관리해야 하고, 이벤트를 발생시킬 때 모든
커넥션에 대한 인터페이스 포인터에 대해 적절한 메서드를 호출하는 코드도 구현해야 합니다.
더 쉬운 방법은 없을까요? Microsoft Foundation Classes (MFC)를 사용하는 것도 좋은
방법 중 하나입니다. 하지만, 많은 개체가 필요하다는 단점이 있지요. 그렇다면, ATL(Active Template Libraries)을
사용하면 어떨까요?
구현 능력이 뛰어난 ATL
ATL은 IUnknown, IDispatch, IclassFactory와 같은 중요한 인터페이스에 대해 가장
효율적이고, 완벽하고, 디버깅된 스톡 템플릿 구현을 가능케 할 뿐만 아니라, 이벤트 처리에 필요한 구현 기능도 제공합니다.
이러한 구현은 대부분 템플릿화된 클래스를 통해 가능하지만, 이벤트 발생 루프의 구현은 Implement
Connection Point 마법사에 의해 생성되는 “프록시(proxy)” 클래스에 포함되어 있습니다.(이 마법사는 원래 ATL Proxy
Generator라는 독립적인 프로그램이었습니다. 그 이름만 들어도 골치가 아픈 것 같군요)
IDL 만들기
먼저, 소스 인터페이스에 대한 IDL(Interface Definition Language) 코드가 있어야
합니다. 이 코드는 지난 번 기사에서 자세히 설명한 바 있지만, 여러분의 이해를 돕고자 다시 한번 소개합니다. 소스 인터페이스 자체에 대한
IDL도 있어야 하지만, 개체 IDL의 coclass 섹션에는 인터페이스를 소스 인터페이스로 선언하는 부분도 있어야 합니다. 다음은 소스
인터페이스 자체에 대한 IDL입니다.
[
uuid(F2F660CF-3ED7-11D3-9C8C-000039714C10),
helpstring(“_IAAAFireLimitEvents Interface”)
]
dispinterface _IAAAFireLimitEvents
{
properties:
methods:
[id(1), helpstring(“method Changed”)]
HRESULT Changed(IDispatch *Obj,
CURRENCY OldValue);
[id(2), helpstring(“method SignChanged”)]
HRESULT SignChanged(IDispatch *Obj,
CURRENCY OldValue);
};
coclass 섹션에서 인터페이스를 소스 인터페이스로 선언하는 부분(볼드체로 표시)은 다음과 같습니다
coclass AAAFireLimit
{
[default] interface IAAAFireLimit;
[default, source] dispinterface _IAAAFireLimitEvents;
};
ATL의 마법사를 적절히 사용하면, 위의 코드를 직접 작성할 필요가 없습니다.
클래스 유도
ATL은 IConnectionPointContainer와 IConnectionPoint에 대한 스톡 템플릿 구현인
IConnectionPointContainerImpl과 IConnectionPointImpl을 제공합니다. 두 구현을 모두 사용하기는 하지만,
IConnectionPointImpl는 간접적으로 사용할 것입니다.
메인 소스 개체(클래스 이름이 CAAAFireLimit인 개체) 안에
IConnectionPointContainer를 구현하기 위해서는 두 가지 작업을 거쳐야 합니다.
첫째, 아래의 코드 라인을 상속 리스트에 추가합니다.
public IConnectionPointContainerImpl<CAAAFireLimit>
Dr.GUI는 프로젝트 이름을 “AAAFireLimit”로 정했기 때문에, 클래스 이름이
CAAAFireLimit가 됩니다. 앞의 “AAA”는 이 개체가 Visual Basic 개체 참조 리스트(Project.References)의
맨 위에 들어가게 할 목적으로 사용된 것입니다. 개체가 리스트의 맨 위에 있으면, 개체 참조 및 참조 취소 작업이 더 수월합니다.
둘째, 개체의 COM 맵에 IConnectionPointContainer에 대한 항목을 추가합니다.
COM_INTERFACE_ENTRY(IConnectionPointContainer)
커넥션 포인트 맵
커넥션 포인트 컨테이너는 커넥션 포인트 맵에 의존하며, 커넥션 포인트 맵에는 커넥션 포인트 당 한 개의 엔트리가
들어 있습니다(예를 들어, 소스 인터페이스 당 한 개의 엔트리). 이 맵은 CAAAFireLimit 클래스 선언 내에 포함됩니다.
BEGIN_CONNECTION_POINT_MAP(CAAAFireLimit)
CONNECTION_POINT_ENTRY(DIID__IAAAFireLimitEvents)
END_CONNECTION_POINT_MAP()
이 맵은 IConnectionPointContainer 구현에 ID가
DIID__IAAAFireLimitEvents인 인터페이스(즉, 인터페이스 IAAAFireLimitEvents)에 대한 커넥션 포인트가 단
하나임을 알려줍니다. 더 많은 소스 인터페이스를 구현했다면, CONNECTION_POINT_ENTRY 매크로가 더 많았을 것입니다.
이제 IconnectionPoint 문제는 해결 되었군요. 그러면, 실제 커넥션 포인트와 이벤트를 발생시키는
루프는 어떻게 얻을 수 있을까요?
ATL의 개체 마법사를 적절히 사용하면, 위의 코드는 자동적으로 추가됩니다. 그러나, 이벤트를 디버그하거나 기존의
개체에 직접 코드를 추가해야 하는 경우에는, ATL의 개체 마법사에 의해 추가된 코드를 알아야 합니다.
IConnectionPointImpl과 프록시 클래스
이제, 여러분은 IConnectionPointImpl로부터 상속을 받을 것이라고 생각하시겠지요? 그렇지만,
IConnectionPointImpl로부터 직접 상속받는 대신, Visual Studio로 하여금 IConnectionPointImpl로부터
유도되고 각 이벤트 메서드에 대해 Fire_[event] 추가 멤버 함수를 구현하는 템플릿화된 “프록시” 클래스를 생성하게 합니다. 우리가 만든
인터페이스는 Changed와 SingChanged라는 두 개의 메서드를 가지므로, 커넥션 포인트 프록시 클래스는 Fire_Changed와
Fire_SignChanged를 구현합니다.
각 메서드의 구현에는 IConnectionPoint::Advise로 전달된 인터페이스 포인터 각각에 대한 호출을
생성시키는 루프가 포함되어 있습니다.
이제 템플릿화된 프록시 클래스로부터 상속을 받습니다. 이 클래스(Fire_ 메서드 포함)는 Implement
Connection Points 마법사라고 하는 프로그램(전에는 ATL Proxy Generator로 했음)에 의해 생성됩니다. 이 마법사를
어떻게 실행시키는지는 나중에 자세히 설명할 것입니다. 주의할 것은 이 마법사가 입력 값으로 타입 라이브러리를 요구하므로, 프록시를 생성하기 전에
반드시 IDL 파일을 컴파일해야 한다는 것입니다.
Implement Connection Points 마법사는 아래의 코드 라인을 CAAAFireLimit의 상속
리스트에 추가합니다.
public CProxy_IAAAFireLimitEvents< CAAAFireLimit >
CProxy_IAAAFireLimitEvents는 마법사가 생성한 클래스의 이름입니다. 마지막으로, 마법사는
커넥션 포인트 맵에 커넥션 포인트 엔트리를 추가합니다.
BEGIN_CONNECTION_POINT_MAP(CAAAFireLimit)
CONNECTION_POINT_ENTRY(DIID__IAAAFireLimitEvents)
END_CONNECTION_POINT_MAP()
커넥션 포인트가 여러 개인 경우(즉, 복수의 소스 인터페이스인 경우), 각 커넥션 포인트에 대해 프록시
클래스로부터의 유도를 시도해야 합니다. 주의해야 할 점은 하나의 상속 목록에 있는 동일한 클래스로부터 여러 번 상속을 받을 수 없다는 점인데,
그러나 이것은 별 문제가 되지 않습니다. 동일한 템플릿이라고 해도 서로 다른 매개 변수에 대해 약간씩 다른 클래스를 생성하기 때문에 동일한
클래스로부터 여러 번 상속을 받는 일은 없을 테니까요.
다음은 Fire_ 메서드 중 하나(Fire_SignChanged)가 생략된 클래스의 코드 리스트입니다. 프록시
클래스가 IConnectionPointImpl로부터 유도되는 것을 볼 수 있습니다. Fire_ 메서드에 대한 코드를 보십시오. 이 코드를 직접
작성할 필요가 없다는 게 얼마나 다행입니까?
template <class T>
class CProxy_IAAAFireLimitEvents :
public IConnectionPointImpl<T,
&DIID__IAAAFireLimitEvents,
CComDynamicUnkArray>
{
//Warning this class may be recreated by the wizard.
public:
HRESULT Fire_Changed(IDispatch * Obj, CY OldValue)
{
CComVariant varResult;
T* pT = static_cast<T*>(this);
int nConnectionIndex;
CComVariant* pvars = new CComVariant[2];
int nConnections = m_vec.GetSize();
for (nConnectionIndex = 0;
nConnectionIndex < nConnections;
nConnectionIndex++)
{
pT->Lock();
CComPtr<IUnknown> sp =
m_vec.GetAt(nConnectionIndex);
pT->Unlock();
IDispatch* pDispatch =
reinterpret_cast<IDispatch*>(sp.p);
if (pDispatch != NULL)
{
VariantClear(&varResult);
pvars[1] = Obj;
pvars[0] = OldValue;
DISPPARAMS disp = { pvars, NULL, 2, 0 };
pDispatch->Invoke(0x1,
IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_METHOD, &disp,
&varResult, NULL, NULL);
}
}
delete[] pvars;
return varResult.scode;
}
//… Fire_SignChanged omitted, similar to Fire_Changed
};
IConnectionPointImpl 탐구
지금까지 IConnectionPoint에 대한 COM_INTERFACE_ENTRY는 포함시키지 않았는데, 왜
그랬을까요?
메인 개체가 IConnectionPoint를 구현하지 않는 대신,
IConnectionPointContainer::FindConnectionPoint는 IUnknown과 IConnectionPoint를 구현하는
개별적인 개체에 대한 포인터를 반환합니다.
사실 Dr.GUI는 IconnectionPointImpl이?구성되는 동안 new를 사용하여 이 개체를 생성할
것이라 여겼지만, ATL 설계자들은 그보다 더 똑똑했던 모양입니다. 다중 상속, 템플릿, 그리고 약간의 포인터 조작을 통해, 커넥션 포인트
개체(또는 개체들)와 그 개체가 필요로 하는 데이터(커넥션 컬렉션에 대한 데이터)를 메인 개체 안에서 작성할 수 있게 만들었으니 말입니다.
각각의 커넥션 포인트 개체는 개별적인 COM 식별자(identity)를 갖고 있으므로, 메인 개체의 일부로 구현되더라도 논리적으로는 개별적인
개체라 할 수 있습니다. COM은 구현에 관여하지 않기 때문에(즉, behavior에만 관여), 규칙 위반이라 볼 수 없으며, 또한 COM
규칙을 어기지만 않는다면, 어떤 방식으로든 개체를 구현할 수 있습니다.
이와 같은 교묘한 트릭을 본 칼럼에서 자세히 다루기는 어려울 것 같습니다. 따라서, 이에 대한 자세한 정보를
원하시면 ATL Internals (Rector와 Sells, Addison-Wesley 출판)의 390 – 396쪽을 참고하거나 ATL의 소스
코드를 직접 확인하시기 바랍니다.
중요한 마지막 단계: IProvideClassInfo2
마지막으로, 클라이언트에게 타입 라이브러리를 어떻게 얻을 수 있으며 기본 소스 인터페이스를 어떻게 액세스할 수
있는지 알려주는 게 당연하겠지요. 이를 위해서는 IprovideClassInfo2를 구현해야 합니다. IP아래의 코드를 메인 클래스의 상속
리스트에 추가하면 ATL이 IProvideClassInfo2를 구현해 줍니다.
public IProvideClassInfo2Impl<&CLSID_AAAFireLimit,
&DIID__IAAAFireLimitEvents,
&LIBID_AAAFIRELIMITMODLib,
LIBRARY_MAJOR, LIBRARY_MINOR>
이제, LIBRARY_MAJOR와 LIBRARY_MINOR라는 두 개의 매크로를 타입 라이브러리의 버전 번호에
맞게 정의해야 합니다. 두 매크로는 헤더 파일의 시작 부분에서 정의합니다. 여기서는, 다음과 같이 두 개의 매크로를 정의해 보았습니다. 여러분은
새 버전의 개체를 만들 때마다 이 두 매크로 정의를 업데이트하면 됩니다.
// version number of type library
#define LIBRARY_MAJOR 1
#define LIBRARY_MINOR 0
COM 맵에 엔트리를 추가하는 것도 잊지 마십시오. 여러분이 작성한 개체는 IProvideClassInfo와
IProvideClassInfo2를 구현할 것이므로, 다음과 같이 두 개의 엔트리를 추가해야 합니다.
COM_INTERFACE_ENTRY(IProvideClassInfo2)
COM_INTERFACE_ENTRY(IProvideClassInfo)
나머지 부분은 ATL이 알아서 처리합니다.
스레드 문제
Dr.GUI가 전에도 말씀 드린 적이 있지만, 새 스레드를 만들고 그 스레드에서 이벤트를 발생시키는 것은
불가능하며, 새 스레드에서 그냥 Fire_Change를 호출해도 문제가 발생합니다.
문제는 이벤트 인터페이스 포인터 저장에 사용되는 스레드(IConnectionPoint::Advise를 호출할
때)와 저장된 포인터를 사용하는 스레드(Fire_Changed를 호출할 때)가 서로 다르다는 데 있습니다. Fire_Changed 안에 있는
코드는 저장된 인터페이스 포인터를 직접 사용합니다. 그 코드는 다음과 같습니다.
CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
이것은 마셜링되지 않은(원시) 인터페이스 포인터를 스레드 간에 전달할 수 없다는 COM 규칙을 위반하는 것이지만,
또한 이러한 부분이 간과되기도 쉽습니다. 즉, 인터페이스 포인터가 ATL과 Fire_ 메서드 안에 잘 숨겨지기 때문이죠. 하지만, 클라이언트가
여러 스레드로부터의 호출을 처리하는 능력이 있는지(예를 들어, 클라이언트가 다중 스레드 연산을 처리할 수 있는지)를 알아내는 방법은 없기
때문에, 이 규칙을 지키는 것은 매우 중요합니다.
이 문제를 해결하려면, IConnectionPoint::Advise 에서 인터페이스 포인터를 마셜링하고(원래의
스레드에서 실행할 때), Fire_Changed 에서 인터페이스 포인터 마셜링을 취소해야 합니다(새 스레드에서 실행할 때). 또한, 새
스레드에서 CoInitializeEx를 호출하는 것도 잊지 마십시오. 이 호출은 COM이 사용되는 모든 스레드에 반드시 필요합니다.
이 내용을 이번 기사에서 코드로 소개하기에는 좀 복잡할 것 같군요. 하지만, 솔루션이 필요한 분들은 여기에 나온
내용(기타 COM에 관한 내용)을 유용하게 활용할 수 있을 것입니다. 코드에 관한 부분은 다음 칼럼에서 소개하도록 하겠습니다.
ATL의 예
ATL이 대부분의 성가신 작업을 처리해 준다고 해서, 위의 작업이 쉽다는 것을 의미하는 것은 아닙니다. 하지만
걱정하지 마십시오. ATL의 훌륭한 마법사와 체크 박스가 상기 내용을 코드로 구현하는 데 많은 도움이 될 테니까요. 여러분이 할 일은 소스
인터페이스를 정의하고 필요한 때에 이벤트를 발생시키는 것 뿐입니다.
우리가 만들 개체는 아주 간단합니다. 여러분은 이 개체의 값을 얻거나 설정할 수도 있고, 이 개체에 새로운 값을
추가할 수도 있습니다. 개체의 값이 변경되거나 값의 부호가 바뀌게 되면 이벤트는 발생됩니다.
단계1: 모듈 만들기
COM 개체를 만들 때는, 먼저 개체가 들어갈 모듈을 만들어야 합니다(아직 없다면). 이 단계는 그다지 복잡하지
않으며, ATL COM AppWizard에서 여러분의 프로젝트에 적합하게 옵션을 설정하기만 하면 됩니다. Dr.GUI는 개체의 이름을
“AAA”로 시작해서(개체가 알파벳순으로 정렬되도록 함) “Mod”로 끝내겠습니다(모듈이 개체와 구별될 수 있게 함).
이름을 입력했으면 모든 기본 옵션을 그대로 선택하십시오. 이에 관한 자세한 내용은 Dr.GUI의 ATL 대체에
대한 기사 를 참고하시기 바랍니다
단계 2: 개체 만들기(반드시 커넥션 포인트 지원을 요청해야 함)
일반적인 개체를 만들 때와 마찬가지로, Insert.New ATL Object (Insert 메뉴에 있음)를
사용하여 Simple Object를 선택한 다음, 개체의 이름을 입력합니다. 단, 전 단계에서 사용한 것과 동일한 이름을 입력해야 합니다(접미사
“Mod”는 빼고). 아직 OK 단추를 클릭하지 마십시오. 이 개체에 대한 애트리뷰트를 설정해야 하므로, Attributes 탭을 클릭하고
Support Connection Points 확인란에 표시하십시오. 그러면 다음과 같이 대화상자가 나타납니다.
그림 1. Support Connection Points가 선택된 ATL Object Wizard
반드시 Support Connection Points 확인란에 표시를 해야 합니다. .
참고 어떤 이유에서인지는 모르지만, Dr. GUI는 Visual Studio을 설치할 때 Insert.New
ATL Object 명령을 빠뜨렸군요. 이 문제는 Service Pack 5 를 설치하면 쉽게 해결됩니다.
ATL Object Wizard에 의해 생성된 코드를 살펴 보면, 전에 본 적이 없는 코드가 발견될 수도
있습니다.
· 소스 인터페이스는 위에서 설명한 대로 IDL 파일에 추가됩니다. (단,
메서드 선언은 후속 단계에서 추가됩니다.)
· 소스 인터페이스 선언은 위에서 설명한 대로 IDL 파일의 coclass
섹션에 추가됩니다.
· 위에서 설명한 대로 IConnectionPointImpl로부터 ATL
클래스가 유도됩니다.
· 커넥션 포인트 맵은 이미 추가되어 있습니다. (엔트리는 아직 추가되지
않았음).
단계 3: 소스(이벤트) 인터페이스에 메서드 추가하기
다음 단계는 일반 Visual Studio 메커니즘(Class View에서 소스 인터페이스 이름을 오른쪽 마우스
버튼으로 클릭한 다음, Add Method를 선택함)을 사용하여 두 개의 메서드를 추가하는 것입니다.
Changed 메서드의 프로토타입은 다음과 같습니다.
HRESULT Changed(IDispatch *Obj, CURRENCY OldValue);
이 프로토타입을 보면, 개체 안의 값이 변경될 때 원래 값과 함께 개체에 대한 인터페이스 포인터가 메서드에
전달되는 것을 알 수 있습니다. 인터페이스 포인터를 전달하면 클라이언트가 모든 메서드와 속성을 액세스할 수 있게 됩니다.
여기서는 Dr.GUI의 데이타 유형에 따른 COM 자동화에 대한 기사에서 설명한 대로, COM CURRENCY
타입을 사용했습니다.
SignChanged의 프로토타입도 Changed의 경우와 비슷합니다.
단계 4: IDL 컴파일하기
메서드를 선언한 다음, IDL 파일을 컴파일합니다. 가장 빠른 방법은 File View로 이동하여, IDL 파일을
찾아 오른쪽 마우스 버튼으로 클릭하고 Compile을 선택하는 것입니다(시간이 충분하다면, 전체 프로젝트를 작성해도 상관 없겠지요).
단계 5: 커넥션 포인트 구현하기
IDL을 컴파일한 후에는, 다시 Class View로 이동하여 개체의 클래스 이름을 오른쪽 마우스 버튼으로
클릭하고(이 예에서는, CAAAFireLimit), Implement Connection Point를 선택합니다. 타입 라이브러리를 적절하게
컴파일했다면, 다음과 같은 대화 상자가 나타날 것입니다.
그림 2. 이벤트 인터페이스가 선택된 Implement Connection Point 박스
커넥션 포인트를 구현할 인터페이스(들) 확인란에 표시를 합니다. (다시 한번 말하지만, 이 마법사는 기존의 ATL
Proxy Generator에서 업데이트된 버전입니다)
그러면, 마법사는 다음과 같이 코드를 변경합니다.
· 새 “proxy” 클래스를 프로젝트에 추가합니다(이 예에서는
CProxy_IAAAFireLimitEvents). 이 클래스는 커넥션 포인트의 구현으로서 소스는 개별 파일에 각각 들어 있습니다. 이 클래스는
IConnectionPointImpl로부터 유도되며 이벤트 인터페이스의 각 메서드에 대해 Fire_ 메서드를 추가합니다. 앞에서 이 클래스
목록을 이미 보아서 알겠지만 마법사가 이 메서드를 작성해 줄 수 있다는 것은 정말이지 놀라운 일이 아닐 수 없습니다.
· 새 클래스를 유도 리스트에 추가합니다. .
· 이벤트 인터페이스에 대한 엔트리를 개체의 커넥션 포인트 맵에 추가합니다.
우리는 이 클래스로부터 상속을 받으므로, 이 클래스의 Fire_ 메서드를 쉽게 호출할 수 있습니다. 만약 이벤트
인터페이스를 조금이라도 변경하면, 전체 단계(IDL을 다시 컴파일하고 커넥션 포인트를 구현)를 다시 거쳐야 하고, 프록시 클래스도 처음부터
재생성됩니다. 따라서, 이벤트 인터페이스의 코드는 변경하지 않는 것이 좋겠지요?
단계 6: IClassInfo2의 구현 추가하기
IClassInfo 및 IClassInfo2 구현이 모두 제공되는 것이 바람직하다는 점을 앞에서도 언급한 바
있습니다. 해당 섹션에서 설명한 코드를 추가하면 됩니다.
단계 7: 개체의 속성 및 메서드 추가 그리고 이벤트 발생
마지막으로, 일반 속성 및 메서드를 추가하고 이벤트를 발생시킵니다.
앞에서 말했듯이, 이 개체는 현재 값을 기억하고 있다가 값이 변경되거나 값의 부호가 바뀔 때 이벤트를 발생시키는
개체입니다. 따라서, 우리는 값에 대한 속성을 추가할 것입니다(디스패치 ID를 0으로 설정하여 속성을 기본값으로 설정). 또한, 개체에 숫자를
추가할 수 있도록 Add 메서드를 추가하겠습니다. 이 개체의 인터페이스에 대한 IDL은 다음과 같습니다.
interface IAAAFireLimit : IDispatch
{
[propget, id(0), helpstring(“property Value”)]
HRESULT Value([out, retval] CURRENCY *pVal);
[propput, id(0), helpstring(“property Value”)]
?HRESULT Value([in] CURRENCY newVal);
[id(1), helpstring(“method Add”)]
HRESULT Add(CURRENCY cyAddend);
};
메서드 구현은 여러분이 예상하는 것과 거의 유사할 것입니다. 한 가지 새로운 점이 있다면, 개체의 값을 변경할
가능성이 있는 모든 메서드는 필요한 이벤트를 발생시키는?핼퍼 메서드 CheckAndFire를 호출한다는 것입니다.
STDMETHODIMP CAAAFireLimit::get_Value(CURRENCY *pVal)
{
*pVal = m_cyValue; // 값을 변경할 수 없음.
}
return S_OK;
STDMETHODIMP CAAAFireLimit::put_Value(CURRENCY newVal)
{
CURRENCY oldVal = m_cyValue;
m_cyValue = newVal;
CheckAndFire(oldVal);
return S_OK;
}
STDMETHODIMP CAAAFireLimit::Add(CURRENCY cyAddend)
{
CURRENCY cyOld = m_cyValue;
VarCyAdd(m_cyValue, cyAddend, &m_cyValue);
CheckAndFire(cyOld);
return S_OK;
}
데이터 멤버인 m_cyValue는 CURRENCY 타입의 클래스 멤버입니다. 여기서는 데이터 유형에 대한 칼럼
에서 설명된 대로, COM의 VarCyAdd 함수를 사용하여 값을 추가했습니다.
또한, 이전의 값을 기억하고 있다가 이벤트를 발생시킬 필요가 있는지 검사합니다. 만약 값을 기존의 값으로
재설정하면 이벤트가 발생되지 않습니다. 즉, Changed 이벤트는 실제로 값이 변경될 때에만 발생됩니다.
CheckAndFire 메서드는 약간 복잡하군요. Changed 이벤트의 경우는 비교적 간단하지만,
SignChanged 이벤트의 경우에는 약간 까다로운 편입니다. SignChanged 이벤트는 값이 양수에서 음수로, 또는 그 반대로 바뀔
때에만 발생되어야 하기 때문입니다(값이 0이 되는 것이 아무 의미가 없음). 또한, 값이 양수 0에서 음수 0이 되는 경우나 그 반대의 경우에도
SignChanged 이벤트가 발생되어야 합니다. CheckAndFire 메서드는 이러한 경우를 적절하게 처리하기 위해 인스턴스
변수(hrOldSign, constructor에서 VARCMP_EQ로 초기화됨)를 사용하여 부호를 추적합니다.
COM의 VarCyCmp 함수는 값을 비교하는 데 사용됩니다. 또한, 다음과 같이 0으로 초기화된 전역 변수
cyZero도 만들었습니다.
CURRENCY cyZero = { 0i64 };
CheckAndFire 메서드는 다음과 같습니다.
void CAAAFireLimit::CheckAndFire(CURRENCY cyOld)
{
// Fire event if value changed
HRESULT hrCmpRes = VarCyCmp(m_cyValue, cyOld);
if (hrCmpRes != VARCMP_EQ)
Fire_Changed(this, cyOld);
// Fire event if sign changed
HRESULT hrCmpZero = VarCyCmp(m_cyValue, cyZero);
if (hrCmpZero != VARCMP_EQ) {// 0이 아님
if (hrCmpZero != hrOldSign && hrOldSign != VARCMP_EQ)
{
Fire_SignChanged(this, cyOld);
}
hrOldSign = hrCmpZero;
}
}
이벤트를 발생시켜야할 경우, this 포인터(클라이언트가 메서드를 호출할 수 있도록)와 이전 값(클라이언트
정보용)을 함께 전달합니다.
마지막으로, m_cyValue 를 cyZero로 초기화합니다.
이제, 개체를 작성하고 디버그한 후, 그것을 이벤트에 응답 가능한 클라이언트에서 실행하면 됩니다. 이벤트를 받는
클라이언트에 관한 설명은 다음 기회로 미루겠습니다.
기존의 개체에 이벤트를 추가하려면?
기존의 개체에 이벤트를 추가하려면, 단계 2에서 Support Connection Points 확인란을 표시한 후
추가되었던 모든 코드를 추가해야 합니다. 이때도 마법사를 사용하여 프록시 클래스를 생성할 수 있습니다.
직접 해 보십시오!
지금까지 배운 내용을 실습해 보지 않는다면 수박 겉핥기식이라고 말할 수 있을 것입니다. 직접 해보십시오. 그것이
가장 확실한 방법입니다.
메서드를 호출했을 때 이벤트를 발생시키는 COM 개체를 직접 작성해보십시오(스레드 사이에 인터페이스 포인터를
어떻게 마셜링해야 할지 모르는 경우, 새 스레드를 시작하여 스레드에서 이벤트를 발생시키지 마십시오). 비쥬얼 ATL 컨트롤을 작성할 줄 아는
사람이라면 그 컨트롤에 어떤 조작이 가해졌을 때(예를 들어, 사용자가 컨트롤을 클릭할 때) 이벤트를 발생시킬 수 있을 것입니다.
Visual Basic 클라이언트를 작성하여 테스트해 볼 수도 있고, 웹 페이지와 스크립트를 사용할 수도
있습니다. Visual Basic 클라이언트는 샘플 코드에 나와 있습니다.
현재와 앞으로의 계획
이번 기사에서는 ATL COM 개체에 이벤트를 추가하는 방법을 알아보았습니다. 이벤트를 추가하는 데는 많은 작업이
요구되지만, 마법사와 프록시 생성기(proxy generator)를 적절히 사용하면 복잡한 작업도 쉽게 해결할 수 있습니다.