Quick Reference
IDisposable은 파일 핸들·소켓·스트림처럼 GC 시점까지 기다리면 안 되는 자원을 결정적으로 정리하는 계약입니다. 객체를 소유하고 짧은 범위에서 쓴다면 using, 비동기 정리 계약만 있다면 await using을 씁니다.
using (var reader = File.OpenText(path))
{
string text = reader.ReadToEnd(); // 블록을 나갈 때 Dispose
}
using var writer = File.CreateText(outputPath); // 현재 scope 끝에서 Dispose
await using var stream = File.OpenRead(path); // IAsyncDisposable: scope 끝에서 DisposeAsyncusing (...) {}는 블록 끝,using var는 선언된 scope 끝에서 정리합니다.using은 예외가 나도 정리 경로를 실행하는try/finally패턴입니다.- 주입받거나 빌린 객체는 소유권을 확인하기 전에는 Dispose하지 않습니다.
수명 범위와 소유권
using statement는 블록을 벗어날 때, using declaration은 선언된 scope를 벗어날 때 Dispose를 호출합니다. declaration을 메서드 처음에 두면 생각보다 늦게 정리될 수 있으므로, 파일 잠금이나 연결을 빨리 풀어야 하면 좁은 블록을 사용합니다.
using (var stream = File.OpenRead(path))
{
Upload(stream);
} // Upload가 끝나자마자 stream 정리
DoOtherWork();반환할 스트림이나 reader를 using 안에서 만들면 호출자가 받는 순간 이미 Dispose된 객체가 됩니다. 객체를 생성한 쪽이 일반적으로 소유자이지만, API가 leaveOpen: true, DI container, HttpClientFactory처럼 수명을 별도로 관리할 수 있습니다. “누가 만들었는가”, “누가 마지막으로 Dispose하는가”를 API 경계에서 명확히 합니다.
// 잘못된 반환: 메서드를 나갈 때 stream은 이미 Dispose됨
Stream OpenBroken(string path)
{
using var stream = File.OpenRead(path);
return stream;
}
// 호출자에게 소유권을 넘김
Stream OpenForCaller(string path) => File.OpenRead(path);Dispose와 DisposeAsync
using은 IDisposable.Dispose()를, await using은 IAsyncDisposable.DisposeAsync()를 호출합니다. await using은 async method 또는 async iterator 안에서 사용합니다. 타입이 두 인터페이스를 모두 구현했다면 어떤 정리 경로가 필요한지 API 문서를 보고 문법을 선택합니다.
static async Task<long> ReadLengthAsync(string path)
{
await using var stream = File.OpenRead(path);
return await Task.FromResult(stream.Length);
}비동기 I/O를 했다고 모든 객체가 IAsyncDisposable인 것은 아닙니다. 예를 들어 HttpClient 수명은 connection reuse 정책이 더 중요하며, 일반적인 요청마다 using이나 await using으로 새로 만들지 않습니다. 자원 정리 필요성과 권장 lifetime은 별개의 질문입니다.
직접 IDisposable을 구현할 때
다른 disposable 객체를 생성해 필드로 소유하는 타입은 그 자원도 함께 정리해야 합니다. 관리되는 disposable만 소유하는 sealed class에는 단순한 Dispose 구현이면 충분한 경우가 많습니다.
public sealed class ReportWriter : IDisposable
{
private StreamWriter? _writer;
public ReportWriter(string path) => _writer = File.CreateText(path);
public void WriteLine(string line) =>
(_writer ?? throw new ObjectDisposedException(nameof(ReportWriter)))
.WriteLine(line);
public void Dispose()
{
_writer?.Dispose();
_writer = null;
}
}상속 가능한 타입, unmanaged handle, finalizer가 필요한 경우는 dispose pattern의 범위가 커집니다. raw IntPtr를 직접 finalizer로 관리하기보다 SafeHandle을 우선 사용합니다. 동시 Dispose가 실제 요구사항이면 그 클래스의 동시성 계약에 맞춰 idempotence와 동기화를 설계합니다.
자주 틀리는 부분
using var의 변수는 선언 뒤부터 scope 끝까지 살아 있습니다. 너무 이른 위치에 선언하지 않습니다.- Dispose 후 객체를 다시 사용하면 대개
ObjectDisposedException이 발생합니다. Dispose는 메모리 GC를 강제하는 호출이 아닙니다. 외부 자원과 소유한 disposable을 정리하는 호출입니다.- 예외를 피하려고 Dispose를 생략하지 않습니다. 정리 실패를 어떻게 기록·전파할지는 호출 경계의 오류 정책으로 정합니다.
참고 링크
3 sources