본문 바로가기
C#

왜 IDisposable을 구현하나요?

by 공부봇 2025. 9. 19.
반응형

핵심 한 줄 요약

**IDisposable = “내가 잡고 있는 외부 자원(파일 핸들, GDI, 스레드/Task/Timer 등)을 사용이 끝나면 즉시, 확정적으로 해제할 수 있게 해주는 계약(패턴)”**입니다.
GC는 메모리만 회수하지 OS 자원(파일·소켓·GDI·스레드 핸들 등)은 즉시 정리하지 못하므로, 이런 자원을 가진 타입은 반드시 IDisposable을 구현해야 해요.


왜 IDisposable을 구현하나요?

  1. 결정적(Deterministic) 해제
    • using (...) { ... } 블록이 끝나는 즉시 Dispose()가 호출되어 OS 핸들/타이머/스레드/그래픽 자원 등을 반납합니다.
    • GC는 언제 실행될지 몰라요(비결정적). 파일 잠금·GDI 핸들 같은 건 지연 해제되면 곧바로 문제가 납니다.
  2. 리소스 누수/잠금 방지
    • 예: Bitmap, Graphics, FileStream, SqlConnection, Timer… 모두 IDisposable.
    • Dispose()를 안 하면 파일이 잠긴 채로 남고(“다른 프로세스에서 사용 중…”), GDI 핸들 고갈, 소켓/스레드 핸들 누수 등으로 앱이 불안정해집니다.
  3. 정상 종료·취소 절차 수행
    • CancellationTokenSource.Cancel(), 백그라운드 루프 Task 대기(Wait()), 이벤트 구독 해제, 타이머 정지 등 정리 코드를 한곳에서 표준적으로 실행.
  4. 호출측 개발자 경험(UX) 표준화
    • using (var sim = new Simulation(...)) { ... }처럼 언어 차원의 안전 가드를 활용할 수 있습니다.

Simulation에서 특히 필요한 이유

당신의 Simulation은 다음 관리되는(=IDisposable) 자원을 잡습니다:

  • FixedBitmap[] : 내부적으로 GDI+ 버퍼를 잡고 있을 가능성 높음 → 반드시 Dispose
  • CancellationTokenSource : Dispose 필요
  • 백그라운드 루프용 Task : 취소·대기 후 정리 필요
  • (원본 코드 기준) Bitmap을 열었다면 즉시 using으로 닫거나, 보관한다면 Dispose 필요

따라서 Simulation이 IDisposable을 구현해

  • 취소 신호 전송 → 루프 종료 대기 → 토큰/타스크/이미지들 순서대로 해제
    를 책임지는 게 맞습니다.

언제 IDisposable을 구현(=“상속”)해야 하나?

반드시 구현해야 하는 경우

  • IDisposable을 구현한 타입필드로 보관할 때
    • 예: Bitmap, Image, Graphics, FileStream, SqlConnection, Timer, HttpClient(주의: 재사용 권장), CancellationTokenSource 등
  • OS/네이티브(비관리) 자원을 직접 소유할 때
    • 예: 파일/소켓 핸들, GDI 핸들, Unmanaged 메모리 포인터
    • 가능하면 SafeHandle로 감싸고, 그래도 소유하면 Dispose에서 반납
  • 주기 동작/백그라운드 작업을 구동할 때
    • 예: Task 루프, Thread, Timer → 중지·조인·Dispose

구현을 권장하는 경우

  • 이벤트 구독을 내부에서 하고구독 해제를 보장해야 할 때
    • 이벤트 퍼블리셔가 구독자를 강한 참조로 들고 있으면, 구독 해제하지 않는 한 메모리 회수 안 됨 → Dispose()에서 -=로 해제
  • 큰 버퍼/캐시를 들고 있어 명시적 수명 제어가 필요할 때

구현이 불필요한 경우

  • 순수 관리 메모리만 들고 있고, IDisposable 멤버가 전혀 없는 간단한 DTO/POCO(예: 순수한 데이터만 가진 Person, Point 등)

구현하지 않으면 무엇이 틀어지나요?

  • 파일 잠금 지속: “The process cannot access the file because it is being used by another process.”
  • GDI+ 예외: “A generic error occurred in GDI+” (핸들 고갈/잠금의 후폭풍)
  • 리소스 고갈/성능 저하: 소켓/스레드/타이머 핸들이 쌓여 OutOfMemoryException이나 핸들 고갈
  • 앱 종료 전까지 유지: GC가 언젠가 치워주더라도 그 시점은 알 수 없음 → 운영 중 문제
  • 이벤트 메모리 릭: 구독 해제 안 하면 퍼블리셔가 구독자를 붙잡아 GC 불가

.NET Framework 4.8.1 / C# 7.3에서의 표준 Dispose 패턴 (sealed 클래스)

sealed이면 상속을 고려할 필요가 없으니 간단 패턴을 써도 됩니다.

public sealed class Simulation : IDisposable
{
    private bool _disposed;
    private CancellationTokenSource _cts;
    private Task _loopTask;
    private FixedBitmap[] _images;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // 파이널라이저가 없더라도 관례상 OK
    }

    // sealed라면 private로 해도 무방
    private void Dispose(bool disposing)
    {
        if (_disposed) return;
        _disposed = true;

        if (disposing)
        {
            // 1) 동작 중지
            if (_cts != null)
            {
                try
                {
                    _cts.Cancel();
                    if (_loopTask != null)
                    {
                        try { _loopTask.Wait(); } catch (AggregateException ae)
                        {
                            if (!(ae.InnerExceptions.Count == 1 && ae.InnerException is TaskCanceledException))
                                throw;
                        }
                        finally
                        {
                            try { _loopTask.Dispose(); } catch { }
                            _loopTask = null;
                        }
                    }
                }
                finally
                {
                    try { _cts.Dispose(); } catch { }
                    _cts = null;
                }
            }

            // 2) 이미지 해제
            if (_images != null)
            {
                for (int i = 0; i < _images.Length; i++)
                {
                    try { _images[i].Dispose(); } catch { }
                }
                _images = null;
            }

            // 3) 이벤트 구독 해제/타이머 Dispose 등 추가 자원 정리
        }

        // (비관리 자원이 있다면 여기서 해제.
        //  하지만 보통 SafeHandle로 감싸고, 그 SafeHandle을 disposing 분기에서 Dispose)
    }
}

  • 여러 번 호출해도 안전하도록 _disposed 플래그로 멱등성 보장
  • Dispose() 이후 공개 메서드가 호출되면 **ObjectDisposedException**을 던지는 것이 바람직
  • 파이널라이저(~Simulation)는 **“진짜 비관리 자원”**을 직접 잡고 있을 때만 고려(대부분은 SafeHandle + Dispose로 충분)

호출 측 사용법

using (var sim = new Simulation(/*...*/))
{
    sim.Initialize();
    sim.GrabStart();

    // 사용...

    sim.GrabStop();
} // 여기서 Dispose 자동 호출 → 자원 즉시 반납

정리

  • IDisposable은 OS/외부 자원을 다루는 클래스의 수명 관리 계약이에요.
  • Simulation처럼 Bitmap/FixedBitmap/CTS/Task 등을 보유하는 타입은 반드시 IDisposable을 구현해야 안전합니다.
  • 구현하지 않으면 파일 잠금·핸들 누수·성능 저하 등 운영 문제가 발생할 수 있어요.
  • .NET Fx 4.8.1/C# 7.3에선 위의 간단 Dispose 패턴을 쓰면 충분합니다.
반응형