네이티브 C++에서의 DLL을 CLR 환경에선 Assembly라 부른다. 그런데 CLR 환경에선 한번 불러들인(Load) 어셈블리를 마음대로 해제(Unload)할 수가 없다. 애초에 System::Reflection::Assembly에는 Unload에 해당하는 함수 자체가 없다. 단 독립적인 메모리 공간을 갖는 애플리케이션 도메인(AppDomain)을 하나 생성해서, 그 안에 어셈블리를 불러들이는 방법이 있다. AppDomain에는 Unload 함수가 있기 때문에 가능한 일이다. 그러나 별도의 AppDomain을 생성하면 골치 아픈 문제가 한둘이 아니다.

아, AppDomain에 관한 이야기를 하려던 게 아니었다. C++/CLI은 정말 C# 난이도와 C++의 난이도를 곱해놓은 만큼 어렵다. 특히 Mixed Type을 쓰면 머리에 쥐가 나는데, 네이티브 객체가 쓰는 CRT 힙과 관리되는 객체가 쓰는 CRL 힙을 둘 다 머리 속에 그려 넣고 작업해야 한다. 뿐만 아니라 자원 해제 문제까지 걸려 있다. 특히 네이티브 객체가 관리되는 객체의 포인터를 갖고 있는 경우에 골치 아프다.

class MemTest 
{
private:
	gcroot<Shared::NPC^> npcObj;
public:
	MemTest(Shared::NPC^ obj)
	{
		npcObj = obj;
	}

	~MemTest()
	{
		delete npcObj;
	}
};

클래스 MemTest는 네이티브 객체이고, 클래스 Content::NPCElf는 관리되는 객체이다.


// Create the new AppDomain
AppDomain^ domain = Model2Test::Instance()->GetDomain();
Content::NPCProvider^ provider = static_cast<Content::NPCProvider^>(
	domain->CreateInstanceFromAndUnwrap(Path::Combine(domain->BaseDirectory, "Content.dll"), 
	"Content.NPCProvider"
	)
);

// GC Heap Size   0x810a8(528552)
MemTest* dangling = 0;

for(int i=0; i<1000; i++)
{
	dangling = new MemTest(provider->Create("NPCElf"));
	dangling = NULL;
}

AppDomain::Unload(domain);
domain = nullptr;

// GC Heap Size  0x1c535c(1533232)	
GC::Collect();
// GC Heap Size  0x1c5d44(853184)

Console::ReadLine();

가비지 수집 시점까지 네이티브 객체 MemTest 내부에서 관리되는 객체에 대한 포인터를 유지하고 있기 때문에, 해당 관리되는 객체는 가비지 수집 대상에서 제외된다.


// Create the new AppDomain
AppDomain^ domain = Model2Test::Instance()->GetDomain();
Content::NPCProvider^ provider = static_cast<Content::NPCProvider^>(
	domain->CreateInstanceFromAndUnwrap(Path::Combine(domain->BaseDirectory, "Content.dll"), 
	"Content.NPCProvider"
	)
);

// GC Heap Size   0x810a8(528552)
MemTest* dangling = 0;

for(int i=0; i<1000; i++)
{
	dangling = new MemTest(provider->Create("NPCElf"));
	delete dangling;
}

AppDomain::Unload(domain);
domain = nullptr;

// GC Heap Size  0x1c535c(1292032)	
GC::Collect();
// GC Heap Size  0x1c5d44(336296)	

Console::ReadLine();

MemTest의 인스턴스를 해제하면서, 그 인스턴스가 갖고 있던 관리되는 객체에 대한 포인터도 함께 해제했다. 덕분에 가비지 수집기가 메모리를 깔끔하게 회수해낼 수 있었다. C#의 IDisposable 패턴과 C++의 메모리 관리 디자인 패턴을 적절히 섞어서 구사해야 하니 골 아프다.


	
Shared::NPC^ dangling = nullptr;
// GC Heap Size   0x6c098(442520)

for(int i=0; i<1000; i++)
{
	dangling = gcnew Content::NPCElf();
	dangling = nullptr;
}

AppDomain::Unload(domain);
domain = nullptr;

// GC Heap Size   0x5110c(2035648)
GC::Collect();
// GC Heap Size   0x5110c(332268)

Console::ReadLine();

순수하게 관리되는 객체로만 코드를 짜면, 코드가 깔끔해진다. 물론 네이티브의 장점과 CLR의 장점을 적절히 취하려고 C++/CLI를 쓰는 것이니, 무의미한 이야기이다. 단지 Mixed-Type을 쓸 때는 정말 조심해야 한다는 교훈은 얻을 수 있다.