본문 바로가기

Dev/.NET

RCW, CCW

출처 : http://www.microsoft.com/Korea/MSDN/MSDNMAG/ISSUES/2001/Interop/default.aspx


.NET에서 COM 개체 사용

새로눈 .NET 코드는 기존 COM 코드와 상호 운용되어야 하기 때문에 이 경우에 대해 먼저 설명하겠습니다. .NET 클라이언트는 그림?1과 같이 런터임 호출 가능 래퍼(RCW)를 통해 COM 서버에 액세스합니다. RCW는 COM 개체를 래핑하고 이 개체와 .NET 공통 언어 런타임(CLR) 환경을 서로 연결해 주는 역할을 합니다. 이로 인해 .NET 클라이언트 측면에서는 COM 개체를 기본 .NET 개체인 것처럼 인식하며, COM 개체 측면에서는 .NET 클라이언트를 표준 COM 클라이언트인 것처럼 인식합니다.

그림 1 RCW를 통한 클라이언트 액세스

그림 1 RCW를 통한 클라이언트 액세스

.NET 클라이언트 개발자는 두 가지 방법 중 하나를 사용하여 RCW를 만듭니다. Visual Studio .NET을 사용하는 경우에는 프로젝트의 참조 섹션을 마우스 오른쪽 단추로 클릭한 다음 바로 가기 메뉴에서 참조 추가를 선택하면 됩니다. 그러면 시스템에 등록된 모든 COM 형식 라이브러리를 선택할 수 있는 대화 상자가 나타나며, 등록하지 않은 형식 라이브러리를 찾아서 선택할 수도 있습니다(그림?2 참조). 그런 다음 RCW를 만들려는 COM 개체를 선택하면 Visual Studio .NET에서 RCW를 만들어 프로젝트에 추가해 줍니다.


그림 2 RCW 생성

그림 2 RCW 생성

Visual Studio .NET을 사용하지 않는 경우 .NET SDK에는 명령줄 도구(동일한 작업을 수행하는 도구로 TlbImp.exe라고도 함)가 포함됩니다. 형식 라이브러리를 읽고 RCW 코드를 만드는 논리는 System.Runtime.InteropServices.TypeLibConverter라고 불리는 .NET 런타임 클래스 내에 존재합니다. Visual Studio .NET과 TlbImp.exe는 내부적으로 이 클래스를 사용합니다.


그림 3 .COM을 사용하는 NET 클라이언트

그림 3 COM을 사용하는 .NET

그림?3에서는 COM 개체 서버를 사용하는 .NET 클라이언트 프로그램을 예로 보여주고 있습니다. 본 기사 맨 위에 있는 링크를 따라 예제를 다운로드 하십시오. 이 예제에는 COM 서버, COM 클라이언트 및 .NET 클라이언트가 포함되어 있기 때문에 이 두 개를 비교해 볼 수 있습니다. 소스 코드는 그림?4에 표시되어 있습니다.
일단 RCW를 생성한 다음에는 짧은 이름을 사용하여 개체를 참조할 수 있도록 하기 위해, Imports 문을 사용해 RCW의 네임스페이스를 클라이언트 프로그램으로 가져오고 싶어할 것입니다. 이경우 새로운 연산자를 사용하면 RCW 개체를 쉽게 만들 수 있습니다. 일단 RCW 개체가 생성되면 RCW는 기본 COM 함수인 CoCreateInstance를 내부적으로 호출하고, 이 결과 RCW 개체에 의해 래핑되는 COM 개체가 만들어집니다. 그러면 .NET 클라이언트 프로그램은 RCW가 기본 .NET 개체인 것처럼 RCW에서 메서드를 호출하며, RCW는 각 호출을 COM 호출 규칙으로 변환합니다. 예를 들어 .NET 문자열은 COM이 필요로 하는 BSTR 문자열로 변환하여 해당 개체로 보냅니다. RCW는 COM 개체로부터 반환된 결과를 클라이언트로 반환하기 전에 기본 .NET 형식으로 변환합니다.
예제 COM 클라이언트 프로그램을 실행한 다음 단추를 클릭하면 제가 코드에 삽입해 놓은 대화 상자에 개체가 만들어지고 즉시 없어지는 것을 알 수 있을 것입니다. 예제로 제공한? .NET 클라이언트 프로그램을 실행한 다음 Get Time 단추를 클릭하면 개체가 만들어지지만 즉시 없어지지는 않습니다. 래퍼 개체가 시야에서 사라질 때 당연한 것으로 생각할 수 있지만 그렇지 않습니다. 개체 레퍼런스를 'nothing'으로 명시적으로 설정해 놓아도 마찬가지입니다. RCW가 범위를 벗어나서 프로그램에서 더 이상 액세스할 수가 없지만, 나중에 RCW의 가비지가 수집되거나 사라지기 전까지는 RCW가 래핑하는 COM 개체를 실제로 해제하지는 않습니다. 이것은 문제가 될 수 있습니다. 대부분의 COM 개체는 이러한 주기를 고려하지 않고 작성되기 때문에 클라이언트가 종료될 때 바로 해제되어야 하는 값비싼 리소스를 계속 사용하는 경우가 있을 수 있습니다.
이러한 문제는 두 가지 방법 중 하나로 해결할 수 있습니다. 첫 번째 방법은 System.GC.Collect 함수를 통해 고속 가비지 수집을 강제로 수행하는 것입니다. 이 함수를 호출하면 사용하고 있지 않은 모든 시스템 리소스와 범위 내에 존재하지 않는 RCW를 수집하여 다시 사용하게 됩니다. 이 방법의 단점은 전체 가비지 수집의 오버헤드가 높다는 것입니다. 마치 Julia Child가 전체 부엌을 청소하지 않고도 자신이 좋아하는 과도만을 닦아 낼 수 있는 것처럼, 하나의 개체를 버리기 위해 즉시 비용을 들일 필요는 없습니다. 다른 개체에 영향을 주지 않으면서 하나의 특정 COM 개체를 없애려면 System.Runtime.InteropServices.Marshal.ReleaseComObject 함수를 사용할 수 있습니다.
앞 단락에서 설명한 RCW 메커니즘을 위해서는 초기 바운드 개체가 필요합니다. 이것은 개발자가 개발 시점에서 래퍼 클래스를 구성하기 위해 형식 라이브러리가 제공하는 개체에 대해 근본적으로 이해하고 있어야 함을 의미합니다. 하지만 이것이 모든 디자인 시나리오에서 가능한 일은 아닙니다. 예를 들어, 클라이언트가 개체 및 메서드의 ProgID를 읽고 런타임 도중 이를 스크립트 코드로부터 읽는 경우, 스크립트 상황에서는 후기 바인딩이 필요합니다. 대부분의 COM 개체는 특별히 IDispatch 인터페이스를 지원하므로 이러한 형식의 후기 바운드 액세스를 사용할 수 있습니다. 이런 경우에는 RCW를 미리 만들 수 없습니다. 그렇다면 .NET은 이러한 시나리오를 어떻게 처리할 수 있을까요?


Figure 5 Late Binding

그림 5 후기 바인딩

.NET Framework는 대부분의 COM 개체가 지원하는 IDispatch 인터페이스에 대해 후기 바인딩을 지원합니다. 그림?5는 후기 바인딩 프로그램에 대한 예제이며 그림?6은 코드입니다. 정적 메서드인 Type.GetTypeFromProgID를 통해 개체의 ProgID를 기반으로 하는 .NET 시스템 형식을 만드십시오. ProgID 대신 이를 사용할 경우 정적 메서드인 Type.GetTypeFromCLSID(그림?6에는 없음)는 CLSID를 기반으로 하여 같은 역할을 합니다. 그런 다음 Activator.CreateInstance 메서드를 사용하여 COM 개체를 만들고 Type.InvokeMember 함수를 통해 메서드를 호출하여, 메서드의 매개 변수를 배열 형태로 전달합니다. 후기 바인딩의 경우 이 작업은 더 복잡하지만 어렵지 않게 수행할 수 있을 것입니다.


맨 위로


COM에서 .NET 개체 사용

반대로, 이미 COM과 통신하고 있는 클라이언트에서 .NET 개체를 사용하려는 경우를 가정해 봅시다. 이런 경우는 그 반대 상황보다 흔하지 않은 시나리오입니다. 왜냐하면 이 상황에서는 .NET 환경에서의 새로운 COM 개발을 미리 전제로 하기 때문입니다. 하지만 10개의 COM 개체를 사용하는 기존 COM 클라이언트에 .NET 개체로만 존재하는 기능 집합을 추가하려는 경우에 이러한 상황이 발생할 수 있습니다. 그림?7은 COM 호출 가능 래퍼(CCW)를 통해 이러한 상황을 지원하는 .NET Framework를 나타냅니다.
그림 7 COM 호출 가능 래퍼

그림 7 COM 호출 가능 래퍼

CCW는 .NET 개체를 래핑하고 이 개체와 CLR 환경을 연결해 주며 .NET 개체가 기본 .NET 개체인 것처럼 COM 클라이언트에 나타나도록 만듭니다. COM 호출이 가능한 래퍼와 함께 작동하도록 하려면 .NET 구성 요소의 어셈블리에 유효한 이름이 지정되어야 합니다. 그렇지 않으면 CLR 런타임에서는 이름을 명확하게 식별할 수 없게 됩니다. 이 이름은 표준 .NET 구성 요소의 위치 규칙을 따라야 하는데, 이는 이름이 글로벌 어셈블리 캐시(GAC)에 위치해야 하거나 또는 경우에 따라 클라이언트 응용 프로그램의 디렉터리 트리 내에 존재해야 함을 의미합니다. COM에서 만들려는 모든 .NET 클래스는 기본 생성자를 제공해야 하는데, 기본 생성자란 매개 변수가 필요하지 않은 생성자를 말합니다. COM 개체 생성 함수는 함수에서 만들어진 개체한테 매개 변수를 전달하는 방식을 알지 못하므로, 클래스에서 이러한 상황이 필요하지 않도록 만들어야 합니다. COM 클라이언트에 사용할 필요가 없는 생성자를 가지고 있는 한, .NET 클래스에서는 .NET 클라이언트에 사용하기 위해 매개 변수화된 생성자를 제한 없이 사용할 수 있습니다.
COM 클라이언트가 .NET 개체를 찾기 위해서는, 개체를 만들 때 클라이언트가 서버를 찾기 위해 COM에서 필요로 하는 레지스트리 항목을 만들어야 합니다. 이때 .NET SDK에 있는 RegAsm.exe라는 유틸리티 프로그램을 사용하시면 됩니다. 이 프로그램에서는 .NET 클래스에 있는 메타 데이터를 읽은 후 COM 클라이언트를 메타 데이터로 지정하는 레지스트리 항목을 만듭니다. 그림?8은 이 프로그램에서 만들어지는 레지스트리 항목입니다. 여기에 사용하는 COM 서버는 중개 DLL인 Mscoree.dll입니다. InProcServer32 키의 Class 값은 만들고 래핑하려는 .NET 클래스를 이 DLL에게 알려주며, Assembly 항목은 이 클래스를 찾을 .NET 어셈블리를 DLL에게 알려 줍니다.

GAC의 복잡한 디렉터리 구조 때문에 GAC에 있는 .NET 구성 요소를 등록하는 것은 어렵습니다. Explorer에는 사용자 지정 플러그인이 있어 GAC의 복잡성을 단순화할 수 있기 때문에, Explore에서는 복잡해 보이지 않지만 명령줄에서는 손대기 어려울 정도로 디렉터리가 중첩되어 있습니다. .NET 구성 요소를 표준 디렉터리에 같다 좋으면 아주 편리합니다. 여기서 regasm.exe를 실행한 다음 gacutil.exe를 사용하여 구성 요소를 GAC로 이동시킵니다. 등록한 다음 COM 구성 요소를 이동시키면 사용할 수 없게 될 수 있습니다. 그러나 .NET에서는 이런 경우가 없습니다. 사실, DLL Hell은 .NET 아키텍처가 해결하기 위해 특수하게 디자인되었던 문제 중의 하나입니다. 공유된 모든 구성 요소는 유효한 이름을 사용하여 이름이 충돌되지 않도록 지정된 상태에서 GAC에 저장됩니다. .NET 로더는 요청자의 디렉터리 트리에서 요청된 구성 요소를 검사한 다음 원하는 어셈블리를 찾을 수 없는 경우 GAC를 검사합니다.
COM 클라이언트는 .NET 개체가 기본 COM 개체인 것처럼 .NET 개체를 액세스합니다. 클라이언트가 CoCreateInstance를 호출하여 개체를 만들면 레지스트리는 등록된 서버인 Mscoree.dll로 요청을 보냅니다. 이 DLL은 요청된 CLSID를 검사하고 레지스트리를 읽은 다음, 만들려는 .NET 클래스와 해당 클래스가 포함된 어셈블리를 찾으며 해당 .NET 클래스를 기반으로 즉시 CCW을 수행합니다. 모든 DLL COM 서버는 이러한 목적으로 사용할 DllGetClassObject라는 글로벌 함수를 제공합니다. CCW는 기본 COM 형식을 자신의 .NET 형식으로 변환(예: BSTR을 .NET 문자열로 변환)한 다음 .NET 개체로 보냅니다. 또한 .NET의 결과와 오류를 COM으로 변환하기도 합니다. 이 기사에 대한 예제 코드에는 현재 시간을 제공하는 .NET 구성 요소와 현재 시간에 액세스하는 COM 클라이언트가 포함되어 있습니다.

.NET 개발자는 COM 클라이언트 상에서 일부 메서드, 인터페이스 및 클래스는 사용하고 다른 메서드, 인터페이스 및 클래스는 사용하지 않도록 얼마든지 원할 수 있습니다. 따라서 .NET에서는 System.Runtime.InteropServices.ComVisible라는 메타 데이터 속성을 제공합니다. 이 속성은 어셈블리, 클래스, 인터페이스, 개별 메서드 등에 사용될 수 있습니다. CCW는 런타임 도중 이 메타 데이터 특성을 읽은 다음 이 특성이 False로 설정된 요청에 대해 COM으로부터 오는 모든 액세스를 거부할 수 있습니다. 단 하위 계층에서 만들어진 설정은 상위 계층에서는 무시됩니다.

예를 들어, 인터페이스에서 ComVisible을 False로 설정하고 해당 인터페이스의 메서드 중 하나를 True를 설정하면, 클라이언트는 해당 인터페이스에서 해당 메서드를 호출할 수 있지만 다른 메서드에는 액세스할 수 없습니다. 예제 프로그램에서는 다음 코드에 나타난 것처럼 클래스에 대해 이 속성을 True로 설정했습니다. 따라서 COM에서는 이 항목을 볼 수 있습니다.

<System.Runtime.InteropServices.ComVisible(True)>
Public Class Class1
현재 CLR 기본 설정이 True이므로 이 특성이 False로 특별히 설정되어 있지 않으면 이 항목이 COM에 표시됩니다. 그러나, 어셈블리에 대한 Visual Studio .NET의 현재 기본 동작은 프로젝트의 AssemblyInfo.vb 파일에서 이 특성을 False로 설정하게 되어 있습니다. 이것은 사용자가 특별히 표시하도록 지정하지 않는 한 어셈블리 내에서 COM에 아무 것도 표시되지 않는다는 것을 의미합니다.?

개인적으로는 시스템 기본값을 완전히 반대되는 값으로 재정의하는 개발 도구의 관행에 전혀 동의하지는 않습니다. 사용자를 깜짝 놀라게 하는 것은 일반적으로 좋지 않은 것이므로 가능한 한 피해야 할 일입니다. 시스템 설명서를 참조하는 모든 프로그래머는 ComVisible의 기본값이 True임을 알고 있을 것입니다. 그러나 Visual Studio를 사용하여 간단한 구성 요소를 작성할 때 COM은 그 구성 요소를 찾을 수 없으며 프로그래머는 그 이유를 알 수 없을 것입니다. 기본값이 특별히 옳다거나 잘못되었다고 말하는 것은 아닙니다. 각각의 기본값에는 장점과 단점이 있으며 두 기본값이 모두 옳을 수 있다고 생각합니다. 그러나, 운영 체제와 기본적인 개발 환경에서 서로 다른 값을 사용하는 경우 제품이 출시되기 전에 둘 중의 한 기본값을 변경(둘 다 변경하면 결과가 동일)하지 않으면 Microsoft에 수많은 전화 문의가 쇄도할 것입니다.

 

'Dev > .NET' 카테고리의 다른 글

SerializeToXML - C#  (0) 2009.08.10
C# Web Service -> REST  (0) 2009.08.09
Integrating WCF Services with COM+  (0) 2008.09.03
Interop 응용 프로그램 배포  (0) 2007.10.05
RCW, CCW  (0) 2007.10.04
&quot;The underlying connection was closed&quot; - WebServices  (0) 2005.10.13

태그

,