Creative Commons License
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 2.0 Korea License.



맞춤검색


사내에서 C++/CLI 프로그래밍을 함께 하게 될 동료 개발자들을 위해 정리해본다. 이 글은 꾸준히 업데이트될 예정이고, 그 중에서도 제목만 달린 항목부터 추가될 것이다. 단, 함께 일하는 사람들을 위한 문서이므로 외부 사람들이 참조했다간 큰 낭패를 볼 수도 있다. 특정 프로젝트에 최적화한 지침서이기 때문에 어설프게 따라했다가 피 본다 해도 책임지지 않는다.

혼합 네이티브 클래스

클래스의 종류

C++/CLI에는 크게 세 가지 클래스가 있다. 순수 네이티브 클래스, 관리되는 클래스, 그리고 네이티브지만 관리되는 기능을 쓰는 클래스.

// 사소한 문법적 오류는 넘어가주길. 무슨 의도인지만 알면 되지 않겠니?

// 순수 네이티브 클래스의 예.
class PureNativeClass
{
public:
	std::string getName() const
	{
		return std::string("야후~~~~"");
	}
};

// 순수 관리되는 클래스의 예
ref class ManagedClass
{
public:
	property System::String^ Name
	{
		System::String^ get()
		{
			return "어이쿠";
		}
	}
};

// 혼합 네이티브 클래스의 예.
class MixedNativeClass
{
public:
	std::string getName() const
	{
		String^ msg = "문서 쓰기 귀찮다.";
		return marshal_as<std::string^>( message ); // 관리되는 문자열을 표준 라이브러리 문자열로 변환한다.
	}
};

보다시피 혼합 네이티브 클래스에선 관리되는 타입과 네이티브 타입이 혼재한다.

헤더와 소스 코드의 분리

혼합 네이티브 클래스의 경우를 생각해보자. 위의 MixedNativeClass 예제를 MixedNativeClass.h 파일에 정의했다면, 과연 어떻게 될까? 만약 다음과 같은 코드가 어딘가 존재한다면 컴파일 오류가 날 것이다.

// 또 다른 네이티브 소스 코드
#include "MixedNativeClass.h"

class MyClass
{
	MixedNativeClass m_Class;
};

혼합 네이티브 클래스의 인터페이스를 설계할 땐 관리되는 타입을 밖으로 드러내선 안 된다. 그렇기 때문에 인터페이스를 드러내는 헤더 파일과 실제로 클래스를 구현하는 소스 파일을 반드시 분리시켜야 한다.

도대체 혼합 네이티브 클래스가 왜 필요한 거냐?

앞서 혼합 네이티브 클래스를 프로그래밍할 때의 원칙을 설명했는데, 정작 혼합 네이티브 클래스가 왜 필요한지는 이야기 하지 않았다. C++/CLI 프로젝트는 크게 두 가지가 있다. 실제론 더 잘게 나눌 수 있지만, 널리 쓰이는 용도로 나누면 그렇다. 하나는 아예 처음부터 닷넷 애플리케이션을 작성하기로 한 경우다. C#으로 프로그래밍하는 것과 똑같은 결과물을 얻고 싶을 때가 있다. C++ 문법에 더 익숙해서(아마도 C++ 개발자라서) 굳이 C++/CLI를 고른 경우가 대부분일 것이다. 이 경우엔 혼합 네이티브 클래스는 쓰지 않는다. 아니, 쓰지 못한다.

혼합 네이티브 클래스는 보통 기존 네이티브 C++ 소스 코드에 관리되는 기능을 추가할 때 쓴다. 우리는 당연히 이 경우에 해당한다. 기존 라이브러리를 C#에서 쓰고 싶다던가 할 때가 있기 마련이다. 그럴 때 혼합 네이티브 클래스가 기존 네이티브 코드와 새로 추가되는 관리되는 클래스를 연결하는 다리 역할을 하게 된다.


템플릿 사용에 대해

관리되는 클래스를 정의할 때 템플릿을 써도 된다. 템플릿은 네이티브 객체를 감싸는(Wrapping) 관리되는 클래스를 짤 때 특히 유용하다. 그러나 템플릿을 쓸 거라면 정말 주의해야 한다. 우선 C#에선 템플릿화된 클래스를 참조하지 못한다. generic으로 구현한 관리되는 클래스는 C#에서 참조할 수 있지만, template으로 구현한 경우는 그렇지 않다. 단, 다음과 같은 경우는 C#에서 참조해 쓸 수 있다. 요컨대 최종적으로 공개하는 인터페이스에만 템플릿이 없으면 된다.

public ref class CDBObjectManaged abstract : public CSharedObjectManaged<CDBObject>
{ 
}

헤더와 소스 코드를 꼭 분리해야 하나?

앞서 혼합 네이티브 클래스의 경우를 예로 들었다. 혼합 네이티브 클래스는 반드시 헤더와 소스 코드를 분리해야 한다. 하지만 다른 클래스(네이티브 클래스와 관리되는 클래스)도 그래야 할까? 물론이다. 이제부터 왜 그런지 알아보자.


C++/CLI 프로젝트에 소스 코드를 추가할 때

프로젝트 속성을 보자. 프로젝트 기본값으로 공용 언어 런타임 지원(/clr) 옵션을 켜놓은 게 보인다. 소스 파일(.cpp)을 프로젝트에 추가하면 이 기본값이 적용된다. 네이티브 타입만 쓰는 소스 파일일지라도 /clr 컴파일을 하게 된다. 이는 컴파일 시간을 늘리고, 런타임시 애플리케이션의 성능에도 영향을 미치게 된다(네이티브와 CLR 간의 전환 때문에). 그러므로 새 소스 코드를 추가할 때는 해당 파일의 용도부터 차분히 생각해보고, 만약 네이티브 타입만 필요한 경우라면 해당 소스 파일의 속성값을 고쳐주어야 한다. 그런데 /clr 옵션만 끈다고 문제가 해결되는 게 아니라서 골치 아프다. 미리 컴파일된 헤더 파일의 이름이나 기본 런타임 검사 등의 값을 고쳐 써야 하는데, 매우 귀찮은 일이기도 하고 실수하기 쉽기도 하다. 그러나 손쉬운 해결책이 있으니 고민할 필요 없다. 프로젝트 파일(.vcproj)을 직접 열어서 Copy&Paste 신공을 펼치면 된다.

우선 일반적인 /clr 소스 파일은 다음과 같이 구성된다.

<File
	RelativePath=".\src\AppConfiguration.cpp"
	>
</File>

반면 네이티브 소스 파일은 이렇게 구성된다.

<File
	RelativePath=".\src\UserStatSIS.cpp"
	>
	<FileConfiguration
		Name="Debug|Win32"
		>
		<Tool
			Name="VCCLCompilerTool"
			MinimalRebuild="true"
			ExceptionHandling="1"
			BasicRuntimeChecks="3"
			SmallerTypeCheck="true"
			PrecompiledHeaderThrough="StdAfx.h"
			PrecompiledHeaderFile="$(IntDir)\$(TargetName).pch"
			CompileAsManaged="0"
		/>
	</FileConfiguration>
	
	<!-- 중략 -->
</File>

다른 생각할 것 없이 XML 코드를 복사해 붙여넣고 파일 이름만 고치면 된다.


포인터 사용시 주의할 점

System::IntPtr과 네이티브 포인터

ref class MyClass
{
public:
	explicit MyClass(const NativeClass* ptr)
	{
		Init(ptr);
	}

	explicit MyClass(System::IntPtr ptr)
	{
		Init(ptr.ToPointer());
	}

private:
	void Init(const NativeClass* ptr);
};

네이티브 클래스에 대한 래퍼 클래스를 구현할 때는 위와 같은 패턴을 적용하길 권한다. System::IntPtr은 네이티브 포인터를 관리되는 환경에 전달할 때 쓰는 타입이다. 순수한 관리되는 환경(예, C#)에선 네이티브 타입을 알 리 없으므로 이러한 특수 타입이 필요하다. 하지만 System::IntPtr::ToPointer()void*를 반환하므로, 엉뚱한 타입의 포인터를 넘겨도 알 도리가 없다.

그러므로 같은 C++/CLI 어셈블리(같은 프로젝트) 내부의 클래스만이라도 정확한 타입의 포인터를 넘겨 받도록 첫번째 생성자를 구현한다. 안타깝게도 C#쪽에선 첫번째 생성자의 존재를 모르므로(알 수 없으므로), 항상 이렇게 짝을 지어 구현해야 한다. 이는 비단 C#에만 적용되는 이야기가 아니다. 이 클래스가 든 어셈블리를 사용하려는 어셈블리가 C++/CLI로 구현되었더라도 마찬가지다. 관리되는 타입의 정보는 헤더 파일이 아닌 메타데이터를 통해 전달되는데, 첫번째 클래스에 대한 메타데이터는 만들어지지 않는다. 그러므로 다른 어셈블리(다른 프로젝트)에서 MyClass의 인스턴스를 만들려면 두번째 생성자가 반드시 있어야 한다.


스마트 포인터

네이티브 클래스에 대한 래퍼 클래스를 구현하는 경우를 생각해보자. 보통 래퍼 클래스(관리되는 클래스)의 멤버 변수로써, 네이티브 클래스에 대한 포인터를 들고 있기 마련이다. 여기까진 좋다. 위의 예제처럼 생성자에서 원본 네이티브 인스턴스의 포인터를 받아서 멤버 변수에 저장하면 된다.

문제는 네이티브 클래스 쪽에서 스마트 포인터를 쓸 때 발생한다. 스마트 포인터를 가비지 콜렉터가 없는 네이티브 환경에서 자원 관리를 조금이라도 편하게 만들고자 도입한 수단이다. 그런데 이 스마트 포인터는 실은 포인터 흉내를 내는 클래스다. 그런데 관리되는 클래스는 진짜 포인터만을 멤버로 가질 수 있다. 즉, 스마트 포인터를 멤버로 가지려면, 스마트 포인터에 대한 진짜 포인터를 멤버로 가져야 한다는 말이다. 말부터 이렇게 꼬이는데 실제로 스마트 포인터에 대한 포인터를 멤버로 갖는 클래스를 작성하려면 얼마나 힘들겠는가!

안타깝게도 뾰족한 해결책은 없다. 스마트 포인터에 대한 포인터를 멤버로 가지는 대신, 어떻게든 쓰기 편하게 잘 구현해보는 것도 좋은 방법이리라 생각한다. 하지만 우리는 스마트 포인터 대신 소유권의 개념을 적용했다. 이에 대해선 자세히 언급하진 않겠다. 다만 오직 네이티브 클래스에 대한 포인터만이 관리되는 클래스의 멤버가 될 수 있다는 사실을 잊지 말자.


연산자 오버로딩 패턴

이 문제에 대해선 C++/CLI에서의 연산자 오버로딩 패턴을 참고하기 바란다.



피드백과 지원

이 문서에 대한 피드백이나 지원을 하고 싶으신 분은 저자에게 연락해주시기 바랍니다. 발전적인 정보 제공 및 건의는 언제나 환영합니다.



댓글들

daybreaker on 07.08.2008 at 01:37 PM

음, RSS에서 이 글을 보니 서식이고 뭐고 다 한 줄로(...) 붙어나오네요;; 블로그 뭔가 수정을 하고 계시는 듯한데 손좀 봐주셔야 할 것 같습니다;

최재훈 on 07.08.2008 at 02:07 PM

원복이 제대로 안 됐네요. 구글 리더 쪽에서 테스트해보니 이 블로그의 상당수 피드가 제대로 업데이트가 안 되는 듯 하여 손을 좀 보던 중이예요. 혹 구글 리더 쓰면 제대로 새 글이 업데이트되는지 알려줄래요? 적어도 내 계정에선 안 되던데 말이죠.

테스트용 rss 페이지를 만들었어야 했는데, 덥고 짜증나다 보니 대충하다 일 냈네요. 쩝.

daybreaker on 07.09.2008 at 12:35 AM

저는 HanRSS를 쓰고 있습니다만 지금 글을 쓰는 현재 제대로 잘 나오고 있습니다. smile

최재훈 on 07.14.2008 at 06:07 PM

HanRSS는 문제 없는 걸 확인했는데, 구글 리더가 파싱을 제대로 못 하는 듯 해서. 계정 하나 더 만들어서 테스트해봐야 하나.


댓글을 달아주세요

(필수)

(필수)


(필수) 이모티콘

(필수)