본문 바로가기

출판물/C# .NET 4 Platform

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

이 문서에 대해


원래 이 문서는 C# and .NET 3.5 Platform 한국어판에 부록으로 포함된 내용입니다. 하지만 내부적인 사정 때문에 C# and .NET 3.5 Platform 한국어판(원서로는 4판에 해당합니다)의 출판이 중지되었고, 그 대신 이번(이 글은 2013 5월에 쓰여지고 있습니다)에 
C# and .NET 4 Platform 한국어판(원서로는 5판에 해당합니다)이 새로 출판되었습니다. 이 `COM과 .NET 상호운용성`은 4판에는 있었지만 5판에서는 삭제된 내용으로, COM과 .NET 상호운용성에 관련된 내용은 사실 아주 어려운 내용은 아니지만 다루는 책을 찾아보기가 어려운 편입니다. .NET으로 COM을 다뤄야 하는 상황이 요즘에는 거의 없다고 해도 괜찮을 정도이고, MS의 런타임 프레임워크조차 WinRT 등으로 발 빠르게 옮겨가고 있는 와중이라 이 내용 자체가 뒷북일 수도 있겠습니다만, 분명 누군가에게는 필요할 내용이라 판단되어 이렇게 별도로 블로그를 통해서라도 책의 내용을 공개할 수 있으면 좋겠다고 생각했습니다. 그래서 이 책의 출판에 관련된 권리를 갖고 있는 지앤선 측에 이 문서의 내용을 제 블로그(혹은 그 외의 곳이라도)에 게재하면 좋을 것 같다는 의견을 드렸습니다. 그리고, 흔쾌히 허락해 주셨습니다. (대신 책이 출간된 이후에 게재해 달라는 조건을 달으셨지만요) 이 지면을 빌어 게재를 허락해 주신 지앤선 관계자 분들께 심심한 감사를 드립니다. (_ _)

 

지금 시점에서는 비즈니스 관점에서 봤을 적에 마이크로소프트의 개발 환경이 거의 모두 .NET으로 이전해 간 상태라고 봐도 될 것 같습니다. 대표적으로 VSTO를 예로 들자면 이제 COM API 쪽은 존재는 하지만 개발자 눈에는 잘 띄지 않는 정도까지 왔다고 할까하지만 (프로그래머라면 공감하시겠지만) 20세기에 머물러 있는 레거시 환경은 여전히 팔팔하게 살아 있습니다. 제 개인적으로는 최근 Quickbook이라는 프로그램의 간단한 서드파티 플러그인을 .NET으로 만드는 작업을 한 적이 있었는데, 이 프로그램의 COM API를 다루면서 삽질을 하다보니왜 내가 2012년에 이런 일을 하고 있어야 하는가라는 생각까지 들더라구요.

어쨌거나, 윈도우가 존재하는 한 아마 COM API가 없어지는 일은 없을 겁니다. 은행권의 메인프레임에서 COBOL이 몇십 년간 살아남은 것과 마찬가지로요. 프로그래머가 신기술을 익히기 위해 필요한 자료를 만들어 내는 것도 중요한 일이지만, 레거시 환경과의 가교 역할을 하는 이러한 기술 주제에 대한 내용 또한 존재하는 것만으로도 가치가 있는 일이 아닐까 싶어서, 이렇게 정리하여 글을 올리게 되었습니다.

그리고 하나 더 언급해 두자면, 저는 기본적으로 제 블로그의 글을 전재하는 것에 대해 까다롭게 구는 편은 아닙니다만(공식적으로는 스크랩을 거부합니다만, 굳이 쫓아가서 시비를 걸진 않습니다) 이 책만큼은 예외로, 이 문서의 내용이 출판사의 저작권과 직접적으로 연관이 있는 만큼 문서의 내용을 무단으로 스크랩하거나 다른 곳에 활용할 경우 저작권법의 뜨거운 맛을 보실 수도 있음을 미리 알려드립니다. 공개적인 장소에 업로드하거나 하는 일은 피해주세요. 학교 숙제나 레포트에 베껴넣지도 말아주세요.

소개하는 글이 길어졌네요. 필요하신 곳에 잘 활용하시길 바랍니다! :)

 

-       다음 책에서 발췌: Pro C# 2008 and the .NET 3.5 Platform, Andrew Troelsen

-       번역 및 정리: 이수겸 (keniallee@gmail.com)

 

 

 

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

 

- 목차

이 문서에 대해

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

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

.NET Interop 어셈블리 살펴보기

런타임 호출 가능 래퍼

COM IDL의 역할

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

좀 더 복잡한 COM 서버 만들기

Interop 어셈블리에 대하여

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

CCW의 역할

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

.NET 타입 만들기

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

생성된 타입 정보 확인하기

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

 

 

이 책의 목적은 C#.NET 플랫폼이 제공하는 핵심 서비스에 대한 튼튼한 기초를 제공하는 것이다. 필자가 생각하건대 .NET의 현재 객체 모델과 마이크로소프트의 기존 객체 모델(COM)을 독자들이 비교해봤을 때 이 둘이 전혀 다른 시스템이라는 사실을 확신할 것이라는 데는 의심의 여지가 없다. COM이 이제는 한물간 레거시 프레임워크라고는 하지만 새로운 .NET 응용 프로그램과 통합하여 사용하고자 하는 기존의 COM 기반 시스템은 계속 존재할 것이다.

고맙게도 .NET 플랫폼은 COM.NET의 상호운용이 비교적 쉽게 이뤄질 수 있도록 여러 가지의 타입, 유틸리티와 네임스페이스를 제공한다. 부록 A의 전반부에는 .NET에서 COM으로의 상호운용성과 이와 관련된 런타임 시 호출 가능한 래퍼(RCW: Runtime Callable Wrapper)에 대해 알아본다. 후반부에는 반대의 경우인 COM 타입이 .NET 타입과의 통신을 위해 사용하는 COM 호출 가능한 래퍼(CCW: COM Callable Wrapper)에 대해 알아본다.

알아두기

.NET 상호운용성에 대한 전반적인 내용을 다루려면 그 자체만으로도 책 한 권이 필요할 정도이다. 부록 A에서 다루지 않는 좀 더 상세한 내용을 원하는 독자는 필자의 『COM and .NET Interoperability(Apress, 2002)를 참고하기 바란다.

 

 

 

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

.NET에서 사용 가능한 컴파일러로 어셈블리를 빌드하면 공용 언어 런타임(CLR)에 의해 실행 가능한 관리 코드가 생성된다고 배웠다. 관리 코드는 자동 메모리 관리, 공형 타입 시스템(CTS), 자기 자신을 기술하는 메타데이터를 갖는 등의 몇 가지 이점을 제공한다. 여러분이 또한 봐왔듯이 .NET의 어셈블리는 내부 구성이 독특하다. 어셈블리는 CIL 명령어와 타입 메타데이터 외에도 실행 시 필요한 외부 어셈블리에 대한 모든 정보를 담고 있는 매니페스트는 물론이고 다른 파일 관련 내용(강력한 이름, 버전 값 등) 또한 담고 있다.

다른 한편으로는 기존의 레거시 COM 서버(당연히 비관리 코드다)가 있다. 이 바이너리들은 공통의 파일 확장자(*.dll이나 *.exe)를 갖는다는 점 외에는 .NET 어셈블리와는 별 관계가 없다. 우선 COM 서버는 플랫폼에 관용적인 CIL 코드가 아닌 플랫폼과 밀접한 기계어를 담고 있으며 oleautomation이나 variant-compliant 같은 고유의 데이터 타입들로만 작업을 해야 한다.모든 COM 바이너리에 COM만의 코드 기반 구조(‘COM infrastructure’를 번역한 말인데 이는 모든 COM 객체가 구현해줘야 하는 IUnknown 인터페이스, DllRegisterServer() 등의 것들을 말한다. – 옮긴이)를 구현해줘야 한다는 점(레지스트리에 추가해줘야 하는 항목들과 IUnknown 같은 주요 COM 인터페이스에 대한 구현) 외에도 COM 타입은 COM 객체의 수명 관리를 위해 참조 카운트(횟수)를 관리해줘야 한다. 이는 관리 힙에 생성되고 CLR 가비지 수집기에 의해 알아서 처리되는 .NET 객체와 현저한 차이를 보인다.

.NET 타입과 COM 타입이 이렇게 다른데 이 두 아키텍처가 어떻게 서로 간의 서비스를 활용할 수 있다는 것인가? 여러분이 ‘100% 순수 .NET’ 개발에만 매진할 수 있는 회사에서 일하는 것이 아니라면 기존의 레거시 COM 타입을 이용하는 .NET 솔루션을 만들 일이 왠만하면 있을 것이다. 또 어떤 경우엔 기존의 레거시 COM 서버가 방금 새로 만든 새 .NET 어셈블리에 담겨 있는 타입과 통신해야 하는 경우도 있을 것이다.

이를 종합해 다시 말해보면 앞으로 당분간은 COM.NET이 서로 친하게 지낼 수밖에 없다는 것이다. 부록 A는 관리 타입과 비관리 타입이 서로 조화를 이루면서 함께 사용될 수 있도록 .NET 상호운용성 계층을 사용하는 과정을 설명하고자 한다. 간단히 봤을 때 .NET 프레임워크는 상호운용성에 있어 다음과 같은 두 가지 주요 방법을 제공한다.

∙ COM 타입을 사용하는 .NET 응용 프로그램

∙ .NET 타입을 사용하는 COM 응용 프로그램

 

부록 A를 보면 알 수 있듯이 .NET Framework 3.5 SDKVisual Studio 2008은 이 두 독특한 구조 간의 거리를 좁힐 수 있도록 여러 가지 유틸리티를 제공한다. .NET 기본 클래스 라이브러리 또한 System.Runtime.InteropServices라는 상호운용을 위한 전용 네임스페이스를 제공한다. 더 자세히 파고들기 전에 .NET 클래스가 COM 서버와 통신을 하는 아주 간단한 예제부터 한 번 만들어보자.

알아두기

.NET 플랫폼은 .NET 어셈블리에서 OS의 기반 API(C로 만들어진 비관리 *.dll까지도) 또한 쉽게 호출해 사용할 수 있도록 PInvoke(platform invocation)라는 기술을 제공한다. C#의 관점에서 봤을 때 PInvoke를 사용하기 위해 개발자가 해줘야 하는 일은 호출하고지 하는 외부 메서드에 [DllImport] 어트리뷰트를 추가해주기만 하면 된다. 더 자세한 내용을 원하는 독자는 .NET Framework 3.5 SDK의 도움말에서 [DllImport] 항목을 참고하면 된다.

 

 

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

상호운용성 서비스에 대한 탐험을 시작하기에 앞서 이것이 겉으로 보기에는 얼마나 간단한지 직접 체험해보기로 하자. 이 절의 목표는 C# 응용 프로그램에서 사용될 Visual Basic 6.0 ActiveX *.dll 서버를 만드는 것이다.

알아두기

VB6 외에도 ATL(Active Template Library)MFC(Microsoft Foundation Classes) 같은 COM 프레임워크들도 있다. 부록 A에서는 COM 응용 프로그램을 제작하기에 가장 편한 문법을 제공하는 VB6를 이용하기로 했다. ATL/MFC를 사용하고 싶은 독자는 이를 이용해도 된다.

 

VB6를 실행한 후 SimpleComServer라는 이름의 새 ActiveX *.dll 프로젝트를 생성한다. 초기 클래스 파일은 ComCalc.cls로 변경하고 클래스의 이름 또한 ComCalc로 변경한다. 알다시피 프로젝트의 이름과 프로젝트 내에 포함되는 클래스명을 이용해 COM 타입의 프로그램 식별자(ProgID)를 생성하게 된다(이 경우엔 SimpleComServer.ComCalc). 마지막으로 ComCalc.cls에 다음과 같은 메서드 정의를 추가한다.

 

'The VB6COM object

Option Explicit

Public Function Add(ByVal x As Integer, ByVal y As Integer) As Integer

  Add=x+y

End Function

 

Public Function Subtract(ByVal x As Integer, ByVal y As Integer) As Integer

  Subtract = x - y

End Function

 

이제 *.dll(File → Make 메뉴 항목을 통해) 컴파일한다. COM 세상의 모든 것이 문제없이 작동할 수 있도록 VB6 IDE를 종료하기 전에 프로젝트 속성 탭의 Component 탭의 binary compatibility 옵션을 켠다. 이렇게 하면 프로젝트를 다시 빌드하는 경우에도 지정된 GUID(globally unique identifier)가 유지된다.

소스코드

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

 

 

C# 클라이언트 만들기

이제 Visual Studio 2008을 열고 CSharpComClient라는 이름으로 새 C# 콘솔 응용 프로그램을 생성한다. 기존의 레거시 COM 서버와 통신을 해야 하는 .NET 응용 프로그램을 만들 때 가장 먼저 해야 할 일은 바로 프로젝트에 COM 서버에 대한 참조를 추가하는 것이다(.NET 어셈블리의 참조를 추가하는 것과 비슷하다).이를 위해서는 프로젝트참조 추가 메뉴를 선택하면 나오는 참조 추가 대화상자의 COM 탭을 연다. COM 서버는 알파벳순으로 정렬되어 나오는데 방금 VB6에서 빌드한 COM 서버도 VB6 컴파일러가 자동으로 추가해 놓았다. 그림 A1에서처럼 SimpleComServer.dll을 선택하고 대화상자를 닫는다.

 

그림 A-1  Visual Studio 2008에서 COM 서버 참조하기

 

이제 솔루션 탐색기의 참조 폴더를 보면 그림 A-2에서처럼 새 .NET 어셈블리로 보이는 항목의 참조가 추가된 것을 볼 수 있을 것이다. COM 서버를 참조하면 생성되는 어셈블리는 정식으로 interop 어셈블리라고 한다. 이에 대한 내용은 나중에 좀 더 자세히 보기로 하고, 일단 interop 어셈블리는 .NET에서 사용하는 COM 타입들에 대한 정보를 담고 있다고만 알아두자.

그림 A-2  참조하는 interop 어셈블리

 

새로 생성한 C# 클래스 타입에 아무런 코드를 추가하지 않았지만 이 상태에서 응용 프로그램을 빌드해보자. 그리고 프로젝트의 bin\Debug 디렉토리를 보면 interop 어셈블리가 생성되어 응용 프로그램 디렉토리에 추가된 것을 볼 수 있다(그림 A3 참조). 생성된 interop 어셈블리에 Interop.이라는 접두어가 붙어 있는데 이는 Visual Studio 2008이 자동으로 붙이는 것으로, 하나의 관행일 뿐 CLR이 요구하는 필수사항은 아니다.

 

그림 A-3  자동 생성된 interop 어셈블리

 

이 예제를 완성하기 위해 기본 클래스의 Main() 메서드에 ComCalc 객체의 Add() 메서드를 호출하고 그 결과 값을 출력하도록 수정하자. 예를 들면 다음과 같다.

 

using System;

using SimpleComServer;

 

namespace CSharpComClient

{

  class Program

  {

    static void Main(string[] args)

    {

      Console.WriteLine("***** The .NET COM Client App *****");

      ComCalc comObj = new ComCalc();

      Console.WriteLine("COM server says 10 + 832 is {0}",

        comObj.Add(10, 832));

      Console.ReadLine();

    }

  }

}

 

위의 예제 코드에서 볼 수 있듯이 ComCalc COM 객체를 포함하는 네임스페이스는 원래 VB6 프로젝트의 이름과 동일하다(using 구문을 주목). 출력 내용은 예상대로 그림 A4와 같다.

 

그림 A-4  짜잔! .NET에서 COM을 호출

 

방금 본 예제에서 확인할 수 있듯이 .NET 응용 프로그램에서 COM 타입을 가져다 사용하는 일은 매우 쉽다. 이미 짐작하고 있겠지만 이 이면에는 이러한 통신이 가능하도록 내부적으로 몇 가지 작업이 이뤄지고 있다. 바로 이 작업들이 부록 A에서 다루는 내용으로, interop 어셈블리부터 자세히 들여다보자.