본문 바로가기

출판물/C# .NET 4 Platform

[C# .NET4] 부록(3/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 어셈블리 만들기

VB6는 이 외에도 여러 가지 IDL 어트리뷰트를 생성하지만 그에 대한 내용은 차차 설명하기로 한다. 하지만 이 시점에서 필자가 왜 이 COM IDL에 대해 이렇게 몇 페이지씩이나 할애하며 설명하는지 궁금해할 것이다. 그 이유는 간단하다. Visual Studio 2008에서 COM 서버의 참조를 추가하면 IDE는 타입 라이브러리를 읽어 해당 interop 어셈블리를 생성한다. Visual Studio 2008interop 어셈블리를 잘 생성해주긴 하나 참조 추가 대화상자는 기본으로 정해진 규칙대로 interop 어셈블리를 생성할 뿐 개발자가 이 과정을 세밀하게 조정할 수 있게 해주진 않는다.interop 어셈블리 생성 과정을 좀 더 세밀하게 조정해야 한다면 명령 프롬프트에서 tlbimp.exe라는 .NET 유틸리티(타입 라이브러리 임포트 유틸리티)를 이용해 interop 어셈블리를 생성하면 된다. 이 유틸리티의 여러 가지 기능 중 하나로 타입을 포함하는 .NET 네임스페이스와 출력 파일의 이름을 지정할 수 있도록 하는 기능이 있다. 그리고 GAC에 설치하여 사용하기 위해 강력한 이름을 부여하고자 한다면 tlbimp.exe/keyfile을 이용해 *.snk 파일을 지정할 수 있다(강력한 이름에 대한 자세한 내용은 15장을 참조하라). 기타 옵션들을 확인하고 싶으면 Visual Studio 2008 명령 프롬프트에서 tlbimp를 입력하고 엔터 키를 누른다(그림 A12 참조).

 

그림 A-12  tlbimp.exe의 옵션들

 

제공되는 여러 가지 옵션 중에서 다음의 명령을 이용해 CalcInteropAsm.dll을 생성할 수 있다(myKeyPair.snk라는 파일이 이미 생성됐다는 가정하에).

 

tlbimp SimpleComServer.dll /keyfile:myKeyPair.snk /out:CalcInteropAsm.dll

 

Visual Studio 2008이 기본으로 생성하는 interop 어셈블리로도 충분하다면 굳이 tlbimp.exe를 이용해 어셈블리를 생성할 필요는 없다.

 

CoCalc coclass로의 늦은 바인딩

우선 interop 어셈블리 생성에 성공했으면 .NET 응용 프로그램에서 초기 바인딩이나 늦은 바인딩 방법을 이용해 해당 COM 타입을 사용할 수 있다. 부록 A의 앞부분에서 (C#new 키워드를 이용하여) 초기 바인딩을 이용해 COM 타입을 사용하는 방법은 이미 봤으니 여기서는 늦은 바인딩을 이용한 방법을 알아보기로 한다.

16장에서 이미 살펴봤듯이 System.Reflection 네임스페이스를 이용하면 주어진 어셈블리 안에 포함된 타입들을 런타임 시에 프로그램 코드로 확인해볼 수 있다. COM에서는 이와 동일한 기능을 몇 가지 표준 인터페이스(예를 들어 ITypeLib, ITypeInfo )를 통해 제공한다. 클라이언트가 런타임 시(컴파일 때가 아닌) 특정 멤버에 바인딩을 하면 이것을 클라이언트가늦은바인딩을 한다고 칭한다.

대체적으로는 어떤 경우에라도 C#new 키워드를 이용해 초기 바인딩 방법을 사용하는 것이 좋다. 하지만 때로는 늦은 바인딩을 이용해 coclass를 사용해야 할 상황이 발생할 수 있다. 하나의 예로 어떤 기존 레거시 COM 서버는 타입 정보가 전혀 없이 만들어진 경우가 있을 수 있다. 이런 경우라면tlbimp.exe를 아예 이용할 수 없다. 이런 드문 경우에는 .NET 리플렉션 서비스를 이용하면 기존 COM 타입에 접근할 수 있다.

늦은 바인딩의 과정은 클라이언트가 주어진 coclass로부터 IDispatch 인터페이스를 얻는 것으로부터 시작된다. IDispatch 표준 COM 인터페이스는 총 네 개의 메서드를 정의하고 있는데, 지금으로서는 이 중 두 개에 대해서만 알아두면 된다. 첫 번째 메서드로 GetIDsOfNames()가 있다. 이 메서드를 이용하면 호출하고자 하는 메서드를 식별할 수 있는 숫자 값(dispatch ID 또는 DISPID라고 부르는)을 얻어 늦은 바인딩에 사용할 수 있게 해준다.COM IDL에는 [id] 어트리뷰트를 이용해 멤버의 DISPID를 지정한다. VB6가 생성한 IDL 코드를 보면(OLE View 유틸리티를 이용하면 된다) Add() 메서드의 DISPID가 다음처럼 지정되어 있는 것을 볼 수 있다.

 

[id(0x60030000)] HRESULT Add( [in] short x, [in] short y, [out, retval] short* );

 

바로 이 값이 GetIDsOfNames()가 늦은 바인딩을 하려는 클라이언트에게 반환하는 값이다. 이 값을 얻은 클라이언트는 이 값으로 Invoke()라는 함수를 호출한다. 이 메서드는 몇 가지 인자를 받는데 이 중 하나가 바로 GetIDsOfNames()를 통해 얻은 DISPID이다. 여기에 추가로 COM VARIANT 타입의 배열을 인자로 받는데, 이는 호출하고자 하는 함수에 넘겨줄 매개변수들이다. Add() 메서드의 경우 이 배열은 두 개의 short 값을 갖는다. Invoke()는 마지막 인자로 또 하나의 VARIANT 타입 값을 받는데, 이는 메서드의 반환 값을 담는 용도로 사용된다.

비록 늦은 바인딩을 사용하는 .NET 클라이언트가 IDispatch 인터페이스를 직접적으로 사용하진 않지만 System.Reflection 네임스페이스를 이용해 비슷한 기능을 쓸 수 있다. 다음의 C# 클라이언트는 이를 보여주는 예제로 늦은 바인딩을 이용해 Add()를 호출하고 있다. 이 응용 프로그램은 해당 어셈블리에 대한 참조를 전혀 사용하지 않는다는 데 주목하자. 따라서 tlbimp.exe 유틸리티 또한 사용하지 않아도 된다.

 

// System.Reflection 네임스페이스를 사용하는 것을 잊지 말자.

static void Main(string[] args)

{

  Console.WriteLine("***** The Late Bound .NET Client *****");

 

  // 우선 coclass로부터 IDispatch 참조를 얻는다.

  Type calcObj =

    Type.GetTypeFromProgID("SimpleCOMServer.ComCalc");

  object calcDisp = Activator.CreateInstance(calcObj);

 

  // 인자를 담을 배열을 만든다.

  object[] addArgs = { 100, 24 };

 

  // Add() 메서드를 호출해 합계를 얻는다.

  object sum = null;

  sum = calcObj.InvokeMember("Add", BindingFlags.InvokeMethod,

    null, calcDisp, addArgs);

 

  // 결과를 출력한다.

  Console.WriteLine("Late bound adding: 100 + 24 is: {0}", sum);

  Console.ReadLine();

}

 

소스코드

CSharpComLateBinding 응용 프로그램은 Appendix A 서브디렉토리에 있다.

 

 

좀 더 복잡한 COM 서버 만들기

초급 산수는 이제 그만하고 VB6를 이용해 좀 더 복잡한 COM 프로그래밍 기술을 이용한 ActiveX 서버를 만들어보자. VB6에서 새로운 ActiveX *.dll 워크스페이스를 생성하고 이름은 Vb6ComServer라고 짓자. 초기 클래스 파일의 이름을 CoCar로 변경하고 다음의 코드를 넣는다.

 

Option Explicit

 

' COM 열거자

Enum CarType

  Viper

  Colt

  BMW

End Enum

 

' COM 이벤트

Public Event BlewUp()

 

' 멤버 변수

Private currSp As Integer

Private maxSp As Integer

Private Make As CarType

 

' 주목! 모든 public 멤버는

' 기본 인터페이스에 노출된다!

Public Property Get CurrentSpeed() As Integer

  CurrentSpeed = currSp

End Property

 

Public Property Get CarMake() As CarType

  CarMake = Make

End Property

 

Public Sub SpeedUp()

  currSp = currSp + 10

  If currSp >= maxSp Then

    RaiseEvent BlewUp ' 엔진 출력이 최대를 넘으면 이벤트 발생

  End If

End Sub

 

Private Sub Class_Initialize()

  MsgBox "Init COM car"

End Sub

 

Public Sub Create(ByVal max As Integer, _

  ByVal cur As Integer, ByVal t As CarType)

  maxSp = max

  currSp = cur

  Make = t

End Sub

 

위에서 볼 수 있듯이 이 간단한 COM 클래스는 이 책의 앞에서 많이 본 C# Car 클래스를 구현해놓은 것이다. 여기서 주목해야 할 점은 Car 객체를 나타내는 상태 데이터를 받을 수 있도록 되어 있는 Create() 함수다.

 

 

추가 COM 인터페이스에 대한 지원 추가하기

하나의 (기본) 인터페이스를 지원하는 COM 클래스를 지원하는 방법은 배웠으니 다음과 같은 IDriver- Info 인터페이스를 지원하는 새 *.cls 파일을 추가한다.

 

Option Explicit

'드라이버는 이름을 갖는다.

Public Property Let DriverName(ByVal s As String)

End Property

Public Property Get DriverName() As String

End Property

 

여러 개의 인터페이스를 지원하는 COM 객체를 만들 때 VB6에서는 이를 위해 Implements 키워드를 제공한다. 해당 COM 클래스가 구현하는 인터페이스를 지정하면 VB6의 코드 창을 이용해 메서드의 기본 틀을 만들 수 있다. 우선 CoCar 클래스 타입에 String 타입의 변수(driverName)를 추가하고 IDriverInfo 인터페이스의 메서드를 다음처럼 구현했다고 가정해보자.

 

' 구현하는 인터페이스

' [General][Declarations]

Implements IDriverInfo

...

'***** IDriverInfo impl ***** '

Private Property Let IDriverInfo_DriverName(ByVal RHS As String)

driverName = RHS

End Property

Private Property Get IDriverInfo_DriverName() As String

IDriverInfo_driverName = driverName

End Property

 

이제 인터페이스 구현 마무리를 위해 IDriverInfoInstancing 속성을 PublicNotCreatable로 지정한다(외부에서 인터페이스 타입을 할당하지 못하도록 한다는 가정하에).

 

 

내부 객체 노출

VB6에서는(COM 또한) 기존의 구현 상속이라는 것이 불가능하다. 대신 포함/위임 모델(바로가지다(has-a)’ 관계)을 사용할 수밖에 없다. 우선 테스트를 해보기 위해 현재 VB6 프로젝트에 마지막 *.cls 파일을 Engine이란 이름으로 추가하고 이의 instancing 속성을 PublicNotCreatable로 지정한다(외부에서 이 객체를 직접 생성하지 못하도록 하기 위한 것이다).

Engine의 기본 인터페이스는 매우 짧고 간결하다. 엔진(Engine)의 각 실린더의 별명을 담는 문자열 배열을 외부로 반환하는 함수 하나를 정의한다(자기 차 엔진의 실린더에 별명을 지어놓는 사람이 몇이나 되겠습니까마는, 알 수 없는 일이죠).

 

Option Explicit

Public Function GetCylinders() As String()

Dimc(3) As String

c(0) = "Grimey"

c(1) = "Thumper"

c(2) = "Oily"

c(3) = "Crusher"

GetCylinders = c

End Function

 

마지막으로 CoCar의 기본 인터페이스에 GetEngine()이라는 메서드를 추가한다. 이 메서드는 CoCar가 포함하는 Engine 객체의 인스턴스를 반환하도록 한다(이를 위해 Engine 타입의 Private 멤버 변수를 추가한다).

 

' Engine을 외부에 반환한다..

Public Function GetEngine() As Engine

SetGetEngine = eng

End Function

 

이로써 하나의 ActiveX 서버에 두 개의 인터페이스를 지원하는 COM 클래스를 구현해봤다. 또한 CoCar[default] 인터페이스를 이용해 내부 COM 타입을 반환하는 것도 해봤고, 열거와 COM 배열과 같은 몇 가지 프로그래밍 개념을 사용해보기도 했다. 이제 컴파일한 후 VB6 워크스페이스를 닫는다(컴파일 완료 후엔 binary compatibility 옵션을 켜자).

 

소스코드

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