Effective C# ITEM 17
표준 Dispose 패턴을 구현하라
GC의 영역 밖인 비관리 리소스는 반드시 사용자가 직접 메모리 해제 코드 작성을 해야 한다.
해제 방법은 간단하다. 아래에서 설명한 Dispose와 finalizer를 작성하여 처리해야 한다.
Dispose 패턴에 필요한 작업
Super class
1. 리소스를 정리하기 위해서 IDisposable 인터페이스를 구현해야 한다.
2. 멤버 필드로 비관리 리소스를 포함하는 경우에 한해 방어적으로 동작할 수 있도록 finailzer를 추가해야 한다.
3. Dispose와 finailzer(존재하는 경우)는 실제 리소스 정리 작업을 수행하는 다른 가상 메서드에 작업을 위임하도록 작성돼야 한다. 파생 클래스가 고유의 리소스 정리 작업이 필요한 경우 이 가상 메서드를 재정의할 수 있도록 하기 위함이다.
Derived class
1. 파생 클래스가 고유의 리소스 정리 작업을 수행해야 한다면 베이스 클래스에서 정의한 가상 메서드를 재정의 한다.
2. 멤버 필드로 비관리 리소스를 포함하는 경우에만 finailzer를 추가해야 한다.
3. 베이스 클래스에서 정의하고 있는 가상 함수를 반드시 재호출해야 한다.
핵심은 비관리 리소스를 멤버 필드로 포함하고 있다면 반드시 finalizer를 구현해야 한다.
하지만 finalizer는 GC의 finalizer queue에 객체들의 참조값을 한번 추가된 후에 수행되기 때문에 자연스레 GC의 관리 힙에 로드가 생기게 된다.
우리는 여기서 finailzer에서 생기는 로드를 최대한으로 줄이는 방법을 찾아보겠다.
class Exampleclass : IDisposable
{
bool disposed = false;
public Exampleclass(){}
protected virtual void Dispose(bool disposing)
{
if(disposed)
return;
if(disposing)
{
//관리 객체 처리
}
//비관리 객체 처리
disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SupperssFinalize(this); // 해당 객체에 대해서 GC가 가비지 수집을 하지 않도록 예약
}
public void ExampleFunc()
{
if(disposed)
throw new ObjectDisposedException("Exampleclass","Called Example Method on Disposed object");
...//Func 구현
}
//finalizer는 GC가 호출 한다.
//Client 개발자가 직접 Dispose를 호출했다면 finalizer는 호출되지 않는다.
~Exampleclass() => Dispose(false);
}
public class Progream
{
static public void Main()
{
Exampleclass ex = new Exampleclass(); // 할당...
...//사용 하는 코드
ex.Dispose(); //사용자가 호출하는 코드 사용자가 호출하지 않았다면 GC에서 Dispose(false) 자동 호출
}
}
코드 설명
1. IDisposable 인터페이스를 상속받은 Exampleclass.
2. Dispose 처리 유무를 확인하기 위한 bool disposed 멤버 변수를 false로 초기화.
3. public void Dispose 메서드 구현(IDisposable 상속 받았기 때문)
- Dispose(bool) 메서드 호출
- GC의 가비지 수집을 스킵하기위한 GC.SuppressFinalize(this)
4. protected virtual void Dispose(bool disposing) 메서드 구현(코드 작성자가 직접 해제 작업을 처리 하기 위함)
- 매게 변수 disposing을 확인
true : 비관리, 관리 리소스 삭제
false : 비관리 리소스 삭제
- disposed를 true로 변환 (삭제 되었기 때문)
5. finalizer 구현(Client 개발자가 dispose를 직접 호출하지 않을 경우를 대비하여)
- Dispose(false) 호출
Client개발자는 해당 객체를 사용하고 Dispose 메서드를 직접 호출해야한다.
Dispose메서드를 호출하지 않을 경우에는 finalizer가 호출되어 비관리 리소스가 처리 되지만 프로그램의 전체적인 속도만 느려진다.
Class 설계자는 Client개발자의 코드에서 메모리 릭을 안남기고 오로지 코드 구조에 따라 프로그램의 퍼포먼스만 영향을 준다.
다음은 해당 class에 대한 파생 클래스 코드이다.
public class DerivedExampleclass : Exampleclass
{
private bool disposed = false;
protected override void Dispose(bool disposing)
{
if(disposed)
return;
if(disposing)
{
//관리 객체 처리
...
}
//비관리 객체 처리
...
disposed = true;
//부모 class 객체 처리
base.Dispose(disposing);
}
public void DerivedFunc()
{
if(dieposed)
throw new ObjectDisposedException("Derivedclass","Called Derived Method on Disposed object");
}
//Client 개발자가 호출하지 않을시 GC에서 자동으로 호출 (파생클래스 -> 베이스 클래스 순서로 호출)
~DerivedExampleclass() => Dispose(false);
}
자 주의 할점은 base.Dispose(disposing) 이다.
그외에는 이전 Exampleclass와 같다.
코드 설명
1. DerivedExampleclass는 Exampleclass를 상속.
2. DerivedExampleclass는 Exampleclass의 protected virtual void Dispose(bool disposing)을 override하여 구현
- 매게변수 disposing을 확인
true : 비관리, 관리 리소스 삭제
false : 비관리 리소스 삭제
- base.Dispose(dispose) 호출
- disposed를 true로 변환 (삭제 되었기 때문)
3. finalizer 구현(Client 개발자가 dispose를 직접 호출하지 않을 경우를 대비하여)
- Dispose(false) 호출
- base.Diespose(flase) 호출
파생 클래스도 슈퍼 클래스와 원리는 비슷하다. 다른점이 있다면 파생클래스는 Dispose하면서 슈퍼 클래스도 같이 호출되는 것이다. 이는 파생클래스만 Dispose 할 경우 슈퍼 클래스는 나중에 finalizer가 GC에 의해 호출되지만 퍼포먼스의 영향을 주기 때문에 함께 호출시킨 것이다.
파생 클래스도 개별적으로 disposed라는 플래그가 존재한다. 이는 파생 클래스에서 독자적으로 비관리 리소스를 가짐을 처리 하기 위해서다.
결론
1. 비관리 리소스를 사용할 경우 반드시 Dispose패턴을 사용하자.
2. 비관리 리소스를 사용할 경우 반드시 반드시 finalizer를 작성하자. (상식적으로 Dispose를 호출해야 하지만 호출하지 않는 Client 개발자를 탓할수는 없다)
3. Dispose와 finalizer는 리소스 삭제만 하자 다른 기능도 추가로 가진 경우 야근을 부른다.