[Effective C# 개정판 3판 ] 17장 표준 Dispose 패턴을 구현하라

1. 참조

2. 소개

  • 비관리 리소스 : 메모리가 아닌 자원 을 말하며, 윈도우 핸들, 파일 핸들, 소켓 핸들시스템 자원 을 뜻합니다.
  • 반대로 관리 리소스에는 new List() 등, 메모리 처럼 쓰는 자원을 말합니다.
  • 이런 비관리 리소스들은 가비지 콜렉터가 아닌 개발자가 직접 관리해줘야 합니다.
  • 이미 .NET 프레임워크에는 비관리 리소스를 정리하는 표준화된 패턴을 사용하고 있습니다.
  • 이것이 바로 Dispose 패턴 입니다.

3. Dispose 패턴 사용해야 하는 이유

  • Dispose 패턴은 비관리 리소스 를 관리하기 위한 패턴입니다.
  • 이 패턴을 이용하면 개발자들에게 IDisposable 인터페이스를 통해서 리소스를 삭제할 수 있는 기능을 안정적으로 제공할 수 있습니다.
  • 또, 비관리 리소스를 명시적으로 정리해야 한다는 사실을 잊어버리거나 인지하지 못한 경우에도 finalizer 를 통해 올바르게 리소스가 정리될 수 있도록 해줍니다.
  • 비관리 리소르를 포함하는 클래스는 반드시 finalizer 는 호출하도록 해야 하지만, 그런 경우가 아니라면 성능에 부정적인 영향을 미치는 것을 최소화합니다.

4. finalizer가 왜 성능에 안좋나?

  • finalizer를 구현한 객체는 바로 메모리에서 제거되지 않고 가비지 콜렉터가 finalizer 큐 라는 곳에 이 객체들의 참조를 삽입해 둡니다.
  • 이후에 finalizer 스레드 라는 특별한 스레드를 이용하여 finalizer를 순차적으로 호출 합니다.
  • 이 객체들은 가비지 콜렉터에 의해서 제거될 수 있는 대상으로 간주합니다.
  • 때문에, 다른 객체에 비해서 상대적으로 메모리에 오래 살아 남습니다.
  • 즉, 메모리에 오래 남는 타입이고 한 세대 높아지고 가비지 콜렉터가 finalizer 객체 검사하는 과정에서 연산을 잡아 먹으면서 성능적으로 비효율적입니다.

5. IDisposable.Dispose() 메서드 수행

  • IDisposable.Dispose() 메서드는 다음 네가지 작업을 무조건 수행합니다.
    1. 모든 비관리 리소스를 정리 합니다.
    2. 모든 관리 리소스를 정리 합니다.
    3. 객체가 이미 정리되었음을 나타내기 위한 상태 플래그를 설정합니다. 앞에서 이미 정리된 객체에 대하여 추가로 정리 작업이 요청될 경우, 이 플래그를 확인하여 ObjectDisposed 예외를 발생시킵니다.
    4. finalizer 호출 회피. 이를 위해 GC.SupperessFinalize(this) 호출
public interface IDisposable
{
    void Dispose();
}

6. Dispose() 취약점

  • Dispose() 패턴도 취약한 부분이 있습니다.
  • 파생 클래스의 메모리 해제 후, 베이스 클래스의 메모리 해제 문제입니다.
  • 이 경우엔, protected로 선언된 virtual void Dispose(bool isDisposing); 이라는 함수를 통해 해결이 가능합니다.

6.1 Dispose 패턴 예제 코드

using System;

namespace Chapter17
{
    class Program
    {
        static void Main(string[] args)
        {

        }
    }

    public class BaseClass : IDisposable
    {
        // Dispose 체크 플래그 변수
        private bool alreadDisposed = false;

        // IDisposable을 구현
        // 가상 Dispose 메서드를 호출하고
        // finalize를 회피하도록 합니다.
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        // 가상 Dispose 메서드
        protected virtual void Dispose(bool isDisposing)
        {
            // Dispose 는 한 번만 수행되도록 합니다.
            if (alreadDisposed)
                return;

            if(isDisposing)
            {
                // 해당 구문에 관리 리소스 정리합니다.
            }
        }

        public void ExampleMethod()
        {
            if(alreadDisposed)
            {
                throw new ObjectDisposedException("BaseClass", "Called Example Method on Disposed Object");
            }
        }
    }

    public class DerivedClass : BaseClass
    {
        // 자신만의 dispose 플래그
        private bool disposed = false;

        protected override void Dispose(bool isDisposing)
        {
            // Dispose는 한 번만 수행되도록 한다.
            if (disposed)
                return;

            if(isDisposing)
            {
                // 여기서 관리 리소스 관리 합니다.
            }

            // 여기서 비관리 리소스 정리합니다.

            // 베이스 클래스가 자신의 리소스를 정리할 수 있도록 해주어야 합니다.
            // 베이스 클래스는 GC.SuppressFinalize()를 호출해야 합니다.
            base.Dispose(isDisposing);

            // 파생 클래스의 리소스가 정리되었음을 표시합니다.
            disposed = true;
        }
    }
}

6. 정리

  • 새롭게 작성할 타입이 비관리 리소스를 포함하거나 혹은 IDisposable을 구현한 다른 타입을 포함해야 하는 경우에만 finalizer 를 제한적으로 구현합니다.
  • IDisposable 인터페이스만 필요하고 finalizer를 구현할 필요가 없는 경우라 하더라도 표준 Dispose 패턴 의 구조는 온전히 유지하는 것이 좋습니다.
  • 온전히 유지하지 않으면 파생 클래스에서 표준 Dispose 패턴을 구현하는 것이 복잡해 지는 단점이 있습니다.
728x90

이 글을 공유하기

댓글

Designed by JB FACTORY