본문 바로가기

출판물/C# .NET 4 Platform

[C# .NET4] 부록(4/5): COM과 .NET 상호운용성

부록 A - COM.NET 상호운용성

- 목차

이 문서에 대해

.NET 상호운용성의 유효 범위

.NET에서 COM Interop을 사용하는 간단한 예제

.NET Interop 어셈블리 살펴보기

런타임 호출 가능 래퍼

COM IDL의 역할

타입 라이브러리를 이용해 Interop 어셈블리 만들기

좀 더 복잡한 COM 서버 만들기

Interop 어셈블리에 대하여

COM에서 .NET으로의 상호운용성

CCW의 역할

.NET 클래스 인터페이스의 역할

.NET 타입 만들기

타입 라이브러리 생성과 .NET 타입 등록

생성된 타입 정보 확인하기

비주얼 베이직 6.0 테스트 클라이언트 만들기

 

 

Interop 어셈블리에 대하여

tlbimp.exe 유틸리티를 사용하는 대신 Visual Studio 2008로 새 콘솔 프로젝트(CSharpCarClient라는 이름으로)를 생성한 후 참조 추가 대화상자의 COM 탭에서 Vb6ComCarServer.dll에 대한 참조를 추가한다. 그러고 나서 VS 2008 개체 브라우저 유틸리티를 이용해 그림 A13에서처럼 interop 어셈블리를 열어보자.

 

그림 A-13  Interop.VbComCarServer.dll 어셈블리

 

앞서 봤던 것과 비슷하게 Class 접미사와 밑줄 문자가 앞에 붙은 인터페이스 타입을 볼 수 있다. 또한 지금까지 다루지 않았던 항목들이 몇 가지 있고 이름으로 유추해보면 COM으로부터 .NET으로 전달되는 이벤트에 관련된 것임을 알 수 있다(_CoCar_Event, __CoCar_SinkHelper, __CoCarBlewUp- EventHandler). 부록 A의 앞부분에서도 언급했듯이 COM 객체가 COM 이벤트를 외부로 노출하는 경우에는 interop 어셈블리에 추가적인 CIL 코드가 삽입되고 이 코드를 이용해 COM 이벤트를 .NET 이벤트로 변환시켜준다고 했다(잠시 후 이에 대한 실제 예를 볼 것이다).

 

 

C# 클라이언트 응용 프로그램 만들기

CLR이 필요한 RCW를 자동으로 생성해주므로 C# 응용 프로그램에서는 CoCar, CarType, Engine, IDriverInfo 타입들을 마치 관리 코드로 작성된 타입인 것처럼 직접 불러다 사용할 수 있다. 다음이 바로 이에 대한 구현 코드다.

 

// Vb6ComCarServer 네임스페이스를 import하는 것을 잊지 말자

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** CoCar Client App *****");

// 초기 바인딩을 이용해 COM 클래스 생성.

CoCar myCar = new CoCar();

// BlewUp 이벤트 처리.

myCar.BlewUp += new __CoCar_BlewUpEventHandler(myCar_BlewUp);

// Create() 메서드 호출.

myCar.Create(50, 10, CarType.BMW);

// Driver의 이름 설정.

IDriverInfo itf = (IDriverInfo)myCar;

itf.DriverName = "Fred";

Console.WriteLine("Drive is named: {0}", itf.DriverName);

// car의 타입 출력.

Console.WriteLine("Your car is a {0}.", myCar.CarMake);

Console.WriteLine();

// Engine 객체를 얻고 각 실린더의 이름 출력.

Engine eng = myCar.GetEngine();

Console.WriteLine("Your Cylinders are named:");

string[] names = (string[])eng.GetCylinders();

foreach (string s in names)

{

Console.WriteLine(s);

}

Console.WriteLine();

// car의 속도를 높여 이벤트 발생.

for (int i = 0; i < 5; i++)

{

myCar.SpeedUp();

}

}

 

코드 중 눈여겨볼 만한 부분으로, GetCylinders()를 호출하는 부분에서 반환 값을 문자열 배열로 캐스팅하는 것이 있다. 이렇게 하는 이유는 COM에서 배열은 (대부분의 경우) SAFEARRAY COM 타입으로 표현되기 때문이다(VB6COM 응용 프로그램을 만들 때는 항상 이렇다). RCWSAFEARRAY 타입을 C# 구문으로 표현된 배열로 자동 변환하기보다는 System.Array 타입으로 변환을 시켜준다. 따라서 Array 객체를 string[]으로 캐스팅함으로써 이를 좀 더 자연스럽게 처리할 수 있다.

 

 

CoCar 타입 사용하기

앞서 만든 VB6 CoCar는 컴파일러가 자동으로 만들어준 기본 인터페이스(_CoCar)IDriverInfo라는 인터페이스를 추가로 정의했다. Main() 메서드에서 CoCar 인스턴스를 생성하고 이를 사용할 때에 _CoCar 인터페이스에 의해 노출되는 메서드들만 직접 접근할 수 있다.

 

// 실제로는 [default] 인터페이스를 사용하는 것이다.

myCar.Create(50, 10, CarType.BMW);

 

IDriverInfo 인터페이스의 DriverName 속성을 호출하려면 다음의 예에서 볼 수 있는 것처럼 CoCar 객체를 IDriverInfo 인터페이스로 명시적으로 캐스팅해줘야 한다.

 

// Driver의 이름을 설정한다.

IDriverInfo itf = (IDriverInfo)myCar;

itf.DriverName = "Fred";

Console.WriteLine("Drive is named: {0}", itf.DriverName);

 

하지만 앞에서 언급한 대로 타입 라이브러리를 interop 어셈블리로 변환시키면 이 어셈블리 안에는 모든 인터페이스의 모든 메서드를 노출하는 Class 접미사가 붙은 타입들도 포함된다. 따라서 CoCar 객체 대신 CoCarClass 객체를 생성해 사용하면 좀 더 간단한 프로그램 코드를 작성할 수 있다. 예를 들어 CoCar의 기본 인터페이스 외에도 IDriverInfo 인터페이스를 사용하는 다음과 같은 메서드가 있다고 가정해보자.

 

static void UseCar()

{

  // Class 접미사가 붙은 타입들은  // 모든 인터페이스의 모든 멤버를 노출한다.

  CoCarClass c = new CoCarClass();

  // 이 속성은 IDriverInfo의 멤버다.

  c.DriverName = "Mary";

 

  // 이 메서드는 _CoCar의 멤버다.

  c.SpeedUp();

}

 

이 하나의 타입이 각 인터페이스의 모든 메서드를 어떻게 노출하는지 궁금하다면 Visual Studio 2008의 개체 브라우저에서 CoCarClass의 기본 클래스와 구현하는 인터페이스 목록을 한 번 들여다보길 바란다(그림 A14 참조)

 

그림 A-14  CoCarClass의 구성

 

앞의 그림에서 볼 수 있듯이 해당 타입은 _CoCar_IDriverInfo 인터페이스를 구현하며보통public 멤버들을 노출하는 것을 알 수 있다.

 

 

COM 이벤트 받기

11장에서 알아본 .NET 이벤트 모델의 구조에서는 응용 프로그램의 한 부분에서 처리 로직을 다른 부분으로 위임하는 방식으로 동작한다고 배웠다. 이 과정에서 이벤트 처리 요청을 실제로 전달하는 개체가 바로 System.MulticastDelegate를 상속받는 타입으로 C#delegate 키워드를 이용해 생성한다고도 배웠다.

tlbimp.exe 유틸리티에서 COM 서버의 타입 라이브러리를 읽을 때 이벤트와 관련된 정의를 발견하면 tlbimp.exeCOMconnection point 아키텍처를 래핑하는 몇 개의 관리 타입을 생성해준다. 이 타입들을 이용하면 마치 System.MulticastDelegate의 내부 메서드 리스트에 멤버를 추가하는 것처럼 사용할 수 있다. 하지만 실제 내부 작동 방식은 프록시가 들어 오는 COM 이벤트를 이에 해당하는 .NET 관리 이벤트로 변환하는 방식으로 작동한다. A3을 보면 이들 타입에 대한 설명을 볼 수 있다.

 

A-3. COM 이벤트 도우미 타입들

생성된 타입 (_CarEvents [source] 인터페이스에 기반한)

실제 의미

__CoCar_Event

System.MulticastDelegate의 연결 리스트에 메서드를 추가(또는 제거) 하는 addremove 멤버들을 정의 하는 관리 타입 인터페이스

__CoCar_BlewUpEventHandler

System.MulticastDelegate를 상속 받는 관리 delegate

__CoCar_SinkHelper

이 자동 생성된 클래스는 .NET에서 사용 가능한 sink 객체로, 이벤트를 받기 위한 인터페이스를 구현 한다.

 

결론적으로 말하면, .NET 위임을 이용한 이벤트 처리와 동일한 방식으로 COM 이벤트를 처리할 수 있다.

 

class Program

{

  static void Main(string[] args)

  {

    Console.WriteLine("***** CoCar Client App *****");

    CoCar myCar = new CoCar();

    // BlewUp 이벤트 처리

    myCar.BlewUp += new __CoCar_BlewUpEventHandler(myCar_BlewUp);

    ...

  }

 

  // BlewUp 이벤트 처리  static void myCar_BlewUp()

  {

    Console.WriteLine("Your car is toast!");

  }

}

 

한가지 더 짚고 넘어 가고 싶은 것은 C# 코드에서 COM 객체의 이벤트를 처리 할 때 익명 메서드, 메서드 그룹 변환, 람다식 등의 이벤트 관련 구문을 다 활용 할 수 있다는 점이다.

 

소스코드

CSharpCarClient 프로젝트는 Appendix A 서브디렉토리에 있다.

 

이것으로 .NET 응용 프로그램과 기존 레거시 COM 응용 프로그램이 서로 통신하는 방법에 대한 설명을 마친다. 지금까지 배운 이 기술들은 모든 COM 서버에 적용 가능하다는 점을 알아두자. 이것이 중요한 이유는 기존의 많은 COM 서버가 .NET 응용 프로그램으로 재작성될 가능성이 없기 때문이다. 한 가지 예로 마이크로소프트 아웃룩의 객체 모델은 현재 COM 라이브러리를 통해 노출되고 있다. 따라서 이 제품과 상호작용하는 .NET 응용 프로그램을 만들고자 한다면 이 상호운용성 계층을 사용하는 방법 외엔 (현재로서는) 없다.

 

 

COM에서 .NET으로의 상호운용성

부록 A에서 다룰 다음 주제는 바로 COM 응용 프로그램이 .NET 타입과 통신하는 과정이다. 방향interop는 기존의 레거시 COM 코드(기존의 VB 프로젝트와 같은)에서 새로 제작된 .NET 어셈블리에 포함된 기능들을 불러다 사용하는 것을 가능케 해준다. 이런 상황은 .NET에서 COM으로의 interop보다는 드물게 발생하겠지만 여전히 알아둘 만한 가치는 있다.

COM 응용 프로그램에서 .NET 타입을 사용하기 위해서는 어떻게든 COM 응용 프로그램이 사용하고자 하는 .NET 타입이 비관리 코드인 것처럼 속이는 방법이 필요하다. 문제의 본질은 바로 COM 아키텍처에서 사용 가능한 기능만을 이용해서 .NET 타입과 상호작용이 가능하도록 만들어줘야 한다는 것이다. 예를 들자면 COM 타입은 QueryInterface()를 호출하여 새 인터페이스를 얻을 수 있어야 하고, AddRef()Release() 호출을 통해 비관리 코드의 메모리 관리도 흉내 낼 수 있어야 하며, COMconnection point도 사용 가능해야 하는 것 등이다.

COM 클라이언트를 속이는 것 외에도 COM에서 .NET으로의 상호운용성을 위해서는 COM 런타임도 속여야 한다. COM 서버를 이용할 때는 CLR이 아닌 COM 런타임이 사용된다. COM 런타임이 COM 서버를 실행할 때는 시스템 레지스트리에서 ProgIDs, CLSIDs, IIDs 등의 여러 가지 정보를 읽어오는데, 여기서 문제는 .NET 어셈블리들은 레지스트리에 애초에 등록이 되지 않는다는 것이다.

이런 주어진 문제들을 해결하고 .NET 어셈블리를 COM 클라이언트가 사용할 수 있게 하려면 다음의 과정들을 실행해줘야 한다.

1. .NET 어셈블리를 시스템 레지스트리에 등록하여 COM 런타임이 찾을 수 있도록 한다.

2. COM 클라이언트가 public 타입과 상호작용할 수 있도록 COM 타입 라이브러리(*.tlb) 파일을 (.NET 메타데이터를 이용해) 생성한다.

3. 해당 어셈블리를 COM 클라이언트와 동일한 디렉토리에 설치(배포)하거나 (좀 더 일반적으로) GAC에 설치한다.

후에 설명하겠지만 이 모든 과정은 Visual Studio 2008을 이용해 처리하거나 .NET Framework 3.5 SDK와 함께 배포되는 여러 가지 도구를 이용해 명령 창에서 실행할 수도 있다.

 

System.Runtime.InteropServices의 어트리뷰트

앞에서 설명한 과정들을 실행하는 것 외에도 해당 C# 타입에 여러 가지 어트리뷰트를 추가해줘야 한다. 이 어트리뷰트들은 모두 System.Runtime.InteropServices 네임스페이스에 정의되어 있는 어트리뷰트들이다. 이 어트리뷰트에 따라 COM 타입 라이브러리의 내용에 무엇이 들어가는지 결정되고, 따라서 COM 응용 프로그램이 해당 관리 타입과 어느 선까지 서로 상호작용 가능한지도 이에 따라 달라진다. A4를 보면 COM 타입 라이브러리 생성에 영향을 미치는 몇 가지(전부는 아닌) 어트리뷰트에 대한 내용을 볼 수 있다.

 

A-4. System.Runtime.InteropServices의 몇 가지 속성들

 

.NET Interop 속성

실제 의미

[ClassInterface]

.NET 클래스 타입의 기본 COM 인터페이스 생성에 사용된다.

[ComClass]

이 속성은 [ClassInterface]와 비슷하나 한가지 다른 점은 타입 라이브러리의 COM 타입의 클래스 ID(CLSID)와 인터페이스 ID로 사용될 GUID를 설정 할 수 있는 기능을 제공 한다.

[DispId]

늦은 바인딩을 위해 멤버들에 부여된 DISPID 값을 코드에 명시적으로 하드코딩해 넣는데(hard-code) 사용된다.

[Guid]

COM 타입 라이브러리 내에 사용되는 GUID의 값을 코드에 명시적으로 넣는데 사용한다.

[In]

COM IDL에서 멤버 파라미터를 입력 값으로 노출 될 수 있도록 한다.

[InterfaceType]

.NET 인터페이스가 COM 쪽에 어떻게 노출 되는지를 결정 한다(IDispatch 만으로, dual, 또는 IUnknown 만으로).

[Out]

COM IDL에서 멤버 파라미터가 반환(output) 값으로 노출 될 수 있도록 한다.

 

COM에서 사용하고자 하는 .NET 타입이 아주 간단한 경우에는 앞에서 설명한 어트리뷰트들을 굳이 이용해 타입 라이브러리에 포함되는 내용을 변경할 필요는 없다. 그러나 .NET 타입이 COM에 어떻게 노출되는지에 대한 내용을 아주 구체적으로 제어하고 싶은 경우라면 COM IDL에 대해 좀 더 자세히 알아둘 필요가 있다. 왜냐하면 System.Runtime.InteropServices 네임스페이스에 정의된 어트리뷰트들은 IDL에 정의되어 있는 키워드들을 .NET으로 옮겨놓은 것뿐이기 때문이다.

 

 

CCW의 역할

.NET 타입을 COM에 노출시키는 과정을 설명하기 전에 COM 프로그램이 COM 호출 가능 래퍼 또는CCW(COM Callable Wrapper)를 사용해 어떻게 .NET 타입과 상호작용하는지 알아보자. 앞서 본 대로 .NET 프로그램이 COM 타입과 통신하기 위해서 CLR은 런타임 호출 가능 래퍼(RCW)를 생성한다. 이와 같은 이치로 COM 클라이언트가 .NET 타입을 사용하려고 할 때는 CLR이 이 둘 사이에 COM 호출 가능 래퍼(COM Callable Wrapper)를 두고, CCWCOM에서 .NET으로 흐르는 호출에 대한 내용을 담당한다(그림 A15 참조).

 

그림 A-15  COM 타입은 CCW를 이용해 .NET 타입과 통신한다.

 

여느 COM 객체와 마찬가지로 CCW 또한 참조 횟수를 갖는 개체다. 이도 충분히 말이 되는 것이 COM 클라이언트의 입장에서는 CCW도 실제 COM 타입이고, 따라서 AddRef()Release()를 호출해야 하는 COM의 법칙도 따라야 한다는 것이다. COM 클라이언트가 CCW에 마지막으로 Release()를 호출하면 CCW도 자신이 갖고 있는 .NET 타입의 참조를 해제하고 가비지 수집기도 이제 해당 타입을 해제할 수 있다.

CCW는 자신이 진정으로 COM coclass라는 환상을 더 확실히 하기 위해 몇 가지 인터페이스를 추가로 구현하고 있다. .NET 타입이 구현하는 사용자 정의 인터페이스(잠시 후 보게 될 클래스 인터페이스(class interface)라고 불리는 것을 포함하여) 외에도 CCW는 표 A5에 나와 있는 표준 COM 인터페이스들을 지원한다.

 

A-5. 핵심 COM 인터페이스를 지원하는 CCW

CCW가 구현하는 인터페이스

실제 의미

IConnectionPoint

IConnectionPointContainer

.NET 타입에서 발생 시키는 이벤트는 COMconnection point로 나타낸다.

 

IEnumVariant

.NET 타입이 IEnumerable 인터페이스를 지원하는 경우 COM 클라이언트에서는 이를 COM 열거자(Enumerator)로 보이게 해준다.

 

IErrorInfo

ISupportErrorInfo

이 인터페이스들을 이용해 coclassCOM 에러 객체들을 반환 한다.

ITypeInfo

IProvideClassInfo

이들 인터페이스들을 이용하면 COM 클라이언트가 어셈블리의 COM 타입 정보를 다루는 것처럼 인식 하게 할 수 있다. 하지만 실제로 COM 클라이언트는 .NET의 메타데이터를 다루게 된다.

 

IUnknown

IDispatch

IDispatchEx

이 핵심 COM 인터페이스들을 이용해 이른/늦은 바인딩을 한다.