1. C#에서 리소스 제대로 해제하는 방법?
운영체제를 엄격한 사서라고 생각해봐. 너가 책을 빌렸어(파일/스트림을 열었어), 읽고 있는데... 돌려주는 걸 까먹었어! 사서가 화내지: "아직도 책 안 돌려줬어?!" 스트림도 똑같아. 열린 스트림은 리소스를 차지해: 파일 디스크립터, 메모리 조각, 그리고 다른 앱에서 파일을 못 쓰게 막아버려.
스트림을 안 닫으면 결과가 "뭔가 안 돼"부터 "다 망가져서 아무도 이 파일에 못 써"까지 다양해. 프로그램에 안 닫힌 스트림이 많으면 시스템이 리소스를 "새고" 그냥 멈춰버릴 수도 있어.
어떤 점이 위험할까?
- 파일이 안 닫혀서 명령이 디스크까지 안 감 (예를 들어, 쓰기 중이면 데이터가 버퍼에만 남을 수 있음).
- 파일이 다른 프로세스에 막힘 — 동료나 다른 프로그램이 짜증남.
- 디스크립터 제한: Windows/Linux에서 프로세스마다 열린 파일/스트림 개수 제한이 있어.
IDisposable 인터페이스
비관리 리소스(스트림, 파일, DB, 소켓 등)를 다루는 모든 클래스는 IDisposable 인터페이스를 구현해야 해.
public interface IDisposable
{
void Dispose();
}
Dispose() 메서드 안에서는 보통 모든 불쌍한 리소스를 해제해: 파일이 드디어 닫히고, 연결이 끊기고, 메모리가 해제돼.
스트림은 파일, 연결, 메모리 등 중요한 리소스를 손에 쥐고 있는 객체야. 이걸 안 닫으면 파일이 계속 잠겨있을 수 있어(Word가 "파일이 다른 앱에서 열려있어!"라고 할 때처럼), 시스템은 메모리가 부족해질 수도 있어. 그래서 작업 끝나면 꼭 스트림을 해제해야 해.
.NET에서 스트림은 IDisposable 인터페이스를 구현해. 즉, Dispose()를 호출해서 닫아주거나, 그냥 using 블록에 넣으면 돼.
2. 스트림 닫는 방법: 위험한 것부터 안전한 것까지
방법 1. "수동"으로 닫기: 이렇게 하지 마!
이건 옛날 방식이야. 내가 예전 예제에서 보여주긴 했지만, 요즘 개발에서는 거의 안 써 :P
var stream = new FileStream("file.txt", FileMode.Open);
// 스트림 작업
stream.Close(); // 또는 stream.Dispose()
문제점: 열고 닫는 사이에 에러/예외가 나면 파일이 계속 열려서 잠겨있어. 마치 도서관에서 책 빌려놓고 피자 냄새 맡고 뛰쳐나가는 것처럼...
방법 2. try...finally 사용
FileStream stream = null;
try
{
stream = new FileStream("file.txt", FileMode.Open);
// 스트림 작업
}
finally
{
if (stream != null)
stream.Dispose();
}
이 방법은 안전해: finally는 에러가 나도 무조건 실행돼. 근데 솔직히 이렇게 쓰기 귀찮지.
방법 3. 깔끔하고 안전하게: using 연산자
고전 문법 (using ( ... ) { ... })
using (var stream = new FileStream("file.txt", FileMode.Open))
{
// 스트림 작업
}
// 여기서 stream.Dispose()가 자동 호출됨!
핵심은 — using 블록 안에서 스트림을 쓰고, 블록이 끝나면(예외가 나도) 파일이 닫힌다는 거야.
방법 4. 최신 문법
최신 문법 (using var)
using var stream = new FileStream("file.txt", FileMode.Open);
// 스트림 작업
// ... Dispose는 변수의 스코프가 끝나면 자동 호출됨
완전 좋아! 괄호랑 들여쓰기 덜어낼 수 있어.
이게 "속에서" 어떻게 동작할까?
using 연산자는 컴파일러가 try...finally로 바꿔줘. 농담: "using이 너 대신 깔끔한 코드를 써줘 — 곧 커피 마시고 Stack Overflow에 빠질지도?".
고전 using과 최신 using의 차이
| 고전 using-블록 | using var (선언) | |
|---|---|---|
| 모양 | |
|
| 스코프 | 블록의 중괄호 안 | 현재 블록(메서드, 루프 등) 끝까지 |
| 간결함 | 조금 더 장황함 | 짧고 들여쓰기 적음 |
| 도입 버전 | C# 1.0 | C# 8.0 이상 |
3. using-선언이란?
5년 전에 IDisposable 객체를 다루는 새로운, 간결한 방법이 나왔어.
using-선언은 블록 대신 변수 선언에 using 키워드를 붙이는 거야. 그러면 그 변수는 현재 블록(예: 메서드, 루프) 끝에서 자동 해제돼. 중괄호 블록 끝이 아니라!
using var stream = new FileStream("file.txt", FileMode.Open);
// 스트림 작업
Console.WriteLine(stream.Length);
// 여기선 파일이 아직 열려있음!
// ... 메서드 끝
// stream.Dispose()가 여기서 자동 호출됨
고전 using과의 주요 차이점:
- 중괄호 필요 없음, 중첩 블록이 안 생김.
- 변수는 선언된 블록 끝까지 쓸 수 있음(보통 메서드, 가끔 루프, 클래스 수준이면 클래스 전체).
- 리소스 해제는 블록 실행이 끝날 때만 일어남.
왜 이게 좋은가?
- 중첩 레벨이 줄어듦 — 코드가 훨씬 짧고 읽기 쉬워져.
- 여러 리소스 다루기 쉬움 — using 변수 여러 개 선언하면, 메서드 끝에서 다 해제돼.
- 실수할 확률이 줄어듦 — Dispose() 호출 구간을 놓칠 일이 없어.
4. 비교: 고전 vs. 최신 using
코드로 비교해보자.
고전 방식
using (var reader = new StreamReader("input.txt"))
{
using (var writer = new StreamWriter("output.txt"))
{
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
}
} // 여기서 두 파일 다 닫힘
최신 방식 (C# 8+)
using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line.ToUpper());
}
// 메서드 끝에서 두 파일 다 닫힘
확실히 더 간단하지? 중첩이 더 많아지면 최신 방식이 훨씬 편해.
5. Dispose()는 언제, 어디서 호출될까?
여기서 학생들이 자주 실수해: Dispose가 사용 직후 바로 호출된다고 생각하는데 — 그게 아니야!
이 예제 봐봐:
void MyMethod()
{
using var fileStream = new FileStream("data.bin", FileMode.Open);
// ... 코드가 많고, 루프나 함수 호출도 있을 수 있음
// fileStream은 아직 열려있음!
// 여기서 fileStream에 접근 가능
}
// 여기, 메서드의 }에서 fileStream.Dispose() 호출됨
중요: using 변수를 루프 안에 선언하면, Dispose는 매 반복마다 호출돼.
foreach (var path in filePaths)
{
using var reader = new StreamReader(path);
// reader로 작업
} // 각 반복 끝에 reader.Dispose() 호출(파일 닫힘)
6. 옛날 코드 옮길 때 실수
가끔 옛날 코드를 옮기거나 고전 using 예제를 복사하는데, 변수의 "수명"이 블록보다 길어야 할 때가 있어. 그럴 땐 고전 방식은 안 맞고, using-선언이 딱이야.
근데 주의할 점도 있어. 예를 들어, 루프에서 두 리소스를 쓰는데 하나는 더 오래 살아야 한다면, 선언 순서를 잘 맞춰야 해:
using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
using var resource2 = ...;
// resource2는 한 번 반복마다 살아있음
// resource1은 함수 전체에서 살아있음
}
7. 실습
우리 계속해서 학습용 앱 — 지난번에 만들던 작은 커피 주문 시뮬레이터를 발전시켜보자. 이제 주문 내역을 텍스트 파일에 저장하고, 시작할 때 읽어오게 해보자.
1단계: 주문을 파일에 저장
using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("커피: 라떼; 우유: 오트밀크; 사이즈: 라지");
// StreamWriter 생성자의 두 번째 파라미터 append: true는 파일을 덮어쓰지 않고 이어서 쓴다는 뜻이야.
2단계: 주문 내역 읽기
using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"주문: {line}");
}
이 메서드가 끝나면 파일이 자동으로 닫혀.
8. using-선언 쓸 때 베스트 프랙티스
1. IDisposable 구현 객체는 항상 using 써라
.NET에서 파일, 스트림, 리소스 다루는 대부분의 클래스가 이 인터페이스를 구현해. 이건 "나를 using으로 해제해줘!"라는 신호야.
2. 스코프에 주의: 필요 없는 곳에 using-var 선언하지 마
변수가 몇 줄만 필요하면, 꼭 필요한 곳에서만 선언해.
3. 해제 순서 기억해
여러 using 변수를 한 번에 선언하면 Dispose는 역순으로 호출돼:
using var first = new Resource("First");
using var second = new Resource("Second");
// ... 작업
// 먼저 second.Dispose(), 그 다음 first.Dispose()
이게 중요할 때도 있어. 예를 들어, 쓰기 스트림이 파일보다 먼저 해제돼야 할 때.
4. using-선언은 메서드 밖에서 쓰지 마
using-선언은 클래스 필드 같은 데서 못 써. 메서드, 생성자 등 내부에서만 동작해.
5. 예외 처리랑 같이 써라
using만으로 모든 예외를 다루기 불편할 수 있어 — 읽기/쓰기 에러를 컨트롤하려면 try-catch도 같이 쓰는 게 좋아.
GO TO FULL VERSION