어떤 객체는 가비지 컬렉터가 관리하지 않는 자원을 붙잡고 있습니다. 열린 파일, 네트워크 소켓, 데이터베이스 연결, 운영체제 핸들입니다. 이런 객체는 IDisposable을 구현하며, Dispose()를 호출해 자원을 해제합니다. using 문은 예외가 던져지더라도 정해진 지점에서 이를 대신 호출합니다.
이 페이지는 using 문에 관한 것입니다. 파일 맨 위의 using System; 줄은 네임스페이스를 가져오는 다른 기능인 using 지시문입니다.
using 블록
using (declaration) { body }는 객체를 만들고, 본문을 실행한 뒤, Dispose를 호출합니다. Dispose에서 출력하는 작은 클래스로 그 시점을 정확히 볼 수 있습니다:
출력:
open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled
두 번째 블록은 예외를 던지지만, catch가 예외를 처리하기 전에 여전히 "close api"가 출력됩니다. 이 보장이 마지막 줄에서 직접 Dispose()를 호출하는 대신 using을 쓰는 이유입니다. 직접 호출은 그 앞에서 던져진 예외가 있으면 건너뛰어집니다.
using이 컴파일되는 모습
컴파일러는 using 블록을 try/finally로 펼칩니다. 다음 둘은 같습니다:
using (var writer = new StreamWriter("log.txt"))
{
writer.WriteLine("started");
}
// is compiled as:
{
var writer = new StreamWriter("log.txt");
try
{
writer.WriteLine("started");
}
finally
{
if (writer != null) ((IDisposable)writer).Dispose();
}
}
펼친 모습에서 세 가지 세부 사항이 나옵니다. 변수는 블록 범위이고 그 안에서 읽기 전용입니다(writer에 다시 대입할 수 없습니다). 객체는 IDisposable을 구현해야 하며, 그렇지 않으면 코드가 컴파일되지 않습니다. 그리고 null 값이 허용됩니다. Dispose를 그냥 건너뛰며, 팩터리가 아무것도 반환하지 않을 수 있을 때 편리합니다.
파일과 스트림
파일과 스트림 클래스가 일상적인 경우입니다. StreamWriter는 텍스트를 메모리에 버퍼링하고, Dispose는 버퍼를 디스크에 비우고 파일 핸들을 닫습니다:
출력:
id,total
1001,59.90
1002,12.50
Dispose가 없으면 작성기의 마지막 줄들이 여전히 버퍼에 남아 있을 수 있고, Windows에서는 열린 핸들이 다른 프로그램(과 내 코드의 다음 File.Delete)이 파일을 건드리지 못하게 합니다. File.WriteAllText와 File.ReadAllLines 같은 헬퍼는 내부에서 열고 dispose하므로 using이 필요 없습니다. 더 많은 내용은 파일을 참고하세요.
여러 자원: dispose 순서
using 문을 사이에 중괄호 없이 쌓으면 한 블록에서 여러 자원을 열 수 있습니다. 만든 순서의 역순으로 dispose됩니다:
출력:
acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a
역순은 계층화된 자원에 필요한 순서입니다. 작성기는 버퍼를 감싸고 버퍼는 파일을 감싸므로, 작성기가 버퍼로 비우고, 버퍼가 파일에 쓰고, 그다음 파일이 닫혀야 합니다.
using 선언(C# 8)
C# 8은 중괄호가 없는 형태를 추가했습니다. 변수는 둘러싼 범위가 끝날 때, 보통 메서드 끝에서 dispose됩니다:
static void ExportReport(string path, IEnumerable<string> rows)
{
using var writer = new StreamWriter(path);
writer.WriteLine("Report");
foreach (string row in rows)
{
writer.WriteLine(row);
}
} // writer.Dispose() runs here, at the end of the method
들여쓰기 한 단계를 줄여 주며, 메서드가 자원을 두세 개 쓸 때 효과가 쌓입니다. dispose 순서 규칙은 같습니다. 한 범위의 선언들은 역순으로 dispose됩니다. 대가는 범위가 끝날 때까지 자원이 열려 있다는 것이므로, 메서드가 더 많은 일을 하기 전에 파일을 닫아야 한다면 블록 형태를 유지하거나 선언을 자기 { } 블록에 넣으세요.
정리가 비동기인 객체(많은 데이터베이스 연결과 스트림 같은 IAsyncDisposable)를 위해 C# 8에는 같은 자리에서 DisposeAsync()를 await하는 await using도 있습니다.
직접 만든 클래스에 IDisposable 구현하기
클래스가 dispose 가능한 무언가를 소유하고 있어서(스트림, 타이머, 연결을 만들었다면) 차례로 그것을 dispose해야 할 때 IDisposable을 구현하세요. 흔한 경우에는 짧은 메서드입니다:
출력:
1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException
이 코드가 따르는 규칙: Dispose는 객체가 소유한 모든 것을 dispose하고, 두 번 이상 호출되면 두 번째에는 아무것도 하지 않으며, dispose된 뒤 객체를 쓰면 ObjectDisposedException을 던집니다. using (log)는 블록이 선언 대신 기존 변수를 받을 수 있다는 것도 보여 줍니다.
protected virtual void Dispose(bool disposing) 메서드와 종료자를 가진 더 긴 "dispose 패턴"은 클래스가 관리되지 않는 핸들(네이티브 코드의 원시 포인터)을 직접 붙잡고 있을 때만 필요합니다. 최신 .NET에서는 그런 핸들을 SafeHandle 하위 클래스로 감싸고, 클래스에는 위의 단순한 버전만 있으면 됩니다.
흔한 실수
using블록에서 만든 객체를 반환하기. 블록을 빠져나갈 때 dispose되므로 호출하는 쪽은 닫힌 스트림을 받습니다. 데이터를 반환하거나, 호출하는 쪽이using을 갖게 하세요.- 스트림과 작성기에
using을 잊기. 버퍼의 데이터를 잃고 파일이 잠긴 채로 남을 수 있습니다. - 공유 객체를 dispose하기. 소유한 것만 dispose하세요. 예를 들어 오래 사는
HttpClient는 요청마다 만들고 dispose하는 것이 아니라 공유하고 재사용하도록 만들어졌습니다. - 블록 뒤에서 객체 쓰기. dispose된 객체의 메서드는
ObjectDisposedException을 던집니다. - 가비지 컬렉터에 의존하기. 가비지 컬렉터는 파일이나 소켓이 아니라 메모리를 해제하며,
Dispose를 절대 호출하지 않습니다.
자주 묻는 질문
C#에서 using 문은 무엇을 하나요?
using (var x = ...) { ... }는 블록이 정상적으로 끝나든, return으로 끝나든, 예외가 던져져서 끝나든 블록이 끝날 때 x.Dispose()를 호출합니다. 컴파일러가 이를 finally에 Dispose 호출이 있는 try/finally로 바꾸므로, 자원은 가비지 컬렉터가 실행될 때가 아니라 알려진 지점에서 해제됩니다.
C#에서 IDisposable이란 무엇인가요?
IDisposable은 void Dispose() 메서드 하나를 가진 인터페이스입니다. 파일 핸들, 네트워크 소켓, 데이터베이스 연결, 타이머처럼 명시적으로 해제해야 하는 것을 붙잡고 있는 클래스가 이를 구현합니다. 타입이 IDisposable을 구현하는 모든 객체는 다 쓴 뒤 dispose해야 하며, 보통 using으로 합니다.
using과 using var의 차이는 무엇인가요?
using (var x = ...) { }는 자기 블록의 끝에서 dispose합니다. using 선언 using var x = ...;(C# 8 이상)는 자기 블록이 없으며, 둘러싼 범위가 끝날 때, 보통 메서드 끝에서 dispose합니다. 자원이 메서드의 나머지 동안 살아 있어야 할 때 중첩을 줄여 줍니다.
여러 using 객체는 어떤 순서로 dispose되나요?
만든 순서의 역순입니다. using (var a = ...) using (var b = ...) { }에서는 b가 먼저, 그다음 a가 dispose됩니다. FileStream을 감싸는 StreamWriter처럼 b가 a에 의존할 때 원하는 순서입니다.
가비지 컬렉터가 Dispose를 호출하나요?
아니요. 가비지 컬렉터는 메모리를 해제하고, 예측할 수 없는 나중 시점에 종료자를 실행할 수도 있지만, Dispose는 절대 호출하지 않습니다. dispose하지 않은 파일은 잠긴 채로 남을 수 있고, 버퍼에 있는 데이터가 끝내 기록되지 않을 수도 있습니다. 그래서 using이 존재합니다.