좀 긴가민가했던 .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::DisposeFinalize()를 완전 대체하지는 않는다. 컴파일러는 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 코드 분석을 통해 이루어졌다.
Posted by 어쨌건간에

댓글을 달아 주세요

  1. blueasa 2010.06.10 14:22  댓글주소  수정/삭제  댓글쓰기

    감사합니다.
    덕분에 C++/CLI와 C# 사이에서의 소멸 루틴을 조금이나마 이해하게 됐습니다. :)