CodeGym /행동 /C# SELF /BufferedStream 사용하기

BufferedStream 사용하기

C# SELF
레벨 41, 레슨 2
사용 가능

1. 소개

새 클래스를 사용하기 전에, 이 클래스가 왜 필요한지 먼저 이해하는 게 좋아. FileStream으로 파일을 “직접” 다룰 때 무슨 일이 일어나는지 같이 살펴보자.

ReadWriteFileStream으로 만든 스트림에 호출하면 실제로는 컴퓨터의 디스크 서브시스템에 접근해. 이 과정 자체는(특히 오래된 HDD에서는, 물론 최신 SSD라도) RAM으로 작업하는 것보다 훨씬 느려. 맥도날드에서 감자튀김을 시킬 때마다 카운터 직원이 창고에 가서 새 봉투를 꺼내 오는 상황을 상상해봐. 줄이 얼마나 길어질지 알겠지?

작은 조각의 데이터를 자주 다루면 디스크나 네트워크에 대한 빈번한 접근 때문에 성능이 떨어져. 데이터 양이 클수록 효과가 더 뚜렷해.

간단한 비유

버퍼 없는 스트림은 마치 요거트 하나 사러 마트에 10번 가는 것과 같아. 버퍼링된 스트림은 한 번에 카트 가득 담아서 가는 거지 — 왕복 횟수를 줄여주는 셈.

2. 클래스 BufferedStream: 첫인상

무엇을 위해 쓰나

BufferedStream은 어떤 Stream이든 감싸는 래퍼야. 메모리에 중간 버퍼를 유지해. 데이터를 쓸 때 먼저 버퍼에 넣고, 버퍼가 찼을 때 한 번에 디스크로 밀어내. 읽을 때도 비슷해서, 첫 읽기 시 어느 정도 블록을 메모리로 로드하고 그다음엔 메모리에서 조금씩 반환해.

코드 예제: BufferedStream 생성

간단한 예제를 만들어보자. 100,000줄을 파일에 쓰는 과제라고 하자:


string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);

for (int i = 0; i < 100_000; i++)
{
    writer.WriteLine($"라인 번호 {i}");
}

Console.WriteLine("쓰기 완료!");

설명:

  • FileStream으로 파일을 쓰기용으로 연다.
  • 그걸 BufferedStream으로 감싸고, 그 다음에 StreamWriter로 감싼다(텍스트 라인을 스트림에 쓰는 역할).
  • 버퍼가 차면 데이터가 한 번에 디스크로 밀려나간다.

3. 내부적으로 버퍼링은 어떻게 동작하나

다이어그램으로 보면:


[네 코드] → [StreamWriter] → [BufferedStream] → [FileStream] → [디스크의 파일]

StreamWriterWriteLine()을 호출하면 텍스트가 먼저 내부 버퍼에 들어가고, 그 다음 BufferedStream을 통해 또 다른 버퍼로 가고, 버퍼가 찰 때나 스트림이 닫힐 때 디스크로 밀려나.

한 바구니에 몇 바이트?

기본 버퍼 크기는 4096 바이트(4 KB)지만, 명시적으로 지정할 수 있어:


int myBufferSize = 16 * 1024; // 16 KB
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...

실전 팁: 요즘 시스템에선 보통 8–64 KB 버퍼를 쓰는 게 합리적이야. 아주 큰 파일 작업이면 더 크게 써도 되고. 근데 무턱대고 키우면 안 돼: 예를 들어 RAM이 128 KB인 마이크로컨트롤러에서 64 KB 버퍼는 별로야 :)

4. 실험: 버퍼 유무에 따른 속도 비교

얼마나 차이가 나는지 보려면, FileStream으로 버퍼 없이 쓸 때와 버퍼를 썼을 때를 비교하는 테스트를 해보자:


using System.Diagnostics;
using System.Text;

string data = new string('X', 1000); // 1,000 문자

void WriteWithoutBuffer()
{
    using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        fs.Write(bytes, 0, bytes.Length); // 파일에 직접 쓰기 – 매번 디스크 접근
    }
}

void WriteWithBuffer()
{
    using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
    using var bs = new BufferedStream(fs, 16 * 1024);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        bs.Write(bytes, 0, bytes.Length);
    }
}

// 시간 측정
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("버퍼 없음: " + sw.ElapsedMilliseconds + " ms");

sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("버퍼 있음: " + sw.ElapsedMilliseconds + " ms");

예상 결과:
대부분의 경우 버퍼를 쓰면 속도 향상이 눈에 띄게 나타날 거야. 특히 HDD에서는 더 그렇고, SSD여도 효과는 있지만 덜 극적일 수 있어.

5. 어떤 버퍼를 골라야 하나? 비교와 실전

.NET에는 버퍼링 관련 클래스가 많아. 정리해보자:

클래스 용도 버퍼 내장? BufferedStream을 써야 하나?
FileStream
파일 작업 있음 (기본 4 KB) 거의 필요 없음(하지만 가능)
NetworkStream
네트워크 작업 아니오 매우 권장
StreamReader/Writer
텍스트 읽기/쓰기 있음 (약 1 KB) 보통 불필요
GZipStream
압축/해제 아니오 속도 향상을 위해 사용 가능/권장

중요:
생성자 파라미터로 bufferSize를 지정한 FileStream은 이미 버퍼링된 스트림이야. 충분히 큰 버퍼를 지정했다면 추가로 BufferedStream을 덧붙여도 큰 이득이 없을 수 있어. 하지만 네트워크 스트림 같은 다른 종류의 스트림을 다루면 BufferedStream이 큰 도움이 돼.

6. 예제: BufferedStream로 파일 복사하기


string source = "big_input.dat";
string dest = "big_output.dat";

int bufferSize = 64 * 1024; // 64 KB

using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);

byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
    bufferedOutput.Write(buffer, 0, bytesRead);
}
// flush를 잊지 마세요 – 마지막 바이트들이 디스크로 가지 않을 수 있어!
bufferedOutput.Flush();

Console.WriteLine("복사 완료!");

설명:

  • 64 KB 같은 큰 블록 단위로 BufferedStream을 통해 원본 파일에서 읽는다.
  • 같은 크기의 버퍼를 사용해서 다른 파일에 쓴다.
  • 루프가 끝난 뒤 마지막 데이터를 위해 반드시 Flush()를 호출하자.

7. 유용한 팁들

언제 진짜로 BufferedStream이 필요한가

  • 버퍼가 없는 스트림을 다룰 때(예: NetworkStream이나 당신이 만든 custom Stream);
  • 대용량 바이너리 데이터를 처리할 때(예: 파일 복사, 포맷 변환, 백업);
  • 기존 코드 최적화를 할 때, 작은 Read/Write 호출이 병목이라면.

비동기와 버퍼링에 대해 조금

ReadAsync/WriteAsync 같은 비동기 작업이 생기면서도 버퍼링은 여전히 유용해. 다만 버퍼 위에서 비동기 메서드를 쓰면 처리 대부분이 메모리 안에서 이뤄지고, 실제 디스크와의 물리적 상호작용은 더 줄어들어.

.NET 8+와 .NET 9에서는 버퍼링이 더 깊게 통합되어 있고 대부분 클래스가 기본적으로 버퍼를 갖고 있어. 그래도 네트워크 스트림이나 직접 구현한 스트림과의 호환성을 위해 수동으로 BufferedStream을 사용하는 경우가 여전히 있어.

비동기에 대한 자세한 내용은 레벨 58에서 배울 거야 :P

버퍼화된 스트림 동작 시각적 도식

flowchart LR
    A[너의 코드] --> B[StreamReader/Writer]
    B --> C[BufferedStream]
    C --> D[FileStream]
    D --> E[파일/장치]
  • A — 너의 코드가 Write/Read를 호출함.
  • B — 텍스트나 데이터를 다루는 상위 레벨 스트림.
  • C — 버퍼링(속도 향상을 위해 데이터 묶음 처리).
  • D — 구체적인 스트림 구현(파일, 네트워크 등).
  • E — 물리적 장치(하드디스크, SSD, 네트워크 등).

실전에서 쓰는 요령들

  1. 파일에 한 줄씩 쓰는(예: 로깅) 경우라면, 버퍼 크기를 한 줄 크기보다 크게 명시적으로 설정하는 게 좋아. 그러면 한 번에 큰 덩어리로 더 빨리 플러시된다.
  2. 각 동작을 즉시 디스크에 남겨야 한다면(예: 중요한 로그), 각 쓰기 후에 Flush()를 호출해. 단, 이러면 버퍼링의 이점이 줄어든다!
  3. 임시 파일을 만들고 바로 삭제할 거라면 버퍼에 남아있어도 상관없을 수 있지만, 파일이 반드시 온전히 기록되어야 한다면 주의하자.
  4. 수십 기가바이트 같은 아주 큰 파일을 다룰 때는 버퍼 크기를 1_048_576 바이트(1 MB) 이상으로 올려도 괜찮아 — 다만 RAM이 충분해야 한다.

8. 흔한 실수와 사용 시 주의점

지금 당장 모든 곳에 무작정 버퍼를 박아 넣고 싶은 유혹이 들 수도 있지만, 과유불급이야!

자주 하는 실수 중 하나는 Flush()를 호출하거나 스트림을 닫는 것을 잊는 거야. 프로그램이 비정상적으로 종료되면 마지막 바이트들이 메모리 버퍼에 남아 디스크에 기록되지 않을 수 있어. 예를 들어 로그를 파일에 쓰다가 프로세스가 죽으면 마지막 로그가 날아갈 수 있어.

BufferedStream은 네가 보낸 논리적 메시지의 경계(끝)를 “보지” 못해 — 단지 특정 크기의 데이터가 쌓일 때까지 기다려. 그래서 중요한 데이터(로그, 백업 등)는 주기적으로 강제로 Flush()해 주는 게 좋아:

bufferedStream.Flush(); // 버퍼를 강제로 디스크로 밀어냄

또 한 가지: StreamWriter도 자체 버퍼가 있어! 즉, 중첩해서 쓰면 버퍼가 두 번 존재하게 되고 항상 좋은 건 아니야. 보통은 하나의 버퍼 레이어면 충분하고, StreamWriter를 사용하면 추가로 BufferedStream을 덧붙일 필요가 없을 때가 많아.

2
과제
C# SELF, 레벨 41, 레슨 2
잠금
BufferedStream을 사용한 파일 내용 복사
BufferedStream을 사용한 파일 내용 복사
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION