좀 긴가민가했던 .NET core 관련 내용을 정리하다가, 올바른 unmanaged 리소스 청소법을 위한 '지저분하기 짝이 없는, 그러나 반드시 알아야 하는' Finalize, IDisposable에 다다랐는데, 흥미롭게도 C# 쪽 pattern과 C++/CLI 쪽 pattern이 (적어도 표면 상으로는) 완연히 다르다는 사실을 발견.
본 사항은 정상적 application 구현을 위해서는 반드시 숙지해야 할 내용인데, 언어 별로 그리도 달라서야 원. 게다가,
C++/CLI 쪽 MSDN 설명은 뭔가 하나 빠진 듯 하여 다 읽고 나서도 제대로 이해가 가질 않는다. 언어 별로 따로 익혀야 하는 것도 거시기한데, 설명이라도 제대로 해야지.
먼저, C#쪽 pattern. MSDN에 떡하니 올라와 있는 정형화된 pattern이다.
// 기반 클래스에서의 구현 pattern
public class Base: IDisposable
{
public void Dispose()
{
Dispose(true);
// GC가 Finalize를 호출하지 않도록 함(중복호출 배제)
GC.SuppressFinalize(this);
}
// disposing 플래그를 통해 Finalize에서 managed 리소스
// 를 정리하지 않도록(해당 리소스는 GC가 정리할 것임)
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Managed 리소스 정리
}
// Unmanaged 리소스 정리
}
// C# 소멸자. Finalize 메서드임
~Base()
{
// 단순히 Dispose(false).
Dispose (false);
}
}
// 파생 클래스에서의 구현 pattern
public class Derived: Base
{
protected override void Dispose(bool disposing)
{
if (disposing)
{
// managed 리소스의 정리
}
// Unmanaged 리소스 정리
// 부모 개체의 리소스를 정리하도록
base.Dispose(disposing);
}
// 파생 클래스에서는 소멸자 정의를 하지 않음(부모 소멸자
// 에서 재정의된 Dispose를 호출할 것이므로)
}
암만 봐도 복잡하기 짝이 없는 패턴. 하지만 이보다 더 단순한 패턴을 내 머리로 만들어낼 궁리는 안한다(나올 가능성도 거의 없겠지만). 다음은 상기 사항에 대한 C++/CLI 쪽 pattern. 이 역시 MSDN에 명시된 내용이다.
ref class A {
// Dispose()에 해당하는 소멸자. delete를 통해 명시적 호출 가능.
// Native C++의 가상 소멸자와 동일한 행동 양식
// (스택 기반 semantic 개체 생성 시, 자동 호출됨)
~A() {
// managed 리소스 제거
// ...
// finalizer를 통한 unmanaged 리소스 제거
this->!A();
}
// Finalize에 해당하는 Finalizer
!A() {
// unmanaged 리소스 제거
}
};
MSDN에는 몇몇 설명으로 위 C++/CLI의 패턴을 설명하다 마무리 짓는데, 상당히 난감해진다. 패턴은 왜 달라지는지, 달라지면서 없어진
GC.SupressFinalize(),
Dispose(bool)은 어디로 갔는지 등에 대한 설명은 없거나 부실하다. 게다가 파생 클래스에 대한 언급은 아예 없어 과연 위 내용이 올바른 내용인가하는 의심까지 들 정도.
다음은 위 패턴에 대한 MSDN에 없는 내용으로서, 이와 같은 의문을 해소할 key가 되는 사항이다(C++/CLI의 기본 개념에 대해서는 MSDN 및 C++/CLI 소개글(
번역문 링크) 참조).
1. C++/CLI에서의 소멸자는 virtual 키워드가 없더라도 무조건 가상 함수이다.
2. finalizer의 가시성 범위는 accessor가 있건 없건 private이다.
3. destructor와 finalizer가 IDisposable::Dispose와 Finalize()를 완전 대체하지는 않는다. 컴파일러는 IL 코드 내에 Dispose()와 Finalize()를 따로 삽입하며, 각기 내부에서 destructor와 finalizer를 적절히 호출한다.
4. destructor가 호출되면 finalizer는 호출되지 않는다. 이는 IL 코드 내 Dispose() 구현에서 GC.SupressFinalizer()를 호출하기 때문이다.
5. 위 코드를 기반으로 한 컴파일된 IL 코드는 C# 버전과 거의 흡사하다(Dispose(bool)을 통한 파생 클래스에서의 리소스 정리 등).
6. 위와 같은 내용을 기반으로, 파생 클래스에서 역시 위 패턴과 동일하게 작성하면 된다(destructor 또는 Finalizer 등에서 base 클래스의 destructor/finalizer 명시적 호출 등 부가적 행동 불필요).
위의 결론은 김형준님의
C++/CLI의 Dispose Pattern에 대한 고찰과 유사한 실험 및 생성된 IL 코드 분석을 통해 이루어졌다.