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

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

by 코모's 2022. 10. 5.
반응형

제약 조건은 두 가지 역할을 한다.

 

1. 런타임 오류가 발생할 가능성이 있는 부분을 컴파일타임 오류로 돌릴 수 있다.

2. 타입 매개변수로 사용할 수 있는 타입을 명확히 규정하여 사용자에게도 도움이 된다.

 

대부분의 경우에 타입 매개변수로 지정하는 타입이 제약 조건을 통해 요규하는 작업 외에 다른 작업을 추가로 할 수있는지에 대해선 신경 쓰지 않는다.

하지만 타입 매개변수로 지정하는 타입이 IDisposable을 구현하고 있다면 특별한 추가 작업이 반드시 필요하다.

 

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

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() 메서드를 수행한다.

 

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

 

위의 경우보다 더 복잡한 경우다. 이 경우에는 제네릭 클래스에서 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 calss EngineDriver<T> where T : Engine
{
	//null로 초기화 된다.
    private T driver;

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

 

정리

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

 

반응형