C#] IDisposal Interface 구현 방법
IDisposal interface는 관리코드에 리소스 정리 툴을 제공한다. IDisposal을 구현하므로, object에 의해 할당된 관리하지 않는 리소스를 release할 수 있다.
IDisposable interface의 사용목적
주로 리소스를 시기적절하게 release하는데 사용된다.
.NET Garbage Collector는 사용하지 않는 변수를 예상치 못하게 releasing처리 하므로 메모리를 잘 관리한다. GC가 메모리 정리를 하기 전에 메모리가 부족할 수 있다. 따라서 수동으로 메모리 정리 처리를 해줘야 한다.
이러한 리소스는 주로 파일처리, DB, 네트워크 작업과 관련되어 있다.
리소스 부족으로 성능 이슈가 생기지 않도록, 리소스를 release시켜야 한다.
IDisposable 기본 구현
대부분의 경우, IDisposal interface를 직접 구현할 수 있다. IDisposable을 구현하려면, 반드시 Dispose() 함수를 제공해야 한다. 함수 안에 사용하지 않는 리소스를 releasing하는 로직을 작성해야 한다.
Garbage Collector의 메모리 정리를 기다리지 않고, 리소스를 정리해야 한다.
MyClass는 dispose 해야하는 field를 갖고 있으므로 IDisposal interface를 구현한다.
public sealed class MyClass : IDisposable
{
private readonly IManagedResource _managedResource;
public void Dispose()
{
Console.WriteLine($"Called {nameof(MyClass)}.{nameof(Dispose)}");
_managedResource.Dispose();
}
}
구현해야 하는 함수는 Dispose()하나이며, _managedResource object에서 Dispose()함수를 호출한다. sealed class에서 이 버전이 잘 작동한다.
살짝 더 개선해보자면, 우리는 리소스를 딱 한번 dispose해야 한다.
public sealed class MyClass : IDisposable
{
private bool _disposed = false;
private readonly IManagedResource _managedResource;
public void Dispose()
{
Console.WriteLine($"Called {nameof(MyClass)}.{nameof(Dispose)}");
if (_disposed)
{
return;
}
_managedResource.Dispose();
_disposed = true;
}
}
_disposed field를 추가하고 이미 해당 object가 disposed 되었는지 확인한다.
Dispose Pattern
기본 패턴 구현은 다음과 같다.
public class DisposeExample : IDisposable
{
private bool disposed = false; // Flag to detect redundant calls
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Dispose managed state (managed objects).
}
// Free unmanaged resources (unmanaged objects) and override a finalizer below.
// Set large fields to null.
disposed = true;
}
}
// Override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
~DisposeExample()
{
Dispose(false);
}
}
위의 Dispose()함수에서 Dispose(true)를 호출하여 managed, unmanaged resource를 모두 해제하고, GC.SuppressFinalize(this)를 호출하여 GC에게 이 object가 정리되었음을 알리고, 차후에 finalizer를 호출할 필요가 없음을 알린다.
프로젝트가 커지면서 더 많은 상속을 제공한다. IDisposal interface를 더 일반적으로 사용할 수 있도록 만든다.
IManagedResource interface를 만든다.
public interface IManagedResource : IDisposable
{
}
ManagedResource class를 만든다.
public class ManagedResource : IManagedResource
{
public void Dispose()
{
Console.WriteLine($"Called {nameof(ManagedResource)}.{nameof(Dispose)}");
}
}
MyParentClass라는 새로운 class를 만든다.
public class MyParentClass : IDisposable
{
private bool _disposed = false;
private readonly ManagedResource _parentManagedResource;
public MyParentClass(IManagedResource parentManagedResource)
{
_parentManagedResource = parentManagedResource;
}
public void DoSomething()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MyParentClass));
}
Console.WriteLine($"Called {nameof(MyParentClass)}.{nameof(DoSomething)}");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
Console.WriteLine($"Called {nameof(MyParentClass)}.{nameof(Dispose)}");
if (_disposed)
{
return;
}
if (disposing)
{
_parentManagedResource.Dispose();
}
_disposed = true;
}
}
여기서 우리는 disposing 로직을 child classes에서 override할 수 있는 Dispose() virtual method로 분리한다. disposing parameter를 전달 받으며, true이면 관리 리소스를 dispose한다. public 함수 안에서 호출하면 실행된다. 이렇게하므로, IDisposable interface를 성공적으로 구현할 수 있다.
child class를 구현해보자.
public class MyChildClass : MyParentClass
{
private bool _disposed = false;
private readonly ManagedResource _childManagedResource;
public MyChildClass(IManagedResource parentManagedResource, IManagedResource childManagedResource) : base(parentManagedResource)
{
_childManagedResource = childManagedResource;
}
protected override void Dispose(bool disposing)
{
Console.WriteLine($"Called {nameof(MyChildClass)}.{nameof(Dispose)}");
if (_disposed)
{
return;
}
if (disposing)
{
_childManagedResource.Dispose();
}
_disposed = true;
base.Dispose(disposing);
}
}
상속과 IDisposable에 관한 포인트는 자식 클래스에서 특정 리소스를 dispose하기 위해 상속받은 Dispose 함수를 override해야 한다. 또한, 부모 클래스의 Dispose()함수를 호출하므로 상속관계의 모든 리소스를 적절히 release한다. 마지막으로, 부모 class와 자식 class의 private _disposed field를 set하므로, 이미 disposed된 리소스를 확인할 수 있도록 한다.
이를 Dispose Pattern이라 한다. 상속관계의 클래스들이 각 클래스의 리소스를 정리하게 하므로, 리소스를 적절하고 효율적으로 dispose하도록 한다.
IDisposable 자동처리
시스템이 자동으로 Dispose() 함수를 호출할 수 있을까? 답은 아니다.
.NET GC가 효율적을 메모리를 관리하는 반면, IDisposable interface의 Dispose()함수는 자동으로 호출하지 않는다. 그러므로, 우리는 using statement를 사용하거나 반드시 Dispose()함수를 호출해야 한다.
확인하기 위해, Program class에 코드를 추가해서 실행해보자.
Console.WriteLine("Dispose called for the whole hierarchy...");
var parentManagedResource = new ManagedResource();
var childManagedResource = new ManagedResource();
//1. Dispose() 직접 호출방법
var childClass = new MyChildClass(parentManagedResource, childManagedResource);
childClass.DoSomething();
childClass.Dispose();
//2. using 사용방법
using var childClass = new MyChildClass(parentManagedResource, childManagedResource);
childClass.DoSomething();
//output
//Dispose called for the whole hierarchy...
//Called MyParentClass.DoSomething
//Called MyChildClass.Dispose
//Called ManagedResource.Dispose
//Called MyParentClass.Dispose
//Called ManagedResource.Dispose
대부분 우리는 using 구문을 사용하길 선호한다. Dispose()함수를 호출하기 않아서 자동 실행되기 때문이다.
부모 자식 클래스의 관리 리소스를 모두 release하려면 Dispose() 함수가 여러번 실행되어야 한다.