부록 A - COM과 .NET 상호운용성
- 목차
.NET에서 COM Interop을 사용하는 간단한 예제
타입 라이브러리를 이용해 Interop 어셈블리 만들기
타입 라이브러리를 이용해 Interop 어셈블리 만들기
VB6는 이 외에도 여러 가지 IDL 어트리뷰트를 생성하지만 그에 대한 내용은 차차 설명하기로 한다. 하지만 이 시점에서 필자가 왜 이 COM IDL에 대해 이렇게 몇 페이지씩이나 할애하며 설명하는지 궁금해할 것이다. 그 이유는 간단하다. Visual Studio 2008에서 COM 서버의 참조를 추가하면 IDE는 타입 라이브러리를 읽어 해당 interop 어셈블리를 생성한다. Visual Studio 2008이 interop 어셈블리를 잘 생성해주긴 하나 참조 추가 대화상자는 기본으로 정해진 규칙대로 interop 어셈블리를 생성할 뿐 개발자가 이 과정을 세밀하게 조정할 수 있게 해주진 않는다.interop 어셈블리 생성 과정을 좀 더 세밀하게 조정해야 한다면 명령 프롬프트에서 tlbimp.exe라는 .NET 유틸리티(타입 라이브러리 임포트 유틸리티)를 이용해 interop 어셈블리를 생성하면 된다. 이 유틸리티의 여러 가지 기능 중 하나로 타입을 포함하는 .NET 네임스페이스와 출력 파일의 이름을 지정할 수 있도록 하는 기능이 있다. 그리고 GAC에 설치하여 사용하기 위해 강력한 이름을 부여하고자 한다면 tlbimp.exe의 /keyfile을 이용해 *.snk 파일을 지정할 수 있다(강력한 이름에 대한 자세한 내용은 15장을 참조하라). 기타 옵션들을 확인하고 싶으면 Visual Studio 2008 명령 프롬프트에서 tlbimp를 입력하고 엔터 키를 누른다(그림 A‒12 참조).
그림 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 서브디렉토리에 있다.
초급 산수는 이제 그만하고 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
이제 인터페이스 구현 마무리를 위해 IDriverInfo의 Instancing 속성을 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 서브디렉토리에 있다.
'출판물 > C# .NET 4 Platform' 카테고리의 다른 글
[C# .NET4] 부록(5/5): COM과 .NET 상호운용성 (0) | 2013.05.24 |
---|---|
[C# .NET4] 부록(4/5): COM과 .NET 상호운용성 (0) | 2013.05.24 |
[C# .NET4] 부록(2/5): COM과 .NET 상호운용성 (0) | 2013.05.24 |
[C# .NET4] 부록(1/5): COM과 .NET 상호운용성 (0) | 2013.05.24 |
C# and the .NET 4 Platform 한국어판 관련 (2) | 2013.05.07 |