CodeGym /행동 /C# SELF /스트림 닫기와 리소스 해제 ( using...

스트림 닫기와 리소스 해제 ( using)

C# SELF
레벨 36 , 레슨 0
사용 가능

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 (선언)
모양
using (var x = ...) { ... }
using var x = ...; ...
스코프 블록의 중괄호 안 현재 블록(메서드, 루프 등) 끝까지
간결함 조금 더 장황함 짧고 들여쓰기 적음
도입 버전 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도 같이 쓰는 게 좋아.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION