본문 바로가기

출판물/C# .NET 4 Platform

[C# .NET4] 부록(5/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 테스트 클라이언트 만들기

 

 

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

기존의 COM에서 COM 클라이언트가 COM 객체와 통신을 하는 유일한 방법은 인터페이스 참조를 통해서다. 이에 반해 .NET 타입들은 이런 인터페이스들을 지원할 필요가 없기 때문에 이는 COM에서는 문제가 될 수밖에 없다. COM 클라이언트가 해당 객체의 참조를 얻어 사용할 수는 없기 때문에 CCW는 해당 타입의 public 멤버들을 포함하는 클래스 인터페이스를 노출한다. , CCWVisual Basic 6.0과 같은 방식의 접근으로 문제를 해결하고 있다는 것이다.

 

클래스 인터페이스 정의하기

.NET 타입의 클래스 인터페이스를 정의하기 위해서는 COM에 노출하고자 하는 모든 public 클래스에 [ClassInterface] 어트리뷰트를 지정해줘야 한다. 이렇게 함으로써 해당 클래스의 모든 public 멤버가 자동 생성되는 기본 인터페이스에 포함되어 노출된다. 이 인터페이스는 VB6의 명명 방식과 동일하게 명명된다(_클래스이름). 기술적으로 봤을 때 이 속성을 꼭 추가해줘야 하는 것은 아니다. 하지만 추가해주는 것이 좋다. 왜냐하면 이 속성을 넣어주지 않으면 COM 클라이언트가 .NET 타입과 통신할 수 있는 유일한 방법은 늦은 바인딩을 이용하는 방법뿐이기 때문인데, 이 방법은 타입 안전성이 떨어지고 성능 또한 느리다. [ClassInterface] 어트리뷰트는 ClassInterfaceType이라는 이름의 속성을 갖는데, 이를 통해 COM 타입 라이브러리에 해당 인터페이스가 어떻게 보여지는지를 정할 수 있다. A6에 이 속성의 가능한 값들이 나와 있다.

 

A-6. ClassInterfaceType 열거형의 값

CCW가 구현하는 인터페이스

실제 의미

AutoDispatch

자동 생성된 기본 인터페이스가 늦은 바인딩 만을 지원하도록 한다. [ClassInterface] 속성을 사용하지 않는 것과 동일 하다.

 

AutoDual

자동 생성된 기본 인터페이스가 dual interface로 초기 바인딩과 늦은 바인딩에 둘 다 사용 가능 함을 나타낸다. 이는 VB6가 기본 COM 인터페이스를 정의 하는 경우와 동일하다.

 

None

이 값은 해당 클래스에 대한 어떠한 인터페이스도 생성되지 않음을 나타낸다. 이는 자동으로 생성되는 인터페이스를 사용하지 않고 직접 정의한 .NET 인터페이스를 COM에 노출 하고자 할 때 유용하다.

 

다음 예제에서 ClassInterfaceType.AutoDual을 사용해볼 것이다. 이렇게 하면 VBScript 같은 늦은 바인딩만을 사용하는 클라이언트에서는 IDispatch를 통해 Add()Subtract() 메서드를 사용하고 초기 바인딩을 지원하는 클라이언트(VB6 또는 C++)에서는 클래스 인터페이스(_VbDotNetClac라는 이름의)를 이용한다.

 

 

.NET 타입 만들기

COM 타입이 관리 코드와 통신하는 예제를 보여주기 위해 ComCallableDotNetServer라는 이름의 C# 클래스 라이브러리를 생성하여 DotNetCalc라는 클래스를 정의했다고 가정해보자. 이 클래스에는 Add()Subtract()라는 이름의 두 메서드가 있다. 구현 자체는 매우 간단하며 [ClassInterface] 어트리뷰트를 사용한 것에 주목해보자.

 

// 필요한 interop 어트리뷰트를 불러오기 위해 필요하다.

using System.Runtime.InteropServices;

 

namespace ComCallableDotNetServer

{

  [ClassInterface(ClassInterfaceType.AutoDual)]

  public class DotNetCalc

  {

    public int Add(int x, int y)

    { return x + y; }

    public int Subtract(int x, int y)

    { return x - y; }

  }

}

 

부록 A 앞부분에서 언급한 대로 COM 세계에서는 거의 모든 것들은 GUID라고 불리는 128비트 숫자를 이용해 식별한다. 이 값들은 특정 COM 타입을 식별할 수 있도록 시스템 레지스트리에 기록된다. 앞 예제의 DotNetCalc 클래스에는 GUID 값을 정의해주지 않았기 때문에 타입 라이브러리 출력 도구에서(tlbexp.exe) 이 클래스를 불러들이면 그때마다 자동으로 GUID를 생성해 부여해준다. 이 방법의 문제는 바로 타입 라이브러리를 생성할 때마다 다른 GUID 값이 부여되고, 매번 COM 클라이언트에서도 이를 바꿔줘야 한다는 점이다.

GUID 값을 직접 지정해주기 위해서는 guidgen.exe 유틸리티를 이용하면 된다. 이 유틸리티는 Visual Studio 2008의 도구 → Guid 생성 메뉴 항목에서 실행할 수 있다. 이 도구가 지원하는 네 가지 GUID 형식 중에서 그림 A16과 같이 레지스트리 형식으로 생성을 해야 [Guid] 어트리뷰트에 사용할 수 있다.

 

그림 A-16  GUID 값 얻기

 

생성된 값을 클립보드(Copy GUID 버튼을 이용하면 된다)로 복사한 후 이를 [Guid] 어트리뷰트의 인자로 붙여넣는다. GUID 값의 양 끝 괄호를 삭제하는 것도 잊지 말자. 이를 추가한 코드는 다음과 같다(GUID 값은 이 책의 코드와 다를 수 있다).

 

[ClassInterface(ClassInterfaceType.AutoDual)]

[Guid("4137CFAB-530B-4667-ADF2-8E2CD63CB462")]

public class DotNetCalc

{

  public int Add(int x, int y)

  { return x + y; }

 

  public int Subtract(int x, int y)

  { return x - y; }

}

 

이와 관련해서 한 가지 언급하고 싶은 것은 다음과 같다. 솔루션 탐색기에서모든 파일 표시버튼을 누른 후 속성 아이콘 밑에 있는 AssemblyInfo.cs를 열어보자. 기본적으로 모든 Visual Studio 2008 프로젝트 워크스페이스는 어셈블리의 [Guid] 어트리뷰트가 추가되며, 이 값이 .NET 서버의 타입 라이브러리 GUID로 사용된다.

 

// 다음의 GUID가 이 프로젝트가 COM에서 사용될 때 typelibGUID로 사용될 값이다.

[Assembly: Guid("EB268C4F-EB36-464C-8A25-93212C00DC89")]

 

 

강력한 이름 정의하기

COM으로 노출될 모든 .NET 어셈블리에 강력한 이름을 부여하고 이를 GAC에 설치하는 것을 추천한다. 기술적으로 봤을 때 이것이 꼭 필요한 것은 아니지만 GAC에 어셈블리를 설치하지 않으면 해당 어셈블리를 COM 클라이언트와 동일한 디렉토리에 복사를 해줘야 한다.

강력한 이름에 대해서는 15장에서 이미 살펴봤으므로 여기서는 별다른 설명을 하지 않는다. 속성 편집기(Properties Editor)의 서명 탭을 이용해 *.snk 파일을 생성한다. 이제 ComCallableDotNetServer.dll을 컴파일하고 GACgacutil.exe를 이용해 GAC에 설치하면 된다.

 

gacutil -i ComCallableDotNetServer.dll

 

 

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

이제 COM 타입 라이브러리를 생성하고 .NET 어셈블리를 시스템 레지스트리에 등록할 단계다. 이를 하는 방법은 두 가지가 있다. 우선 .NET Framework 3.5 SDK와 함께 배포되는 regasm.exe라는 명령줄 도구를 이용해보자. 이 도구는 시스템 레지스트리에 몇 개의 항목을 추가하며 /tlb 플래그를 이용하면 타입 라이브러리도 생성해준다.

 

regasm ComCallableDotNetServer.dll /tlb

 

알아두기

.NET Framework 3.5 SDKtlbexp.exe 도구도 제공하는데 regasm.exe와 마찬가지로 이 도구를 이용하면 .NET 어셈블리로부터 타입 라이브러리를 생성해준다. 그러나 이와 관련된 레지스트리 값들을 시스템 레지스트리에 추가해주진 않는다. 이렇기에 그냥 regasm.exe를 사용하는 게 더 일반적이다.

 

regasm.exe를 이용하는 것이 타입 라이브러리를 생성하는 데 있어 가장 유연한 방법이긴 하나 Visual Studio 2008도 간편한 방법을 제공한다. 속성 편집기(Properties Editor) 빌드 탭의 ‘COM interop 등록옵션을 그림 A17처럼 체크한 후 어셈블리를 재컴파일하면 된다.

 

그림 A-17  Visual Studio 2008에서 COM 상호운용을 위해 등록하기

 

앞의 두 가지 방법 중 하나를 완료한 후 bin\Debug 폴더를 보면 COM 타입 라이브러리 파일(확장자가 *.tlb )이 생성되어 있을 것이다.

 

소스코드

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

 

 

생성된 타입 정보 확인하기

방금 전 생성한 COM 타입 라이브러리를 들여다볼 차례가 됐다. 이를 위해 OLE View 유틸리티에서 해당 *.tlb 파일을 불러들이면 된다. ComCallableDotNetServer.tlb 파일을 (File → View Type Library 메뉴를 통해) 불러들이면 각각의 .NET 타입에 대한 COM 타입 정보를 볼 수 있다. 예를 들어 DotNetCalc 클래스는 [ClassInterface] 어트리뷰트로 인해 _DotNetClass 인터페이스를 지원하도록 정의되어 있다. 여기에 추가로 (놀라지 마시라!) _Object 인터페이스도 포함되어 있다. 짐작대로 이는 System.Object가 정의하는 기능들이다.

 

[uuid(88737214-2E55-4D1B-A354-7A538BD9AB2D),

 version(1.0), custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},

 "ComCallableDotNetServer.DotNetCalc")]

coclass DotNetCalc {

  [default] interface _DotNetCalc;

  interface _Object;

}

 

[ClassInterface] 어트리뷰트에 지정한 대로 기본 인터페이스는 dual interface로 생성되어 있다. 따라서 초기 바인딩과 늦은 바인딩 둘 다 사용 가능하다.

 

[odl, uuid(AC807681-8C59-39A2-AD49-3072994C1EB1), hidden,

 dual, nonextensible, oleautomation,

 custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},

 "ComCallableDotNetServer.DotNetCalc")]

interface _DotNetCalc : IDispatch {

  [id(00000000), propget,

   custom({54FC8F55-38DE-4703-9C4E-250351302B1C}, "1")]

  HRESULT ToString([out, retval] BSTR* pRetVal);

  [id(0x60020001)]

  HRESULT Equals( [in] VARIANT obj,[out, retval] VARIANT_BOOL* pRetVal);

  [id(0x60020002)]

  HRESULT GetHashCode([out, retval] long* pRetVal);

  [id(0x60020003)]

  HRESULT GetType([out, retval] _Type** pRetVal);

  [id(0x60020004)]

  HRESULT Add([in] long x, [in] long y,

    [out, retval] long* pRetVal);

  [id(0x60020005)]

  HRESULT Subtract( [in] long x, [in] long y,[out, retval] long* pRetVal);

}

 

_DotNetCalc 인터페이스는 Add()Subtract() 메서드뿐만 아니라 System.Object에서 상속받은 멤버들에 대한 정보까지도 담고 있다는 것을 주목하자. 이는 하나의 규칙으로 .NET 클래스를 COM에 사용 가능하도록 노출할 때는 상속받는 모든 public 메서드도 자동 생성된 인터페이스에 포함된다.

 

 

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

.NET 어셈블리가 완성됐으니 COM 클라이언트를 만들어볼 차례다. VB6에서 표준 .exe 프로젝트를 생성한다(이름은 VB6DotNetClient). 그리고 방금 새로 생성한 타입 라이브러리에 대한 참조를 추가한다(그림 A18 참조).

 

그림 A-18  VB6에서 .NET 서버 참조

 

우선 프로그램의 UI는 간단하게 가자. 하나의 버튼을 이용해 DotNetCalc .NET 타입을 제어한다. 코드는 다음과 같다(_Object 인터페이스의 ToString()을 호출하는 점을 눈여겨보자).

 

Private Sub btnUseDotNetObject_Click()

  ' .NET 객체 생성

  Dim c As New DotNetCalc

  MsgBox c.Add(10, 10), , "Adding with .NET"

 

  ' System.Object 멤버들 호출

  MsgBox c.ToString, , "ToString value"

End Sub

 

소스코드

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

 

여기까지 COM 타입과 통신하는 .NET 응용 프로그램과 COM 응용 프로그램에서 .NET 타입과 통신하는 과정을 알아봤다. 다시 말하지만 상호운용 서비스에는 여기서 언급한 내용 외에도 알아볼 만한 주제들이 많다. 하지만 이 정도만 알아둬도 여러분 스스로 추가적인 내용을 공부하기에 충분하리라 생각된다.

 

 

 

정리

.NET은 멋진 기술이다. 하지만 당분간은 관리 코드와 비관리 코드는 서로 공존해야 하는 상황이다. 이를 위해 .NET 플랫폼은 서로 다른 이 두 세계가 잘 섞이도록 여러 가지 기술을 제공한다. 부록 A에서는 기존 레거시 COM을 이용해 .NET 타입을 다루는 내용을 중점으로 다뤘다. 이 과정은 COM 타입에 대한 어셈블리 프록시를 생성하는 것으로부터 시작한다. RCW.NET 응용 프로그램에서 실제 호출하고자 하는 COM 바이너리를 대신 호출해주고 COM 타입을 그에 대응되는 .NET 타입과 매치시켜준다.

다음으로 COM 타입에서 .NET 타입을 호출하는 과정을 알아봤다. 이 과정을 위해서는 .NET 타입을 COM에서 사용할 수 있도록 등록시켜줘야 하고 .NET 타입에 대한 COM 타입 라이브러리를 생성해줘야 한다.