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
- Stream — 기본 추상 클래스
- FileStream — 파일 작업용
- MemoryStream — 메모리 데이터 작업용
- NetworkStream — 네트워크 통신용
- CryptoStream — 암호화/복호화용
스트림의 주요 속성과 메서드 간단 정리
| 속성 / 메서드 | 설명 |
|---|---|
|
이 스트림에서 읽을 수 있는지 |
|
이 스트림에 쓸 수 있는지 |
|
스트림에서 위치 이동이 가능한지 (모든 스트림이 지원하진 않아) |
|
스트림 길이 (지원되는 경우에만 — 모든 스트림이 있는 건 아님) |
|
스트림에서 현재 위치 |
|
데이터 읽기 |
|
데이터 쓰기 |
|
스트림에서 위치 이동 |
|
버퍼 비우기 (쌓인 데이터를 스트림에 다 써버림) |
/ |
스트림 닫고 리소스 해제 |
실제로 어떻게 쓰는지 한번 보자.
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보다 작을 수 있음!
}
GO TO FULL VERSION