본문 바로가기
C#/Effective C#

[Effective C#] Item17 표준 Dispose 패턴을 구현하라

by 코모's 2022. 9. 13.
반응형

이번 포스팅은 메모리가 아닌 다른 유형의 비관리(Unmanaged) 리소스를 포함하는 타입을 작성할 때 리소스 관리를 어떻게 해야 할지를 살펴보는 파트다.

우선 간단하게 비관리 리소스가 무엇인지 살펴보자.

 

비관리 리소스 : 메모리가 아닌 자원을 말하며, 윈도우 핸들, 파일 핸들, 소켓 핸들 등 시스템 자원을 뜻한다. 반대로 관리 리소스에는 new List<string>()등, 메모리 처럼 쓰는 자원을 말한다.

 

이런 비관리 리소스들은 가비지 콜렉터가 아닌 개발자가 직접 관리해줘야 한다. 이미 .NET 프레임 워크에는 비관리 리소스를 정리하는 표준화 된 패턴을 사용하고 있다.

그것이 바로 Dispose 패턴이다.

 

Dispose 패턴 왜 사용해야 되냐?

우선 Dispose 패턴은 앞서 말했듯 비관리 리소스를 관리하기 위한 패턴이다.

이 패턴을 이용하면 개발자들에게 IDisposable 인터페이스를 통해서 리소스를 삭제할 수 있는 기능을 안정적으로 제공할 수 있다.

또, 비관리 리소스를 명시적으로 정리해야 한다는 사실을 잊어버리거나 인지하지 못한 경우에도 finalizer를 통해 올바르게 리소스가 정리될 수 있도록 해준다.

비관리 리소스를 포함하는 클래스는 finalizer는 호출하도록 해야 하지만 그런 경우가 아니라면 성능에 부정적인 영향을 미치는 것은 최소하 한다.

 

finalizer가 왜 성능에 안 좋나?

  1. finalizer를 구현한 객체는 바로 메모리에서 제거되지 않고 가비지 콜렉터가 finalizer 큐라는 곳에 이 객체들의 참조를 삽입해둔다.
  2. 이후 finalizer 스레드 라는 특별한 스레드를 이용하여 finalizer를 순차적으로 호출된다. 이 이후에 이객체들은 가비지 컬렉터에 의해서 제거될 수 있는 대상으로 간주한다.
  3. 이 대문에 다른 객체에 비해서 상대적으로 메모리에 오래 살아남는다.

즉, 메모리에 오래 남는 타입이고 한세대 높아지고 가비지 콜렉터가 finalizer 객체 검사하는과정에서 연산을 잡아먹는다.

 

IDisposable.Dispose() 메서드 수행

  1. 모든 비관리  리소스를 정리한다.
  2. 모든 관리 리소스를 정리한다.
  3. Dispose() 한 후 다시 Dispose()를 호출할 경우 문제가 없도록 객체가 이미 정리되었음을 나타내기 위한 상태 플래그 설정.
  4. Dispose() 한 후 비관리 리소스를 사용시, Objectdisposed() 예외를 발생 시켜야한다.
  5. finalizer 호출 회피. 이를 위해 GC.SupperessFinalize(this)를 호출한다.
public interface IDisposeable
{
	void DisPose();
}

Dispose() 취약점

  • 파생 클래서의 메모리 해제 후, 베이스 클래스의 메모리 해제 문제가 있다.
  • 이 경우, protected로 선언된 virtual void Dispose(bool isDisposing) 이라는 함수를 통해 해결이 가능하다.

[예제코드]

    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)
            {
                // 해당 구문에 관리 리소스 정리합니다.
            }
            
            alreadyDisposed = true;
        }

        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;
        }

 

정리

새로 작성할 타입이 비관리 리소스를 포함하거나 혹은 IDisposable을 구현한 다른 타입을 포함해야 하는경우에만 finalizer를 제한적으로 구현하면 된다.

IDisposable 인터페이스만 필요하고 finalizer를 구현할 필요가 없는 경우라 하더라도 표준 Dispose 패턴의 구조는 온전히 유지하는 것이 좋다.

그렇지 않으면 파생 클래스에서 표준 Dispose 패턴을 구현하는 것이 복잡해진다.

 

 

 

 

 

출처 - Effective C# <강력한 C# 코드를 구현하는 50가지 전략과 기법, 이펙티브>, 빌 와그너, 김명신, 한빛미디어 / https://afsdzvcx123.tistory.com/entry/Effective-C-%EA%B0%9C%EC%A0%95%ED%8C%90-3%ED%8C%90-17%EC%9E%A5-%ED%91%9C%EC%A4%80-Dispose-%ED%8C%A8%ED%84%B4%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%9D%BC

반응형