반응형
좋은 질문이에요! IDisposable을 구현(“상속”)하는 이유는 **“수명(lifetime)이 끝난 자원을 즉시·명시적으로 해제하기 위해서”**입니다. 가비지 컬렉터는 메모리만 자동으로 돌려주지만, 파일 핸들/소켓/데이터베이스 연결/윈도우 핸들 같은 비관리 자원은 누수가 생길 수 있거든요. 아래 구조로 정리해 볼게요. (가정: C# 12 / .NET 8)
1) 왜 생겼는가 — “가비지 컬렉터만으로는 부족하다”
- GC 한계: .NET GC는 관리 힙 메모리만 회수합니다. OS 자원(파일, 소켓 등)은 GC 타이밍과 무관하게 즉시 닫아야 안전합니다.
- 결정적 해제(Deterministic cleanup): IDisposable.Dispose()를 호출하면 그 즉시 자원을 반환할 수 있습니다. using/await using이 이 호출을 자동화하죠.
- 예외 안전성: using 블록은 예외가 나도 Dispose()를 호출하므로 누수를 막습니다.
2) 무엇인가 — IDisposable과 Dispose 패턴
- 정의:
- public interface IDisposable { void Dispose(); // 소유한 자원을 해제 }
- 핵심 개념
- “자원 소유권(ownership)”을 가진 타입이 Dispose()에서 그 자원을 반납.
- 관리 자원만 가진 경우라도, 내부에 자원을 보유한 다른 IDisposable을 필드로 들고 있으면 연쇄 해제를 위해 구현.
- 관련 개념
- IAsyncDisposable.DisposeAsync() : 비동기 자원 해제(예: 네트워크 스트림 flush/close).
- SafeHandle 계열 : 네이티브 핸들 래핑을 안전하게 처리.
- 종종 등장하는 보일러플레이트: Dispose(bool disposing) 패턴, GC.SuppressFinalize(this).
3) 언제/어떻게 쓰는가 — 상황별 구현법 + MWE
3-1. “관리 자원만, 하지만 내부에 IDisposable 필드 있음”
가장 흔한 케이스: 내부 필드가 IDisposable이라 연쇄 해제가 필요.
public sealed class LogWriter : IDisposable
{
private readonly StreamWriter _writer;
private bool _disposed;
public LogWriter(string path)
{
_writer = new StreamWriter(File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read));
}
public void Write(string line)
{
if (_disposed) throw new ObjectDisposedException(nameof(LogWriter));
_writer.WriteLine(line);
}
public void Dispose()
{
if (_disposed) return;
_writer.Dispose(); // 내부 IDisposable 해제
_disposed = true;
// finalizer가 없으므로 GC.SuppressFinalize는 생략 가능
}
}
// 사용
using (var lw = new LogWriter("app.log"))
{
lw.Write("hello");
} // 여기서 자동으로 Dispose 호출
3-2. “비관리 자원(네이티브 핸들)을 직접 보유” — 전형적 Dispose 패턴
SafeHandle 사용을 권장합니다. finalizer를 두되, 대부분의 클린업은 disposing == true에서.
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
public class NativeThing : IDisposable
{
private SafeFileHandle _handle; // 예시: 네이티브 핸들 래핑
private bool _disposed;
public NativeThing(string path)
{
_handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read);
}
~NativeThing() // Finalizer: 최후의 보루
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // finalizer 큐에서 제거
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
// 비관리 자원 해제는 언제나 수행
if (_handle is not null && !_handle.IsInvalid)
{
_handle.Dispose();
}
if (disposing)
{
// 여기서만 관리 자원(IDisposable 필드 등) 해제
// e.g., _managedStream?.Dispose();
}
_disposed = true;
}
}
3-3. “비동기 클린업 필요” — IAsyncDisposable
public sealed class AsyncBuffer : IAsyncDisposable
{
private readonly Stream _stream;
private bool _disposed;
public AsyncBuffer(Stream stream) => _stream = stream;
public async ValueTask DisposeAsync()
{
if (_disposed) return;
await _stream.FlushAsync(); // 비동기 정리
await _stream.DisposeAsync(); // .NET 5+ 스트림은 DisposeAsync 지원
_disposed = true;
}
}
// 사용
await using var buf = new AsyncBuffer(networkStream);
// ... 작업 ...
3-4. 소비자 관점 — using 문 2가지 문법
// 블록 using
using (var conn = new SqlConnection(cs))
{
conn.Open();
} // 블록 끝에서 Dispose()
// 선언형 using(C# 8+)
using var timer = new System.Timers.Timer(1000);
// 스코프(메서드) 종료 시 Dispose()
간단 체크리스트
- 자원 소유하는가? (파일/소켓/핸들/타 IDisposable 포함)
- Dispose가 중복 호출되어도 안전한가? (idempotent)
- Dispose 후 사용 시 ObjectDisposedException을 던지는가?
- finalizer가 필요한가? (대부분 SafeHandle + finalizer 최소화)
- DI 컨테이너/스코프에서 수명과 해제 시점이 맞는가?
4) 자주 하는 실수와 베스트 프랙티스
자주 하는 실수
- “관리 자원만 있으니 필요 없다”의 오해
- 내부에 다른 IDisposable을 들고 있으면 반드시 연쇄 해제가 필요합니다.
- Dispose 중복 호출로 예외
- Dispose()는 여러 번 불려도 조용히 한 번만 해제되도록 플래그 처리하세요.
- finalizer 남발
- finalizer는 GC 비용 증가. 정말 네이티브 자원 직접 해제가 필요할 때만, 가능하면 SafeHandle 사용.
- struct에 IDisposable 구현 후 복사 문제
- 값 복사로 여러 인스턴스가 같은 자원을 가리켜 이중 해제 위험. 대부분 class가 안전.
- Dispose 누락
- using/await using을 습관화하고, 가능하면 소유권을 명확히(“만든 쪽이 닫는다” 규칙 등) 하세요.
- Dispose 후 멤버 접근 허용
- 해제 후엔 ObjectDisposedException을 던지도록 방어(가독성+버그 조기발견).
베스트 프랙티스
- 가급적 SafeHandle 사용해 네이티브 핸들 수명을 안전하게 관리.
- sealed 타입이면 간단한 Dispose(오버로드 불필요). 상속 허용이면 protected virtual Dispose(bool) 제공.
- DI 환경(ASP.NET Core): AddScoped/AddTransient/AddSingleton에 등록된 IDisposable 서비스는 스코프 종료/호스트 종료 시 자동 Dispose됨을 이해하고 수명 설계를 맞추기.
- IAsyncDisposable 병행: 네트워크나 I/O 버퍼 flush 등 비동기 해제가 의미 있으면 함께 구현하고 await using 사용.
반응형
'C#' 카테고리의 다른 글
“C# readonly: 생성 후 불변을 보장해 버그를 줄이고 성능을 챙기는 설계 키워드” (0) | 2025.10.10 |
---|---|
C# “??는 ‘null이면 우변’ — 안전한 기본값 지정의 한 줄 공식” (0) | 2025.10.10 |
“C# 값 형식에도 없음(null)을 — T? = Nullable<T> 제대로 쓰는 법” (0) | 2025.10.10 |
error. 이벤트핸들러 이름 충돌 (0) | 2025.09.22 |
volatile는 멀티스레드에서 “가벼운 동기화”를 위한 읽기/쓰기 가시성 보장 장치 (0) | 2025.09.19 |
왜 IDisposable을 구현하나요? (0) | 2025.09.19 |
“직접 델리게이트 콜백” → “.NET 표준 이벤트 패턴(Event + EventArgs)” (0) | 2025.09.19 |
sealed를 왜 써야 할까? (0) | 2025.09.19 |