CodeGym /행동 /C# SELF /입출력 스트림: Stream

입출력 스트림: Stream

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

1. 들어가기

일단 물이 든 주전자를 상상해봐. 수도꼭지를 열면 물이 흐르지? 한 번에 물을 다 받을 수도 있고, 조금씩 조금씩 채울 수도 있어. 파일도 똑같아 — 항상 파일 전체를 한 번에 메모리로 읽는 게 편하거나 가능한 건 아니야. 파일이 엄청 클 수도 있고, 데이터 소스가 파일이 아니라 네트워크 연결처럼 점점 들어오는 경우도 있거든.

만약 우리가 항상 그냥 바이트 배열로만 작업했다면, 큰 파일에서는 금방 메모리가 부족해질 거고, "끝없는" 데이터 스트림(예를 들면 비디오나 오디오 스트림)에는 이 방식이 아예 안 먹혀. 그래서 스트림이라는 개념이 등장하는 거지!

.NET에서 스트림은 데이터에 순차적으로 접근할 수 있게 해주는 추상화야: 데이터 소스가 파일이든, 네트워크든, 메모리든, 아니면 압축 파일 같은 특이한 거든 상관없어. 스트림 덕분에 데이터를 조각조각 (보통 블록이나 바이트 단위로) 읽고 쓸 수 있어.

핵심 아이디어:

  • 스트림은 데이터 전송을 위한 채널이야. 컨베이어 벨트처럼, 데이터를 "넣거나"(쓰기) "꺼내거나"(읽기) 할 수 있고, 데이터가 어디에 어떻게 저장되는지 신경 안 써도 돼.
  • 데이터는 순차적으로 와: 이전 조각을 읽은 다음에야 다음 조각을 읽을 수 있어 (혹은 반대로, 만약 이동이 지원된다면).
  • 대부분의 경우, 모든 데이터를 한 번에 메모리에 저장하지 않아 (컴퓨터도 그걸 좋아할 거야).

이 추상화는 .NET에서 거의 모든 입출력 작업의 기본이야: 파일, 네트워크, 압축, 심지어 콘솔 작업까지!

2. 스트림 System.IO.Stream

상속과 구조: System.IO.Stream

.NET의 거의 모든 스트림은 추상 클래스 System.IO.Stream에서 상속돼. 이 클래스가 읽기, 쓰기, 이동, 스트림 관리의 기본 메서드를 정의하지.


classDiagram
    class Stream {
        +Read()
        +Write()
        +Seek()
        +CanRead
        +CanWrite
        +CanSeek
        +Length
        +Position
    }
    class FileStream
    class MemoryStream
    class NetworkStream
    class CryptoStream
    Stream <|-- FileStream
    Stream <|-- MemoryStream
    Stream <|-- NetworkStream
    Stream <|-- CryptoStream
.NET에서 스트림 상속 구조도
  • Stream — 기본 추상 클래스
  • FileStream — 파일 작업용
  • MemoryStream — 메모리 데이터 작업용
  • NetworkStream — 네트워크 통신용
  • CryptoStream — 암호화/복호화용

스트림의 주요 속성과 메서드 간단 정리

속성 / 메서드 설명
CanRead
이 스트림에서 읽을 수 있는지
CanWrite
이 스트림에 쓸 수 있는지
CanSeek
스트림에서 위치 이동이 가능한지 (모든 스트림이 지원하진 않아)
Length
스트림 길이 (지원되는 경우에만 — 모든 스트림이 있는 건 아님)
Position
스트림에서 현재 위치
Read(...)
데이터 읽기
Write(...)
데이터 쓰기
Seek(...)
스트림에서 위치 이동
Flush()
버퍼 비우기 (쌓인 데이터를 스트림에 다 써버림)
Close()
/
Dispose()
스트림 닫고 리소스 해제

실제로 어떻게 쓰는지 한번 보자.

3. 예시: Stream으로 파일 읽고 쓰기

스트림이 실제로 어떻게 동작하는지 볼 수 있는 최소한의 예시야:


// 파일을 쓰기 모드로 열기
using var stream = new FileStream("numbers.bin", FileMode.Create);

// 1부터 10까지 숫자를 파일에 쓰고 싶다고 해보자
for (int i = 1; i <= 10; i++)
{
    byte val = (byte)i;
    stream.WriteByte(val); // 한 바이트씩 씀
}

// 파일을 명시적으로 닫고, 다시 읽기 위해 열기
stream.Close(); 

// 이제 이 숫자들을 다시 읽어보자
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
    Console.WriteLine(value); // 1, 2, ... 10 출력됨
}

여기서는 FileStream을 쓰고 있는데, 이게 진짜 스트림이야: 데이터를 블록이나 바이트 단위로 읽고 쓸 수 있지.

스트림의 종류: 어디서 만날 수 있을까?

스트림이 꼭 디스크 파일만 의미하는 건 아니야. 스트림 개념이 쓰이는 예시 몇 가지:

  • 디스크 파일 (예: FileStream — 제일 흔함)
  • 메모리 스트림 (MemoryStream — 임시 데이터나 중간 데이터에 편함)
  • 네트워크 연결 (NetworkStream)
  • 압축/아카이브 (GZipStream, DeflateStream)
  • 암호화 (CryptoStream)
  • 콘솔 입출력 (맞아, 이것도!) — 기술적으로 스트림임

이렇게 하면 코드가 데이터 소스/타입에 상관없이 쓸 수 있어: 스트림만 다루면 코드가 범용적이거든!

4. 유용한 팁들

읽기와 쓰기는 데이터를 조각조각 옮기는 작업이야. 보통 바이트 배열이랑 Read, Write 메서드를 써.

예시: 파일을 블록 단위로 읽기

byte[] buffer = new byte[1024]; // 1024바이트(1KB) 버퍼
using var stream = new FileStream("bigfile.bin", FileMode.Open);

int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
    // buffer 안에서 bytesRead만큼만 처리
    int sum = 0;
    for (int i = 0; i < bytesRead; i++)
        sum += buffer[i];

    Console.WriteLine($"블록 합계: {sum}");
}

이 방식은 백신부터 음악 플레이어까지 다 써.

스트림에서 위치 이동 (Position, Seek)

대부분의 스트림 구현(예: 파일 스트림)에서는 데이터 위치를 이동할 수 있어 — 그냥 "다음 조각"만 읽는 게 아니라, 원하는 위치로 점프해서 거기서부터 데이터 작업이 가능하지.

using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // 6번째 바이트로 이동 (0부터 시작)
int value = stream.ReadByte();
Console.WriteLine($"파일의 6번째 바이트: {value}");

스트림은 읽기 전용, 쓰기 전용, 또는 둘 다 지원할 수 있음

어떤 스트림은 한 가지만 지원해:

  • 쓰기 전용 파일: Write()만 가능
  • 네트워크 데이터 읽기 스트림: Read()만 가능
  • 특이한 경우(예: 프린터 출력 스트림)는 "되감기"나 위치 이동이 아예 불가능할 수도 있어.

지원되는 작업은 CanRead, CanWrite, CanSeek 속성으로 확인해봐:

using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
    Console.WriteLine("읽기 지원됨");
if (stream.CanWrite)
    Console.WriteLine("쓰기 지원됨");
if (stream.CanSeek)
    Console.WriteLine("파일 위치 이동 가능");

스트림의 버퍼링

거의 모든 스트림은 성능을 위해 내부 버퍼를 써. 버퍼링 덕분에 디스크/네트워크 접근을 줄일 수 있어: 데이터가 내부에 쌓였다가 한 번에 전달/저장돼.

Flush() 메서드는 버퍼를 비워서(예를 들어, 모든 데이터가 디스크에 꼭 저장되게) 쓸 수 있어:

using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // 진짜로 디스크에 저장됐는지 보장

진짜 중요한 데이터(예: 결제 트랜잭션!)를 쓸 때는 Flush() 호출이 필수야.

5. 스트림 작업에서 흔한 실수들

초보자들이 자주 하는 실수들:

스트림을 닫는 걸 까먹어서(메모리 누수, "잠긴" 파일 등등의 골치 아픈 문제 발생).

텍스트 스트림이랑 바이너리 스트림을 헷갈려서 — 문자열을 바이트 메서드로 쓰고, 읽을 때 "이상한 문자"가 나옴.

버퍼를 너무 작게 쓰거나(아니면 아예 안 쓰거나) — 작업이 느려짐.

Read()가 항상 요청한 만큼의 바이트를 읽는다고 착각 — 실제로는 더 적게 읽을 수도 있으니, 반환값을 꼭 확인해야 해.

모든 스트림이 위치 이동(Seek)을 지원한다고 생각 — 특히 네트워크 스트림은 안 될 수도 있어.

예를 들면:


// 나쁜 예시: 실제로 읽은 바이트 수를 확인하지 않고 파일의 모든 바이트를 읽으려 함
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
    int bytesRead = stream.Read(buffer, 0, 1024);
    // 파일이 더 작으면 bytesRead가 1024보다 작을 수 있음!
}
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION