스트림 구조
- .NET 스트림 구조는 배경 저장소(backing store), 장식자(decorator), 적응자(adapter)라는 세 가지 개념으로 구성된다. 아래 그림에 이들이 나와 있다.

- 배경 저장소는 입출력 연산이 실제로 효과를 발휘하는 종점(endpoint)이다. 이를테면 파일이나 네트워크 연결이 배경 저장소에 해당한다. 좀 더 정확히 말하면 배경 저장소는 다음 둘 중 하나 또는 둘 다이다.
- 바이트들을 차례로(순차적) 읽을 수 있는 원본(source)
- 바이트들을 차례로 기록할 수 있는 대상(destination)
- 그런데 배경 저장소가 쓸모가 있으려면 프로그래머가 접근할 수 있어야 한다. 그런 용도로 쓰이는 표준 .NET 클래스가 바로 Stream이다.
- 이 클래스에는 읽기, 쓰기, 위치 지정을 위한 일단의 메서드를 제공한다. 모든 지원 자료가 메모리에 들어 있는 배열과는 달리, 스트림은 자료를 직렬로(serially) 다룬다. 스트림에서는 한 번에 1바이트씩 또는 관리 가능한 크기의 블록 하나씩만 읽거나 쓸 수 있다.
- 따라서 배경 저장소가 아무리 커도 스트림을 사용하는데는 아주 적은 양의 메모리만 필요하다.
- 스트림은 크게 두 종류로 나뉜다.
- 배경 저장소 스트림
- 배경 저장소의 구체적인 종류에 맞게 특화된 스트림이다. 이를테면 FileStream이나 NetworkStream이 이 종류에 해당한다.
- 장식자 스트림
- 장식자 스트림은 다른 스트림의 자료를 적절히 변환하는 기능을 제공한다. 이를테면 DeflateStream나 CryptoStream이 장식자 스트림이다.
- 장식자 스트림에는 다음과 같은 구조적인 장점들이 있다.
- 압축이나 암호화 같은 기능을 배경 저장소 스트림마다 따로 구현할 필요가 없다.
- 스트림의 자료를 변환(‘장식’)하기 위해 스트림의 인터페이스를 변경할 필요가 없다.
- 장식자를 실행시점에서 스트림에 연결할 수 있다.
- 여러 장식자를 사슬처럼 이을 수 있다(이를테면 압축한 후 암호화하는 등)
- 배경 저장소 스트림과 장식자 스트림은 바이트만 다룬다. 이것이 유연하고 효율적인 방식이긴 하지만, 응용 프로그램은 텍스트나 XML 같은 좀 더 높은 수준의 자료를 다루는 경우가 많다. 이런 간극을 메우는 것이 적응자(adapter)이다.
- 적응자는 특정 서식에 대응되는 형식으로 특화된 메서드들ㅇ르 가진 클래스로 스트림을 감싼다. 예컨대 텍스트 읽기 적응자는 ReadLine이라는 메서드를 제공하고 XML 쓰기 적응자는 WriteAttributes라는 메서드를 제공한다.
- 장식자처럼 적응자도 스트림을 감싸는 래퍼(wrapper) 클래스이다. 그러나 장식자와는 달리, 적응자 자체는 스트림이 아니다. 일반적으로 적응자는 바이트 지향적 메서드들을 완전히 감춘다.
- 정리하자면 배경 저장소 스트림은 미가공 자료(raw data)를 제공한다.
- 장식자 스트림은 암호화 같은 변환을 수행하되, 역시 바이트 수준에서 작동한다.
- 적응자는 문자열이나 XML 같은 고수준 형식을 다루기 위한 형식있는 메서드들을 제공한다.
- 이들을 하나의 사슬로 이으려면 그냥 한 객체를 다른 객체의 생성자에 넣으면 된다.
스트림 사용
- 추상 Steam 클래스는 모든 스트림 클래스의 기반 클래스이다. Stream은 읽기(reading), 쓰기(writing), 탐색(seeking)이라는 세 가지 근본 연산을 위한 메서드들과 속성들을 정의하며 스트림 닫기와 배출, 시간 만료 설정 같은 관리 작업을 위한 메서드들과 속성들도 정의한다.
제목 없음
- .NET Framework 4.5부터는 Read/Write 메서드들의 비동기 버전들도 생겼다. 비동기 메서드들은 모두 Task를 돌려주며, 선택적 인수로 취소 토큰을 받는다.
- 다음은 파일 스트림을 이용해서 파일을 읽고, 쓰고, 탐색하는 예이다.
using System;
using System.IO;
class Program
{
static void Main()
{
using (Stream s = new FileStream("test.txt", FileMode.Create))
{
Console.WriteLine(s.CanRead); // true
Console.WriteLine(s.CanWrite); // true
Console.WriteLine(s.CanSeek); // true
s.WriteByte(101);
s.WriteByte(102);
byte[] block = { 1, 2, 3, 4, 5, };
s.Write(block, 0, block.Length); // 5바이트 블록을 기록한다.
Console.WriteLine(s.Length); // 7
Console.WriteLine(s.Position); // 7
s.Position = 0; // 시작 위치로 돌아간다.
Console.WriteLine(s.ReadByte()); // 101
Console.WriteLine(s.ReadByte()); // 102
// 스트림을 읽어서 block 배열을 다시 채운다.
Console.WriteLine(s.Read(block, 0, block.Length)); // 5
// 위의 Read 호출이 5를 돌려주어다고 가정하면 현재 위치느느 파일의 끝이므로 다음 Read는 0을 돌려준다.
Console.WriteLine(s.Read(block, 0, block.Length)); // 0
}
}
}
- 비동기 읽기/쓰기는 그냥 Read/Write 대신 ReadAsync/WriteAsync를 호출하고 해당 호출문에 await를 적용하며녀 된다.
async static void AsyncDemo()
{
using (Stream s = new FileStream("test.txt", FileMode.Create))
{
byte[] block = { 1, 2, 3, 4, 5, };
await s.WriteAsync(block, 0, block.Length); // 비동기 쓰기
s.Position = 0; // 시작 위치로 돌아간다.2
// 스트림을 읽어서 block 배열을 다시 채운다.
Console.WriteLine(await s.ReadAsync(block, 0, block.Length)); // 5
}
}
- 잠재적으로 느린 스트림(특히 네트워크 스트림)을 다루는 응용 프로그램을 작성할 때 비동기 메서드들을 이용하면 스레드를 점유하지 않고도 응용 프로그램의 반응성과 규모가변성을 높일 수 있다.
- 간결함을 위해 이번 장의 예제들은 대부분 동기적 메서드들을 사용했지만 네트워크 입출력이 관여하는 대부분의 시나리오에서는 비동기 Read/Write 연산이 더 나은 선택임을 기억하기 바란다.
스트림 읽기와 쓰기