← 문제로

8. IDisposable·파이널라이저와 Dispose 패턴 (결정적 해제와 SuppressFinalize)

난이도 중
내 리뷰 · C#
해설 · C#

해설 — IDisposable·파이널라이저와 Dispose 패턴 (결정적 해제와 SuppressFinalize)

난이도: 중

요약

C#의 자원 해제는 두 갈래다. Dispose/using 은 코드가 정한 시점에 결정적으로 정리하고, 파이널라이저(~T) 는 GC 가 객체를 수거할 때 비결정적으로 한 번 도는 안전망이다. Dispose 안의 GC.SuppressFinalize(this) 는 "이미 손으로 정리했으니 파이널라이저는 돌 필요 없다"고 GC 에 알려, 불필요한 파이널라이저 큐잉과 객체 수명 연장을 막는다. 그래서 using 으로 정리한 객체는 파이널라이저가 실행되지 않고, 버려진 객체만 나중에 GC 시점에 파이널라이저가 돈다.

예측 출력

dispose: A          // using 블록 끝 → Dispose, SuppressFinalize 로 A 의 파이널라이저는 억제
finalizer: B        // B 는 Dispose 안 함 → GC.Collect + WaitForPendingFinalizers 후 파이널라이저
done
  • A: 파이널라이저가 돌지 않는다(SuppressFinalize 효과).
  • B: Dispose 누락 → 파이널라이저가 GC 때 실행. 단, 그 시점은 GC 가 결정하므로 평소엔 언제 돌지 알 수 없다(여기선 Collect+WaitForPendingFinalizers 로 강제 동기화해 관측).

핵심 개념

1. 결정적 vs 비결정적 해제

  • 결정적: using/using var/명시적 Dispose(). 블록을 벗어나는 즉시(예외 포함, try/finally 로 보장) 정리. 소켓/파일/락 같은 희소 자원은 반드시 이 경로로 즉시 반납해야 한다.
  • 비결정적: 파이널라이저. GC 가 "도달 불가"로 판정한 뒤에야, 그것도 별도 파이널라이저 스레드에서 실행. 시점·순서·스레드를 보장하지 않는다.

2. 파이널라이저는 왜 객체 수명을 늘리나(2-페이즈 수거)

파이널라이저가 있는 객체는 GC 가 한 번에 못 없앤다. 1차 수거에서 "파이널라이저 큐"로 올려 살려 두고, 파이널라이저 스레드가 ~T 를 실행한 뒤 다음 GC 에서야 메모리를 회수한다. 즉 파이널라이저 객체는 최소 GC 2사이클을 살고, 그동안 자원도 붙들고 있다. SuppressFinalize 는 이 큐잉을 건너뛰어 정상 객체처럼 즉시 수거되게 한다.

3. SuppressFinalize 를 빼면?

Dispose 에서 다 정리했는데 SuppressFinalize 를 안 부르면, GC 가 그 객체를 여전히 파이널라이저 큐에 올려 이미 정리된 객체의 파이널라이저를 또 실행(이중 정리 위험)하고, 객체 수명도 불필요하게 늘어난다. 그래서 표준 패턴은 "Dispose 끝에 SuppressFinalize".

4. 표준 Dispose 패턴(상속 가능한 형태)

public class Resource : IDisposable
{
    private bool _disposed;
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        if (disposing) { /* 관리 자원(다른 IDisposable) 정리 */ }
        /* 비관리 자원(핸들) 해제 — 파이널라이저 경로에서도 안전해야 */
        _disposed = true;
    }
    public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
    ~Resource() { Dispose(false); }   // disposing=false: 관리 객체 건드리지 않음
}
  • 파이널라이저 경로(disposing=false)에서는 다른 관리 객체를 만지면 안 된다 — 그 객체가 이미 수거됐을 수 있다(파이널라이저 실행 순서 무보장).

흔한 오해·함정

  • "GC 가 알아서 소켓도 닫아주겠지": 닫긴 하지만 언제일지 모른다. 그 사이 FD/핸들 고갈, 포트 점유, 파일 잠금 유지. 희소 자원은 반드시 Dispose.
  • 모든 클래스에 파이널라이저 추가: 파이널라이저는 비용(2-페이즈 수거)이다. 비관리 자원을 직접 쥔 경우에만 둔다. 보통은 SafeHandle 멤버를 쓰면 파이널라이저를 직접 안 짜도 된다.
  • 파이널라이저에서 예외/블로킹/긴 작업: 파이널라이저 스레드를 막으면 다른 객체 정리까지 밀린다.
  • using 누락: Dispose 가능한 타입을 using 없이 쓰면 안전망(파이널라이저)이 있어도 해제가 늦어진다.

면접 포인트

  1. Dispose/using결정적 해제, 파이널라이저는 GC 시점의 비결정적 안전망이다. 소켓·핸들 같은 희소 자원은 결정적 경로로 즉시 반납해야 한다.
  2. Dispose 끝의 GC.SuppressFinalize 는 불필요한 파이널라이저 실행과 객체 수명 연장(2-페이즈 수거)을 막는다.
  3. 실무에서는 비관리 핸들을 SafeHandle 로 감싸 파이널라이저를 직접 작성하지 않는 것이 안전하다 — 게임서버의 소켓/네이티브 리소스 누수 방지에 핵심.