[Effective C# 21장] 타입 매개변수가 IDisposable을 구현한 경우를 대비하여 제네릭 클래스를 작성하라

1. 참조

2. 제약조건의 2가지 역할

  • 런타임 오류가 발생할 가능성이 있는 부분을 컴파일타임 오류로 돌릴 수 있습니다.
  • 타입 매개변수로 사용할 수 있는 타입을 명확히 규정하여 사용자에게도 도움이 됩니다.

대부분의 경우에 타입 매개변수로 지정하는 타입이 제약 조건을 통해 요구하는 작업 외에 다른 작업을 추가로 수행할 수 있는지에 대해선 신경 쓰지 않습니다. 하지만 타입 매개변수로 지정하는 타입이 IDisposable을 구현하고 있다면 특별한 추가 작업이 반드시 필요합니다.

3. 제네틱 메서드 내에서 타입 매개변수로 주어지는 타입을 이용하여 인스턴스를 생성할 경우

public interface IEngine
{
    void DoWork();
}

public class EngineDriverOne<T> where T : IEngine, new()
{
    public void GetThingsDone()
    {
        T driver = new T();
        driver.DoWork();
    }
}
  • 위 예제에서는 T가 IDisposable을 구현한 타입이라면 리소스 누수가 발생할 수 있습니다.
  • 따라서, T 타입으로 지역변수를 생성할 때 마다 T가 IDisposable을 구현하고 있는지 체크하고, 구현하고 있다면 추가적인 처리가 필요합니다.
public void GetThingsDone()
{
    T driver = new T();
    using(driver as IDisposable)
    {
        driver.DoWork();
    }
}
  • 위처럼 코드를 작성하면 컴파일러는 IDisposable로 형변환된 객체를 저장하기 위해서 숨겨진 지역변수를 생성합니다.
  • 만약, IDisposable을 구현하지 않았다면 값이 null이 됩니다.
  • C# 컴파일러는 이 지역변수의 값을 null 체크를 수행합니다.
  • 그리고 null이 아니라면 IDisposable이 구현되었다고 확인하고 using 블록을 종료할 때, Dispose() 메서드를 수행합니다.
  • 이는 매우 단순한 관용 패턴입니다. 타입 매개변수로 주어진 타입을 이용하여 인스턴스를 생성한다면, 반드시 앞에서와 같이 using문을 사용해야 합니다.
  • 또한, 해당 타입이 IDisposable을 구현했을지 알 수 없으므로 반드시 앞에서와 같은 형변환 코드가 필요합니다.

4. 타입 매개변수로 전달한 타입을 이용하여 멤버 변수를 선언한 경우

  • 3번의 경우보다 더 복잡한 경우 입니다.
  • 이 경우에는 제네릭 클래스에서 IDisposable을 구현하여 해당 리소스를 처리해야 합니다.
public sealed class EngineDriver2<T> : IDisposable where T : IEngine, new()
{
    // 생성 작업이 오래 걸릴 수도 있으므로, Lazy를 이용하여 초기화 진행
    private Lazy<T> driver = new Lazy<T> (() => new T());

    public void GetThingsDone() => driver.Value.DoWork();

    // IDisposable 멤버
    public void Dispose()
    {
         if (driver.IsValueCreated)
        {
            var resource = driver.Value as IDisposable;
            resource?.Dispose();
        }
    }
}
  • 먼저 IDisposable 인터페이스를 구현했고, 두 번째로 클래스에 sealed를 추가했습니다.
  • 이처럼 sealed 선언하면 표준 Dispose 패턴을 모두 구현할 필요가 없습니다.
  • 마지막으로 이 클래스는 코딩된 것처럼 driver 변수에 대해 Dispose() 메서드를 한 번만 호출한다고 보장하지 않습니다.
  • IDisposable을 구현하는 모든 타입은 Dispose() 메서드를 여러 번 호출하는 경우에도 문제없이 동작하도록 구현해야 합니다.
  • 하지만 위의 코드도 너무 복잡해 보입니다.
  • 만약, 제네릭 클래스의 복잡한 설계를 피하고 싶다면 Dispose 호출의 책임을 제네릭 클래스 외부로 넘기고, 객체의 소유권을 제네릭 클래스 외부로 옮기면 new() 제약조건을 제거할 수 있습니다.
    아래 코드가 Dispose 호출 책임과 객체 소유권을 외부로 옮겼을 때 제네릭 클래스의 구현한 것입니다.
public sealed class EngineDriver<T> where T : IEngine
{
    // null로 초기화된다.
    private T driver;

    public EngineDriver(T driver)
    {
        this.driver = driver;
    }

    public void GetThingsDone()
    {
        driver.DoWork();
    }
}

5. 정리

  • 제네릭 클래스의 타입 매개변수로 객체를 생성하는 경우 이 타입이 IDisposable을 구현하고 있는지 확인해야 합니다.
  • 항상 방어적으로 코드를 작성하고 객체가 삭제될 때 리소스가 누수되지 않도록 주의해야 합니다.
  • 혹은 코드를 완전히 수정하여 타입 매개변수로 객체를 생성하지 않도록 응용프로그램의 구조를 변경할 수도 있습니다.
  • 그렇게 하고 싶지 않다면, 타입 매개변수로는 지역변수 정도만을 생성하도록 코드를 작성합니다.
  • 타입 매개변수로 멤버변수를 선언해야 하는 경우라면 지연 생성을 사용해야 할 수도 있고, 제네릭 클래스에서 IDisposable을 구현해야 할 수도 있습니다.
728x90

이 글을 공유하기

댓글

Designed by JB FACTORY