Dispose 패턴 두 번째 글로써 고급 기법에 대해 설명하고자 합니다. 지난 포스트에서는 Dispose 패턴의 기초적인 구현 방법을 설명했었습니다. Dispose 패턴을 구현할 때 관리되지 않는(unmanaged) 자원을 해제할 때에는 약간 더 주의를 기해야 하기 때문에 고급이란 이름으로 별도의 포스트를 작성하게 되었습니다. “나는 패 죽여도 unmanaged 자원을 직접 사용하지 않는다”라고 자신하실 수 있는 독자 분은 이 글을 읽지 않으셔도 되겠습니다. 만약 여러분이 WIN32 API를 직접 호출하여 핸들 값을 얻어내거나 하는 등의 코드를 작성하셔야 한다면 한번쯤 고민을 해 보실 필요가 있습니다.

Dispose 패턴 – 정리

필자가 지금까지 여러 차례의 포스트에서 닷넷 CLR의 가비지 컬렉션을 설명하고 Finalizer 가 무엇이며 어떻게 활용할 것이고 문제는 무엇인지 설명하였고 그에 대한 대안으로 Dispose 패턴을 설명하였다. 그리고 그 최종(?) 포스트일지도 모를 이 글을 쓰면서 Dispose 패턴에 대해 정리를 하고자 한다. 지금까지 앞서 글들을 안 읽어 보았다면 초간단 요약본이라도 쓰바 읽어 보기 바란다. 쩌는 귀차니즘을 극복하고 관련 글들에 대한 링크도 졸라 달아 놨으니 심심하면 링크 클릭해서 상세한 내용을 읽어 봐도 좋다. (젭알 읽어 보길……)

닷넷 CRL은 보다 빠르며 개발자에게 부담을 주지 않고 효율적으로 메모리를 관리하기 위해 가비지 컬렉션 메커니즘을 사용한다. 가비지 컬렉션 메커니즘은 메모리를  빠르게 할당할 수 있으며 메모리 회수를 CLR이 담당하므로 개발자가 특별히 메모리 회수에 신경쓰지 않아도 된다는 장점을 가지고 있다. 닷넷 CLR은 메모리 활용도를 최대로 올리기 위해 세대별 가비지 컬렉션 기법이나 LOH, 다양한 가비지 컬렉션 모드 와 같은 최적화를 사용하고 있지만 가비지 컬렉션이 발생하는 시점이 불분명하다는 문제를 가지고 있습니다. 이는 파일이나 데이터베이스 연결과 같이 시스템의 중요한 자원들의 해제 시점이 불분명 하다는 것이며 귀중한 시스템 자원이 더 이상 사용 되고 있지 않음에도 불구하고 아직 해제가 안 된 채로 가비지 컬렉션이 발생할 때까지 남아 있을 수 있음을 의미한다.

따라서 닷넷 프레임워크는 IDispose 인터페이스를 사용하여 시스템 자원을 포함하는 객체는 사용이 끝나면 Dispose 메서드 호출을 통해 자원을 즉시 해제할 수 있도록 하는 것이다(일부 클래스는 Dispose 대신 Close 메서드를 제공하거나 Dispose 메서드와 Close 메서드를 모두 제공하기도 한다). 모든 개발자가 using 키워드를 사용하거나 try~finally 블럭에서 해제가 필요한 객체(Connection 객체, FileStream 객체 등등)를 해제를 해준다면야 아무런 문제가 없을 것이다. 다양한 이유(실수, 날림코드, 음주 코딩, 닭대가리, 븅신 등등)에서 개발자가 Dispose 혹은 Close 메서드를 호출하지 않았을 때의 대비도 해야만 한다.

Finalizer 는 CLR에서 제공하는 특수한 메서드로써 이 메서드를 포함하는 객체가 가비지 컬렉션의 대상이 되면 이 객체에 대해 Finalize 메서드를 호출한 후에 객체를 메모리에서 제거하도록 되어 있다. 따라서 IDispose 인터페이스를 구현하는 타입이라 할지라도 개발자가 Dispose 메서드를 호출하지 않은 경우, Finalizer 에서 자원을 해제해 줌으로써 자원 누수(resource leak)를 막을 수 있다.

하지만 CLR이 객체들의 Finalize 메서드를 호출하는 순서가 정해져 있지 않기 때문에 중복으로 Dispose가 호출되는 상황이 올 수도 있다. 이 상황은 Finalizer 사용시 주의 해야 할 사항으로써 지난 포스트에서 이미 다룬 바가 있다.

결론적으로 Dispose 패턴을 다루었던 지난 포스트에서 설명한 대로 IDispose 인터페이스를 구현할 때에는 명시적으로 Dispose 메서드 호출에 의한 자원 해제인지 아니면 Finalizer에 의한 자원 해제 인지를 구별이 필요하며 그에 따른 적절한 처리가 필요하다.

다음 코드는 전형적인 Dispose 패턴 코드를 보여준다. SafeType 클래스가 내부적으로 사용하는 자원(StreamWriter)을 해제해주기 위해 IDispose 인터페이스를 구현하였고 Finalizer 역시 구현하였다. Dispose 메서드가 명시적으로 호출되면 자원을 해제한 후 이미 자원을 해제 했으므로 가비지 컬렉터가 Finalizer를 호출하지 않도록 한다(SuppressFinalize 메서드 호출). 한편, Dispose 메서드가 명시적으로 호출되지 않고 Finalize 메서드가 호출된 경우에는 자원을 해제하지 않는다. 그 이유는 SafeType 클래스가 사용하는 자원(StreamWriter)이 관리되는 자원이기 때문에 이미 이 관리되는 자원이 가비지 컬렉터에 의해 해제되었을 수도 있기 때문이다. 상세한 내용은 Finalizer에 관련된 글을 참고하기 바란다.

   1: class SafeType : IDisposable
   2: {
   3:     private StreamWriter _stream;
   4:  
   5:     public SafeType()
   6:     {
   7:         _stream = new StreamWriter("Test.txt");
   8:     }
   9:  
  10:     ~SafeType()
  11:     {
  12:         Dispose(false)
  13:     }
  14:  
  15:     public void Dispose()
  16:     {
  17:         Dispose(true);
  18:         GC.SuppressFinalize();
  19:     }
  20:  
  21:     protected virtual void Dispose(bool disposing)
  22:     {
  23:         if (disposing)
  24:         {
  25:             _stream.Dispose();
  26:         }
  27:     }
  28:  
  29:     public void DoSomething()
  30:     {
  31:         ... 생략 ...
  32:     }
  33: }

고급 Dispose 패턴

위 코드를 찬찬히 살펴보면 Finalize 메서드에서 Dispose 메서드 호출을 따라가보면 자원 해제 작업을 전혀 하지 않는다. 다시 말해서 위 코드는 Finalize 메서드를 정의하지 않아도 무방하다는 것이다. 장난하나? 그럼에도 불구하고 “Dispose 패턴”에서는 Finalize 메서드 구현을 포함하는 이유가 무엇일까?

많은 닷넷 객체들은 unmanaged 자원들을 사용한다. 위 예제 코드에서 사용한 StreamWriter 타입은 내부적으로 FileStream 객체를 사용하며 FileStream 객체에서는 WIN32 API를 통해 파일 핸들을 사용한다. 이 파일 핸들은 운영체제에서 제공하는 시스템 자원이다. 이 외에도 윈폼의 컨트롤들도 모두 예외 없이 윈도우 핸들을 사용하고 이 역시 GDI 자원으로써 unmanaged 자원이다. 이들 unmanaged 자원은 CLR에 의해 자동으로 해제되지 않는다. 따라서 이들 unmanaged 자원을 사용하는 타입들은 unmanaged 자원을 해제할 책임이 있는 것이다.

Unmanaged 자원은 CLR이 해제해 주지 않기 때문에 Dispose 혹은 Finalize 메서드 내에서 반드시 해제를 해 주어야 한다. 따라서 Dispose 패턴 내에서도 unmanaged 자원을 해제하도록 해야 할 것이다. Dispose 패턴 내에서 자원 해제는 protected Dispose 메서드에서 중앙 집중적으로 수행하므로 unmanaged 자원도 이 메서드 내에서 해제해 주어야 한다. 다음 코드는 위 코드에서 unmanaged 메모리를 할당하고 해제하는 코드를 추가한 것이다.

   1: class SafeType : IDisposable
   2: {
   3:     private StreamWriter _stream;
   4:     private IntPtr _unmanagedResource;
   5:  
   6:     public SafeType()
   7:     {
   8:         _stream = new StreamWriter("Test.txt");
   9:         _unmanagedResource = Marshal.AllocHGlobal(1024);
  10:     }
  11:  
  12:     ~SafeType()
  13:     {
  14:         Dispose(false)
  15:     }
  16:  
  17:     public void Dispose()
  18:     {
  19:         Dispose(true);
  20:         GC.SuppressFinalize();
  21:     }
  22:  
  23:     protected virtual void Dispose(bool disposing)
  24:     {
  25:         if (disposing)
  26:         {
  27:             _stream.Dispose();
  28:         }
  29:         Marshal.FreeHGlobal(_unmanagedResource);
  30:     }
  31:  
  32:     public void DoSomething()
  33:     {
  34:         ... 생략 ...
  35:     }
  36: }

생성자에서 할당한 unmanaged 힙 메모리를 해제하는 코드의 위치를 잘 살펴보자. 관리되는 자원인 StreamWriter 객체는 Dispose 메서드를 통해 직접적인 해제 시(disposing 매개변수가 true)에만 해제를 수행했었다. 그런데, unmanaged 자원은 disposing 매개변수의 값에 무관하게 해제를 하고 있음에 주목하자. 다시 말해 Finalizer 메카니즘에 의한 암시적 자원 해제와 Dispose 메서드 호출에 의한 명시적 자원 해제에 무관하게 unmanaged 자원을 해제하고 있다는 것이다.

졸라 복잡하게 느껴지겠지만, 앞서 CLR은 관리되는 자원만을 해제해 줄 뿐이라는 것을 다시 기억해 보면 간단하다. protected Dispose 메서드 내에서 Finalizer에 의한 호출인 경우 왜 관리되는 객체인 StreamWriter를 해제해 주지 않았었는지를 생각해 보자. Finalizer 메커니즘 상 관리되는 객체인 StreamWriter 객체는 이미 해제가 되었거나 앞으로 해제될 것이기 때문에 Finalize 메서드 내부에서는 Dispose를 해주면 오류가 발생한다는 것을 지난 포스트에서 이미 설명한 바 있다. 그러나 unmanaged 자원은 CLR 혹은 Finalize 메커니즘에 의해 해제되지 않기 때문에 Dispose에 의한 명시적 해제이건 Finalizer에 의한 암시적 해제이건 상관 없이 무조건 해제를 해주어야만 한다.

만약 위 코드에서 FreeHGlobal 메서드 호출이 if 절 내부에 위치한 경우를 생각해 보자. 어떤 허접한 개발자가 SafeType 객체를 깜박 잊고 Dispose 해주지 않았다면 종국에는 Finalize 메서드가 호출될 것이고 protected Dispose 메서드가 호출될 것이다. 이 때 disposing 매개변수의 값은 false 이다. 따라서 Finalizer 메커니즘에 의한 암시적인 자원 해제 시에는 unmanaged 자원이 해제되지 않게 되는 것이다. 이런 이유에서 FreeHGlobal 메서드 호출은 if 절 밖에 존재해야 하는 것이다.

고정된 코드는 없다. 적응력을 길러야…

지금까지 Dispose 패턴이 등장한 이유부터 간단한 구현 방법, unmanaged 자원을 고려한 고급 Dispose 패턴까지 졸라 자세히 살펴보았다. 여기서 중요한 점이자 필자가 빡세게 강조하고 싶은 것은 이 코드가 완벽하지 않다는 것이다. 지금까지 살펴본 코드는 말 그대로 “패턴”일 뿐이다. 항상 모든 경우에 이러한 방식으로 코드를 작성해야 하는 것은 아니라는 것이다. 다만 이 패턴의 코드의 목적과 어떤 방식으로 자원들을 해제해야 하는지 잘 이해하고 이 “패턴”을 참고하여 코드를 작성해야 한다.

자 무슨 얘기 인고 하니, 여러분은 일단 자신이 사용하는 자원이 어떤 것인지 정확히 파악할 필요가 있다. 자신이 사용하는 자원이 자신이 직접 WIN32 API를 호출하여 획득한 unmanaged 자원인지 아니면 닷넷 프레임워크에서 제공하는 관리되는 객체인지 등등을 명확히 파악해야 한다는 것이다. 자신이 사용하는 자원에 대한 정확한 파악이 끝나면 이제 “Dispose 패턴” 코드를 “참고”하여 코드를 작성하면 된다. 여러분이 작성해야 할 코드는 자신이 사용하는 자원을 효율적으로 할당 및 사용하고 해제하는 코드이어야 할 것이다.

모든 상황에 알맞은 완벽한 자원해제 코드란 없다. 때에 따라서는 “Dispose 패턴”이고 “Finalize 메서드”고 나발이고 다 필요 없는 경우가 많다(실제로 이러한 경우가 가장 많다). 닷넷 프레임워크에 의해 제공되는 타입을 사용하는 경우의 절대 다수는 Dispose 패턴이 필요할 정도로 해제를 해주어야 하는 상황이 별로 없다. 다만, 데이터베이스 연결이나 소켓(socket)과 같이 성능과 직결된 한정된 자원을 사용하는 경우에만 이 자원을 최대한 늦게 취득하고 최대한 빨리 반환하도록 코드를 작성하면 되는 것이다. 다시 말해 다양한 환경이나 상황에 알맞게 코드를 작성해야 한다는 것이며 이러한 적응력은 다양한 원리 지식에서 비롯된다.

결론은… 졸라 열심히 공부하라능……


경고 : 이 글을 무단으로 복제/스크랩하여 타 게시판, 블로그에 게시하는 것은 허용하지 않습니다.