C#의 키워드 using에 대해서 정리했습니다.
The using statement ensures the correct use of an IDisposable instance
출처: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/using
using 문(statement: 실행 가능한 최소의 독립조각, 문 · 구문 · 명령문)은 IDisposable 인스턴스가 제대로 작동하도록 보장해 준다. 어떻게 보장한다는 것일까? 이를 알기 위해서는 IDisposable 인터페이스에 대해서 알아야 한다.

IDisposable은 Dispose 메서드를 작성해야 하는 인터페이스다.
Dispose는 처분하다, 배치하다, 정리하다 등의 뜻을 가지고 있다.
즉, 관리되지 않는 자원을 해제하는 메서드를 작성해줘야 한다.
using은 IDisposable을 상속받은(결국 Dispose 메서드를 구현한) 인스턴스가 using 문의 범위를 빠져나가거나, 중간에 에러가 발생할 경우 해당 인스턴스의 Dispose 메서드를 호출해 준다.
다시 돌아가서 using문이 IDisposable 인스턴스가 제대로 작동하도록 보장한다는 것이 무슨 뜻일까?
IDisposable 인터페이스는 자원을 해제할 필요가 있는 클래스가 상속받는다. 이후, 사용되지 않을 경우 자원을 해제하여 메모리 누수가 발생하지 않도록 한다. 혹은, 런타임 중 오류가 발생하여 작업이 중단되어도 자원 해제를 보장해 줄 수 있다.

만약 using문이 없었다면, 이를 어떻게 처리할 수 있었을까?
try - finally 문으로 작성할 수 있다.
try - finally로 작성하게 된다면 코드의 가독성이 떨어지니 using문을 활용해 보자.
아래는 간단한 using의 예제이다.
Csv 파일을 읽는 CsvReader클래스의 Read메서드에 다음과 같이 using문을 작성할 수 있다.
const string path = @"C:\Users\UserName\sampleData.csv";
var data = new CsvReader().Read(path);
Console.ReadKey();
public class CsvReader
{
public CsvData Read(string path)
{
using var streamReader = new StreamReader(path);
const string Separator = ";";
var columns = streamReader.ReadLine().Split(Separator);
var rows = new List<string[]>();
while(!streamReader.EndOfStream)
{
var cellsInRow = streamReader.ReadLine().Split(Separator);
rows.Add(cellsInRow);
}
return new CsvData(columns, rows);
}
}
public class CsvData
{
public string[] Columns { get; }
public IEnumerable<string[]> Rows { get; }
public CsvData(string[] columns, IEnumerable<string[]> rows)
{
Columns = columns;
Rows = rows;
}
}
여기서 using문이 사용되는 Read 메서드만 확인해 보자.
1. using 선언 : 선언된 식별자가 스코프를 벗어날 때 Dispose 호출
public CsvData Read(string path)
{
using var streamReader = new StreamReader(path);
const string Separator = ";";
var columns = streamReader.ReadLine().Split(Separator);
var rows = new List<string[]>();
while(!streamReader.EndOfStream)
{
var cellsInRow = streamReader.ReadLine().Split(Separator);
rows.Add(cellsInRow);
}
return new CsvData(columns, rows);
}
2. using 문 : 코드블록을 벗어날 때 Dispose 호출
public CsvData Read(string path)
{
using (var streamReader = new StreamReader(path))
{
const string Separator = ";";
var columns = streamReader.ReadLine().Split(Separator);
var rows = new List<string[]>();
while(!streamReader.EndOfStream)
{
var cellsInRow = streamReader.ReadLine().Split(Separator);
rows.Add(cellsInRow);
}
return new CsvData(columns, rows);
}
}
3. try - finally 문 : 코드블록을 벗어날 때 Dispose 호출
public CsvData Read(string path)
{
var streamReader = new StreamReader(path);
try
{
const string Separator = ";";
var columns = streamReader.ReadLine().Split(Separator);
var rows = new List<string[]>();
while(!streamReader.EndOfStream)
{
var cellsInRow = streamReader.ReadLine().Split(Separator);
rows.Add(cellsInRow);
}
return new CsvData(columns, rows);
}
finally
{
streamReader.Dispose();
}
}
간단한 예제이기 때문에 예외 처리를 하지 않았다.
using문을 사용하는 두 가지 방법과 try - catch문을 사용할 수 있다는 것을 기억하면 좋겠다.
또한, 왜 using문을 사용해야 하는가 고민할 수 있는 기회였다.

CLR의 GC가 알아서 Dispose를 호출하는가?
GC는 Dispose를 호출하지 않는다.
그러면, 클래스의 종료자에 Dispose를 호출하게 하면?
GC는 매번 힙에 저장된 데이터 전부를 확인하는 것이 아니다.
종료자가 무조건 원하는 순간에 호출될 보장이 없다.
Microsoft도 다음 글에서 리소스의 명시적 해제를 추천한다.
If your application is using an expensive external resource, we also recommend that you provide a way to explicitly release the resource before the garbage collector frees the object. To release the resource, implement a Dispose method from the IDisposable interface that performs the necessary cleanup for the object. This can considerably improve the performance of the application. Even with this explicit control over resources, the finalizer becomes a safeguard to clean up resources if the call to the Dispose method fails.
출처 : https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/finalizers
'TIL (Today I Learned) > C#' 카테고리의 다른 글
[C#] Tuples, ValueTuples (2) | 2024.04.24 |
---|---|
[C#] Reflection, Attributes, Metadata (0) | 2024.04.22 |
댓글