출처: http://agebreak.blog.me/60059787064



관리 코드와 네이티브 코드의 상호 운용성을 위한 최상의 방법
Jesse Kaplan
어떻게 보면 2009년 초반에 이런 칼럼이 MSDN Magazine에 등장하는 것이 이상하게 느껴질 수도 있습니다. 관리 코드와 네이티브 코드의 상호 운용성은 그 정도의 차이는 있을지 모르지만 2002년 버전 1.0부터 Microsoft .NET Framework에서 꾸준히 지원되어 왔기 때문입니다. 또한 API 및 도구 수준의 자세한 지원 문서도 수천 페이지는 찾을 수 있습니다. 그럼에도 불구하고 어떠한 경우에 interop을 사용하고 아키텍처와 관련하여 어떤 점을 고려하고 어떤 interop 기술을 사용해야 하는지를 설명하는 전반적이고 포괄적인 아키텍처 지침은 찾기 어렵습니다. 이 칼럼은 이러한 틈을 메우기 위한 것입니다.


관리되는 네이티브 Interop의 적합한 용도
관리되는 네이티브 interop을 어떤 경우에 사용해야 하는지에 대해서는 자료가 별로 없고 그나마 있는 자료들도 서로 내용이 상이합니다. 게다가 실제 검증 없이 지침을 작성된 지침도 있습니다. 본론을 시작하기 전에 이 칼럼의 모든 내용은 interop 팀이 다양한 규모의 내부 및 외부 고객을 지원하는 과정에서 실제 경험을 바탕으로 작성되었음을 밝힙니다.
이러한 경험을 취합하면서 interop의 성공적인 사용 사례이자 interop의 사용 유형을 잘 보여주는 세 가지 제품을 예로 들었습니다. Visual Studio Tools for Office는 Office용 관리되는 확장 가능 도구 집합으로, interop하면 가장 먼저 떠오르는 응용 프로그램입니다. 이 응용 프로그램은 관리되는 확장 또는 추가 기능 구현이 가능한 대규모 네이티브 응용 프로그램이라는 interop의 일반적인 사용 방식을 보여 줍니다. 다음으로 관리되는 응용 프로그램과 네이티브 응용 프로그램의 혼합형으로 완전히 새롭게 개발된 Windows Media Center를 들 수 있습니다. Windows Media Center는 주로 관리되는 코드를 사용하여 개발되었고 일부분(TV 튜너 및 기타 하드웨어 드라이버와 직접 작동하는 부분)만 네이티브 코드를 기반으로 합니다. 마지막으로 기존 네이티브 코드 기반에서 새로운 관리되는 기술을 활용하여 차세대 사용자 환경을 제공하려 하는 응용 프로그램의 예로서 Expression Design이 있습니다. 여기서 새로운 관리되는 기술이란 WPF(Windows Presentation Foundation)을 말합니다.
이 세 응용 프로그램은 interop을 사용하는 가장 중요한 이유 세 가지를 보여 줍니다. 첫째는 기존 네이티브 응용 프로그램에 관리되는 확장성을 제공하는 것이고, 둘째는 대부분의 응용 프로그램이 가장 기본적인 수준의 코드는 네이티브 코드로 작성하면서 관리 코드의 이점을 활용하도록 하는 것이고, 셋째는 기존 네이티브 응용 프로그램에 차별화된 차세대 사용자 환경을 추가하는 것입니다.
예전에 제공된 지침에서는 이러한 경우에 단순히 전체 응용 프로그램을 관리 코드로 작성할 것을 권장하고 있습니다. 많은 사람들이 이러한 지침을 따르려고 했지만 그러지 못한 것만 봐도 대부분의 기존 응용 프로그램의 경우 전체 응용 프로그램을 관리 코드로 작성하는 것이 불가능함을 입증한다고 하겠습니다. interop은 개발자가 네이티브 코드에 대해 기존에 투자한 자산을 유지하면서 새로운 관리되는 환경의 이점을 활용하는 데 없어서는 안 될 기술입니다. 다른 이유로 응용 프로그램을 다시 작성할 계획이라면 관리 코드로 작성할 수도 있겠지만 일반적으로 단지 새로운 관리되는 기술을 사용하고 interop을 사용하지 않기 위해 응용 프로그램을 다시 작성하는 것은 바람직하지 않습니다.


Interop 기술: 세 가지 선택
.NET Framework에서 사용 가능한 주요 interop 기술에는 세 가지가 있으며 interop에 사용하는 API의 형식과 경계 제어의 요구 사항과 필요성에 따라 세 가지 기술 중 하나를 선택하게 됩니다. 플랫폼 호출 또는 P/Invoke는 주로 관리되는 코드에서 네이티브 코드로의 interop 기술로서 관리 코드에서 C 스타일 네이티브 API를 호출할 수 있도록 합니다. COM interop은 관리 코드에서 네이티브 COM 인터페이스를 사용하거나 관리되는 API에서 네이티브 COM 인터페이스를 내보낼 수 있도록 합니다. 마지막으로 C++/CLI(이전의 관리되는 C++)는 관리되는 C++ 컴파일 코드와 네이티브 C++ 컴파일 코드가 혼합되어 있는 어셈블리를 만들 수 있도록 해주며, 관리 코드와 네이티브 코드를 연결하는 다리 역할을 합니다.


Interop 기술: P/Invoke
P/Invoke는 세 가지 기술 중 가장 간단하며 주로 C 스타일 API에 관리 코드에 대한 액세스를 제공할 목적으로 설계되었습니다. P/Invoke를 사용하는 경우 각 API를 개별적으로 래핑해야 합니다. 따라서 래핑할 API 수가 적고 서명이 그다지 복잡하지 않은 경우에 적합합니다. 그러나 관리되지 않는 API에 동등한 관리되는 항목이 없는 인수(변수-길이 구조, void *s, 중첩되는 공용 구조체 등)가 많으면 P/Invoke를 사용하기가 어려워집니다.
.NET Framework BCL(기본 클래스 라이브러리)에 포함된 여러 가지 API 예제는 수많은 P/Invoke 선언 주위의 두꺼운 래퍼일 뿐입니다. .NET Framework의 기능 중 관리되지 않는 Windows API를 래핑하는 기능 대부분은 P/Invoke를 사용하여 작성됩니다. 사실 Windows Forms도 거의 전체가 P/Invoke를 사용하는 ComCtl32.dll을 기반으로 합니다.
P/Invoke를 훨씬 쉽게 사용할 수 있도록 하는 유용한 리소스가 몇 가지 있습니다. 첫째로, 웹 사이트 pinvoke.net에는 CLR interop 팀의 Adam Nathan이 처음 설정한 Wiki가 있는데, 이 Wiki에는 많이 사용되는 다양한 Windows API에 대해 사용자가 만든 서명이 많이 포함되어 있습니다.
Visual Studio에서 pinvoke.net을 쉽게 참조할 수 있도록 해주는 유용한 Visual Studio 추가 기능도 있습니다. 개발자의 자체 라이브러리에 있는 API든, 다른 사람이 만든 API든, pinvoke.net에서 다루지 않는 API를 위해 interop 팀에서는 P/Invoke Interop Assistant라는 P/Invoke 서명 생성 도구를 발표했습니다. 이 도구는 헤더 파일을 기준으로 네이티브 API에 대한 서명을 자동으로 생성합니다. 다음 스크린샷은 이 도구의 작동 모습을 보여 줍니다.
P/Invoke Interop Assistant에서 서명 만들기


Interop 기술: COM Interop
COM interop은 관리 코드에서 COM 인터페이스를 사용하거나 관리되는 API를 COM 인터페이스로 제공할 수 있도록 해줍니다. TlbImp 도구를 사용하면 특정 COM tlb와 통신하기 위한 관리되는 인터페이스를 제공하는 관리되는 라이브러리를 생성할 수 있습니다. TlbExp는 반대의 작업을 수행하며 관리되는 어셈블리의 ComVisible 형식에 해당하는 인터페이스를 제공하는 COM tlb를 생성합니다.
응용 프로그램 내부적으로 또는 확장 모델로서 이미 COM을 사용하는 경우에 COM interop이 적합한 솔루션이 될 수 있습니다. 또한 관리되는 코드와 네이티브 코드 간에 COM 의미 체계를 정확히 일치하도록 유지하는 가장 손쉬운 방법이기도 합니다. CLR은 기본적으로 Visual Basic 6.0과 동일한 COM 규칙을 따르기 때문에 특히 Visual Basic 6.0 기반 구성 요소와 상호 작용하는 경우에 COM interop이 적합합니다.
반면 응용 프로그램에서 내부적으로 COM을 사용하고 있지 않거나, COM 의미 체계를 완전히 동일하게 유지할 필요가 없고 성능이 응용 프로그램에 적합하지 않은 경우에는 COM interop이 적합하지 않습니다.
COM interop을 관리되는 코드와 네이티브 코드를 잇는 교량으로 사용하는 응용 프로그램의 대표적인 예로 Microsoft Office를 꼽을 수 있습니다. Office에는 확장 메커니즘으로서 COM이 오래 전부터 사용되어왔고 VBA(Visual Basic for Applications) 또는 Visual Basic 6.0에서 많이 사용되었기 때문에 COM interop을 활용하기에 더할 나위 없이 좋습니다.
원래 Office는 관리되는 개체 모델로 TlbImp와 얇은 interop 어셈블리를 전적으로 사용합니다. 그러나 시간이 지나면서 VSTO(Visual Studio Tools for Office) 제품이 Visual Studio를 기반으로 하게 되고 이 칼럼에서 설명하는 여러 가지 원칙을 통합하여 보다 기능이 풍부한 개발 모델을 제공하게 되었습니다. 현재는 BCL의 많은 부분이 P/Invoke를 기반으로 한다는 사실을 잊기 쉬운 것처럼 VSTO 제품을 사용할 때 COM interop이 VSTO의 기반이 된다는 사실을 잊는 경우가 많습니다.


Interop 기술: C++/CLI
네이티브 환경과 관리되는 환경 사이의 교량 역할을 하도록 설계된 C++/CLI는 관리 C++ 코드와 네이티브 C++ 코드를 같은 어셈블리는 물론 같은 클래스에도 컴파일할 수 있도록 하고 어셈블리의 두 부분 간에 표준 C++ 호출을 만듭니다. C++/CLI을 사용하는 경우 어셈블리에서 관리되는 환경으로 구현할 부분과 네이티브 환경으로 구현할 부분을 선택하게 됩니다. 그 결과로 MSIL(모든 관리되는 어셈블리에 포함된 Microsoft Intermediate Language)과 네이티브 어셈블리 코드가 혼합된 어셈블리가 생성됩니다. C++/CLI는 interop 경계를 거의 완벽하게 제어할 수 있는 매우 강력한 interop 기술이지만 경계에 대해 개발자의 제어권을 지나치게 강요한다는 단점이 있습니다.
C++/CLI는 정적 형식 검사가 필요하고 성능이 엄격하게 요구되고 종료를 예측할 수 있어야 하는 경우에 교량 역할로서 적합합니다. 요구 사항에만 맞다면 일반적으로 P/Invoke 또는 COM interop을 사용하는 편이 간단합니다. 특히 C++에 익숙하지 않은 개발자에게는 더욱 편리합니다.
C++/CLI를 고려할 때는 몇 가지에 유의해야 합니다. 먼저 C++/CLI를 사용하여 처리 속도가 빠른 버전의 COM interop을 제공하려는 경우 COM interop은 자동으로 처리하는 작업이 많기 때문에 C++/CLI보다 느리다는 점을 알아야 합니다. 따라서 응용 프로그램에서 COM을 한정적으로만 사용하고 충실도 높은 COM interop이 필요하지 않다면 문제가 되지 않습니다.
그러나 COM 사양의 많은 부분을 사용하는 경우 필요한 COM 의미 체계를 C++/CLI 솔루션에 다시 추가하려면 작업량이 많을 뿐만 아니라 COM interop이 제공하는 성능보다 향상되지도 않습니다. 몇몇 Microsoft 팀에서 이런 전철을 밟았다가 문제를 깨닫고 다시 COM interop을 사용한 사례가 있습니다.
C++/CLI 사용과 관련하여 두 번째로 중요하게 고려해야 할 사항은 C++/CLI가 관리되는 환경과 네이티브 환경을 잇는 교량의 역할만 할 뿐이며 응용 프로그램의 대부분을 작성하기 위한 기술이 아니라는 점입니다. 물론 C++/CLI로 응용 프로그램의 대부분을 작성할 수도 있지만 순수하게 C++ 또는 C#/Visual Basic 환경에서 작성할 때보다 개발자 생산성이 훨씬 떨어지고 부팅 시에 응용 프로그램 실행 속도가 현저하게 느려집니다. 따라서 C++/CLI를 사용할 때는 필요한 파일만 /clr 스위치를 사용하여 컴파일하고 순수한 관리되는 어셈블리 또는 순수한 네이티브 어셈블리의 조합을 사용하여 응용 프로그램의 핵심 기능을 작성해야 합니다.


Interop 아키텍처 고려 사항
응용 프로그램에 interop을 사용하기로 결정하고 사용할 기술도 선택했다면 솔루션을 설계할 때 API 디자인이나 interop 경계에 대한 코드를 작성하는 개발자 환경 등 상위 수준에서 고려해야 할 사항이 있습니다. 또한 네이티브-관리 코드 전환과 이러한 전환이 응용 프로그램에 미치는 영향도 고려해야 합니다. 마지막으로 수명 관리를 고려하고 관리되는 환경의 가비지 수집 환경과 네이티브 환경의 수동/결정적 수명 관리 사이의 격차를 메울 수단이 필요한지를 결정해야 합니다.


API 디자인 및 개발자 환경
API 디자인과 관련해서는 스스로 몇 가지 질문에 대한 답을 구해야 합니다. 어떤 개발자들이 interop 계층에 대해 코드를 작성하게 되며 그러한 개발자의 환경을 개선하거나 경계 구축 비용을 최소화하기 위해 최적화해야 하는가? 이 경계에 대해 코드를 작성하는 개발자와 네이티브 코드를 작성하는 개발자가 동일한가? 아니면 회사의 다른 개발자들인가? 아니면 응용 프로그램을 확장하거나 서비스로 사용하는 타사 개발자들인가? 개발자의 숙련도는 어떤 수준인가? 네이티브 패러다임에 익숙한가 아니면 관리되는 코드를 작성하는 데만 익숙한가?
이러한 질문에 대한 답을 구하면 네이티브 코드의 매우 얇은 래퍼와 내부적으로 네이티브 코드를 사용하는 기능이 풍부한 관리되는 개체 모델 사이에서 적절한 지점을 결정하는 데 도움이 됩니다. 얇은 래퍼의 경우 모든 네이티브 패러다임이 명확히 드러나고 개발자가 네이티브 API에 대해 코드를 작성하고 있다는 사실과 경계를 확실하게 인식하게 됩니다. 반면 두꺼운 래퍼를 사용하는 경우 네이티브 코드가 기반이 된다는 사실이 드러나지 않습니다. BCL의 파일 시스템 API는 중요한 관리되는 개체 모델을 제공하는 매우 두꺼운 interop 계층의 좋은 예입니다.


성능과 Interop 경계의 위치
공연히 응용 프로그램을 최적화하느라 시간을 허비하지 않으려면 interop 성능이 문제가 되는지 여부를 확인하는 것이 중요합니다. 응용 프로그램에서 성능이 중요한 부분에 interop이 사용되기 때문에 주의를 기울여야 하는 경우가 많습니다. 그러나 사용자의 마우스 클릭 동작에 대한 응답으로 interop을 사용하고 interop 전환이 수십, 수백, 수천 번 발생하더라도 사용자에게 지연을 유발하지 않는 응용 프로그램도 많습니다. 따라서 interop 솔루션의 성능을 높이려면 interop 전환 횟수와 각 전환 시에 전달되는 데이터 양을 줄여야 합니다.
관리되는 환경과 네이티브 환경 간에 일정한 데이터 양으로 발생하는 일정 횟수의 interop 전환은 기본적으로 그에 해당하는 고정 비용을 발생시킵니다. 이 고정 비용은 선택하는 interop 기술에 따라 다르지만 해당 기술이 필요해서 선택했다면 변경할 수 없을 것입니다. 즉, 경계의 빈번한 전환을 줄이고 경계를 넘어 전달되는 데이터의 양을 줄이는 데 초점을 맞추어야 합니다.
이러한 목표를 달성하는 방법은 응용 프로그램에 크게 좌우됩니다. 그러나 많은 개발자가 성공적으로 활용한 일반적인 전략으로서 경계의 한쪽에 자주 사용되고 데이터 양이 많은 인터페이스를 정의하는 코드를 작성하여 격리된 경계를 이동하는 방법이 있습니다. 여기서 핵심은 매우 자주 사용되는 인터페이스에 대한 호출을 일괄 처리하는 추상 계층을 작성하거나 응용 프로그램 논리에서 경계를 넘어 이 API와 상호 작용해야 하는 부분을 이동하고 입력 및 결과만 경계를 넘어 전달하는 것입니다.


수명 관리
수명 관리에 있어서 관리되는 환경과 네이티브 환경의 차이점은 interop 고객에게 가장 큰 걸림돌이 되곤 합니다. .NET Framework의 가비지 수집 기반 시스템과 네이티브 환경의 수동 및 결정적 시스템 간의 근본적인 차이점은 진단하기 어려운 형태로 나타나는 경우가 많습니다.
interop 솔루션에서 가장 먼저 나타나는 문제는 관리되는 개체가 관리되는 환경에서 사용이 끝난 후에도 네이티브 리소스를 오랫동안 보유하게 되는 문제입니다. 이는 네이티브 리소스가 매우 부족하여 호출자가 사용이 끝나는 즉시 해제해야 하는 경우에 문제가 됩니다. 대표적인 예로 데이터베이스 연결을 들 수 있습니다.
이러한 리소스가 부족하지 않다면 가비지 수집기가 개체의 종료자를 호출하여 종료자가 암시적으로 또는 명시적으로 네이티브 리소스를 해제하도록 하면 됩니다. 반면 리소스가 부족한 경우에는 관리되는 dispose 패턴이 매우 유용할 수 있습니다. 네이티브 개체를 관리되는 코드에 직접적으로 노출하는 대신 네이티브 개체 주위에 최소한 얇은 래퍼라도 만들어 IDisposable을 구현하고 표준 dispose 패턴을 따르도록 해야 합니다. 이렇게 하면 리소스 고갈이 문제가 될 경우에 관리되는 코드에서 이러한 개체를 명시적으로 정리하고 사용이 끝나는 즉시 리소스를 해제할 수 있습니다.
응용 프로그램에 많이 영향을 미치는 두 번째 수명 관리 문제는 개발자가 까다로운 가비지 수집 문제로 오해하는 경우가 많습니다. 즉, 메모리 사용량이 계속 증가하지만 어떤 이유에서인지 가비지 수집기의 실행 빈도가 낮고 개체가 활성화된 상태로 유지되는 것입니다. GC.Collect에 대한 호출이 계속 추가되어 문제가 더욱 심각해지기도 합니다.
일반적으로 이러한 문제의 근본 원인은 크기가 매우 작은 관리되는 개체가 매우 큰 네이티브 데이터 구조를 계속 사용하면서 활성화된 상태로 유지되기 때문입니다. 그 결과로 가비지 수집기가 불필요하거나 효과가 없는 수집 작업에 시간을 낭비하지 않기 위해 자체 튜닝됩니다. 또한 가비지 수집을 추가로 실행할지 여부를 결정할 때 프로세스의 현재 메모리 사용량은 물론 각각의 가비지 수집으로 확보되는 메모리 양까지 확인합니다.
하지만 이러한 상황에서 가비지 수집기가 실행되면 확보되는 관리되는 메모리 양만 인식하므로 각각의 수집으로 확보되는 메모리 양이 적다고 판단하고 그러한 작은 개체를 정리함으로써 전반적인 메모리 부족 상황을 크게 개선할 수 있다는 사실은 인식하지 못합니다. 때문에 메모리 사용량은 계속 증가하지만 가비지 수집 실행 빈도는 점점 더 낮아지는 현상이 나타납니다.
이 문제는 이러한 작은 관리되는 래퍼에 따른 실질적인 네이티브 리소스 비용에 대한 힌트를 가비지 수집기에 제공함으로써 해결할 수 있습니다. .NET Framework 2.0에는 이러한 용도로 사용되는 API가 한쌍 추가되었습니다. 명시적으로 리소스를 해제하는 대신 dispose 패턴을 추가하는 데 사용하는 것과 동일한 형식의 래퍼를 부족한 리소스에 사용하여 가비지 수집기에 힌트를 제공하는 데 재활용할 수 있습니다.
이 개체의 생성자에서 GC.AddMemoryPressure 메서드를 호출하여 네이티브 개체의 네이티브 메모리에서 소모되는 대략적인 비용을 전달합니다. 그런 다음 개체의 종료자 메서드에서 GC.RemoveMemoryPressure를 호출하면 됩니다. 이 두 호출은 가비지 수집기가 이러한 개체의 실제 비용과 해당 개체를 해제했을 때 확보되는 실제 메모리를 인식하는 데 도움을 줍니다. 이 경우 Add/RemoveMemoryPressure에 대한 호출의 균형을 완벽하게 유지하는 것이 중요합니다.
관리되는 환경과 네이티브 환경의 수명 관리에 있어서 나타나는 세 번째 차이점은 개별 리소스나 개체의 관리보다 전체 어셈블리 또는 라이브러리와 관련이 있습니다. 네이티브 라이브러리는 응용 프로그램에서 사용을 마쳤을 때 쉽게 언로드할 수 있지만 관리되는 라이브러리는 자동으로 언로드되지 않습니다. 대신 CLR에는 개별적으로 언로드할 수 있고 언로드 시에 실행 중인 모든 어셈블리, 개체 및 스레드를 정리하는 AppDomains라는 격리 단위가 있습니다. 네이티브 응용 프로그램을 작성하는 개발자가 사용이 끝난 추가 기능을 언로드하는 데 익숙한 경우 관리되는 추가 기능마다 AppDomains를 사용하면 개별 네이티브 라이브러리를 언로드할 때와 동일한 유연성을 얻을 수 있습니다.


질문이나 의견이 있으면 clrinout@microsoft.com으로 보내시기 바랍니다.


Jesse Kaplan은 현재 Microsoft CLR 팀에서 프로그램 관리자로서 관리되는 환경/네이티브 환경의 상호 운용성을 담당하고 있으며 이전에는 호환성과 확장성을 담당하기도 했습니다.


Posted by 세모아
,