본문 바로가기
C#

[C#] IDisposable (Dispose), 종료자(Finalizer)

by DANEW 2023. 10. 10.

IDisposable 인터페이스

https://learn.microsoft.com/ko-kr/dotnet/api/system.idisposable?view=net-7.0 

 

IDisposable 인터페이스 (System)

관리되지 않은 리소스 해제를 위한 메커니즘을 제공합니다.

learn.microsoft.com

namespace System
{
    public interface IDisposable
    {
        void Dispose();
    }
}

- C#의 using문은 IDisposable을 구현하는 개체에 대해
  Dispose 메서드를 try/finally 블록을 이용해서 호출하는 코드를 단축 표기

// 컴파일러는 이 코드를
using (FileStream fs = new FileStream("myfile.txt", FileMode.Open))
{
    // ...
}
 
// 이렇게 바꾸어서 컴파일 한다
FileStream fs = new FileStream("myfile.txt", FileMode.Open);
try
{
 
}
finally
{
    if(fs != null)
    {
        ((IDisposable)fs).Dispose();
    }
}

- 예외가 발생하거나 기타 이유로 try 블록을 벗어날 때에도 반드시 Dispose 메서드가 호출됨

Dispose
- 특별한 표준안이나 명세는 없으나 사실상의 표준이 존재
 1. 처분된 객체는 다시 살릴 수 없음
  * 처분된 객체의 메서드(Dispose 제외), 속성을 호출하면 ObjectDisposedExcetion 예외 발생
 2. 한 객체에 대해 Dispose를 여러번 호출 가능
 3. IDispose X가 IDispose Y를 소유한 경우, X의 Dispose는 Y의 Dispose를 자동으로 호출

Close
- 역시 특별한 표준안은 없으나 아래 둘 중 하나의 기능을 수행함
 1. Dispose와 정확히 동일한 기능
 2. Dispose 기능의 일부

Stop
- Dispose와 비슷하지만 Start로 작동을 다시 시작시킬 수 있음

Dispose를 호출해야 할 때
- 거의 모든 경우에서 의심스럽다면 사용해서 처분하는 것이 안전함
- 가끔 처분하지 말아야 할 상황
 1. 현재 코드가 객체를 소유하지 않을 때. 정적 필드나 속성을 통해서 공유 객체를 얻은 경우
  ex) Brushes.Blue, Font.FromHdc 등을 통해 얻은 객체들
 2. 객체의 Dispose가 상황에 맞지 않는 작업을 수행 할 경우
  ex) MemoryStream: 나중에 스트림을 읽거나 써야 할 경우
        StreamReader, StreamWriter: 스트림을 계속 열어두고 싶을 때(Flush를 호출하여 비워줘야 함)
        IDbConnection: 나중에 Open으로 연결을 다시 열고 싶을 때(Close를 호출하여 닫아줘야 함)
        DataContext: 게으르게 평가되는 질의가 문맥에 연결되어 있을 가능성이 있을 경우

 3. 객체의 설계 차원에서 Dispose가 꼭 필요한 것이 아닌 경우, 객체를 처분하려면 프로그램이 쓸대없이 복잡해질 때
  * 기반 클래스가 처분 가능이라서 처분 가능 형식이 된 것일 뿐, 본질적인 마무리 작업을 하지 않음
  ex) WebClient, StringReader, StringWriter, BackgroundWorker 등

반응형


명시적 선택 기반 처분
- 비본질적인 작업은 옵션으로 본질적인 작업은 항상 진행 가능하도록 설정

public sealed class HouseManager : IDisposable
{
    public readonly bool checkMailOnDispose;
 
    public HouseManager(bool checkMailOnDispose)
    {
        this.checkMailOnDispose = checkMailOnDispose;
    }
 
    public void Dispose()
    {
        if (checkMailOnDispose)
        {
            CheckTheMail();
        }
        LockTheHouse();
    }
 
    void CheckTheMail()
    {
        // ...
    }
 
    void LockTheHouse()
    {
        // ...
    }
}

- 이렇게하면 소비자는 고민 없이 항상 Dispose를 호출 할 수 있음
 ex) DeflateStream - public DeflateStream(Stream stream, CompressionLevel compressionLevel, bool leaveOpen);

- 이 패턴을 따르지 않은 예(4.5 이전)
 * StreamReader, StreamWriter - Flush 사용
 * CryptoStream - FlushFinalBlock 사용

처분 시 필드 비우기
- 일반적으로 Dispose 메서드에서 객체의 필드들을 비울 필요는 없음
- 객체가 암호화 키 같은 고가의 비밀을 담는다면 처분 도중에 필드 내용을 비우는 것이 좋음
- 내부적으로 등록한 이벤트들의 구독을 해제하는 것은 좋은 습관
- 객체가 처분되었음을 뜻하는 필드를 두고 Dispose에서 그 필드를 설정하는 것이 좋음
  (이렇게 하면 처분된 객체에 대해 멤버 함수를 호출하려 할 때 ObjectDisposedException을 던질 수 있음)
- 누구나 읽을 수 있는 자동 속성을 두는 것도 좋음 (ex) IsDisposed)
- 객체 자신의 이벤트 처리부들을 Dispose 메서드에서 비우는것도 좋음

종료자 (Finalizer; 종결 함수)
- 객체 종료자가 있다면 객체의 메모리가 해제되기 전에 종료자가 호출 됨

class Test
{
    ~Test()
    {
        // 종료자 코드
    }
}

- 진행 순서
 1. 종료자가 없는 객체는 즉시 삭제, 종료자가 있다면 특별한 대기열에 넣음
 2. 대기열에 넣는 작업이 끝난다면 한 번의 쓰레기 수거 주기가 끝나는 것이고,
    프로그램의 실행과 동시에 종료자 스레드를 만듬
 3. 종료자 스레드는 프로그램과 병렬로 대기열에서 객체를 뽑아 종료자 메서드를 실행
  * 종료자 메서드가 실행되지 않은 객체는 여전히 살아 있는 것으로 간주됨
 4. 종료자 메서드가 실행되면 객체는 버림 받은 상태가 되며, 다음번 쓰래기 수거에서 삭제됨

- 종료자 사용에서 염두에 둬야 할 점
 1. 종료자 때문에 메모리 할당과 쓰레기 수거가 느려짐 (종료자 존재 여부 및 실행 여부 추적 비용)
 2. 종료자는 객체와 그 객체가 참조하는 모든 객체의 수명을 필요 이상으로 늘림 (실제 삭제는 다음번 GC 때이므로)
 3. 여러 객체들에 대해 종료자가 호출 될 순서를 예측 할 수 없음
 4. 종료자가 실행되는 시점을 프로그래머가 거의 제어 할 수 없음
 5. 종료자 안에서 코드 실행이 차단되면 나머지 종료자들이 호출되지 못함
 6. 응용 프로그램이 정상적으로 종료되지 않으면 종료자들이 호출되지 않을 수 있음
 7. 객체를 생성하는 도중에 예외가 발생해도 그 객체의 종료자가 호출 될 수 있음
   (객체의 필드들이 정확히 초기화 되지 않았다고 가정하는 것이 좋음)

- 종료자 구현시 따를 만한 지침
 1. 종료자의 실행은 빠르게 끝나도록
 2. 종료자 않에서 코드 실행이 차단되지 않도록 함
 3. 종료자 안에서 다른 종료 가능 객체를 참조하지 않음
 4. 예외 던지지 말기

종료자에서 Dispose 호출
- 흔히 쓰이는 패턴: 종료자 안에서 Dispose를 호출
 * 자원 해제와 메모리 해제의 결합도가 올라감
 1. 객체의 정리 작업이 그리 급하지 않을 때
 2. Dispose를 호출 해서 정리 작업을 촉진하는 것이 꼭 필요해서보다는 일종의 최적화를 위한 것을 때 적합
 3. 소비자가 Dispose 호출을 까먹는 경우의 대비책
  * 단, 프로그래머가 나중에라도 버그를 고칠 수 있도록 Dispose 호출 누락 사실을 로그에 기록하는 것이 좋음

class Test : IDisposable
{
    public void Dispose()
    {
        // 사용자가 직접 Dispose한 경우 true로 넘겨줌
        Dispose(true);
 
        // 추후 GC가 이 객체를 거둬 갈 때 종료자의 실행을 방지
        // 꼭 필요하진 않지만 이렇게 넣어주면 성능이 향상 됨
        GC.SuppressFinalize(this);
    }
 
    // 실제로 처분을 진행하는 메소드
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // 이 인스턴스가 소유한 객체들에 대해 Dispose를 호출
            // 다른 종료 가능 객체들을 참조해도 괜찮음
        }
 
        // 이 객체가(객체만) 소유하고 있는 비관리 자원들을 해제                
        // 1. OS 자원들(ex: P/Invoke로 Win32 API를 호출해서 얻은)에 대한 모든 직접 참조 해제
        // 2. 생성 시 만든 임시 파일을 삭제
        //
        // try/catch 블록을 감싸서 예외 처리를 철저히 해야하고,
        // 가능하다면 간단하고 안정적인 방법으로 로그에 기록                
    }
 
    ~Test()
    {
        // 종료자에 의해서 Dispose가 호출되는 경우 false를 돌려 줌
        Dispose(false);
    }
}

 

반응형