Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C#] 스트림 (Stream) 본문

Programming/C#

[Programming/C#] 스트림 (Stream)

scii 2020. 9. 28. 00:33

스트림은 영어로 시내, 강 또는 도로의 차선을 뜻하는 단어로, 파일을 다룰 때 말하는 스트림은 "데이터가 흐르는 통로"를 뜻한다. 메모리에서 하드디스크로 데이터를 옮길때에는 먼저 이 스트림을 만들어 둘 사이를 연결한 뒤에 메모리에 있는 데이터를 바이트 단위로 하드디스크로 옮겨 넣는다. 하드디스크에서 메모리로 데이터를 옮길 때도 마찬가지다. 하드디스크와 메모리 사이에 스트림을 놓은 후 파일에 담겨 있는 데이터를 바이트 단위로 메모리로 차례차례 옮겨온다.

스트림은 데이터의 "흐름"이기 때문에 스트림을 이용하여 파일을 다룰 때는 처음부터 끝까지 순서대로 읽고 쓰는 것이 보통이다 (이것을 순차 접근(Sequential Access 방식이라고 함). 이러한 스트림의 구조는 네트워크나 데이터 백업 장치의 데이터 입/출력 구조와도 통하기 때문에 스트림을 이용하면 파일이 아닌 네트워크를 향해서 데이터를 흘려 보낼 수 있고 테이프 백업 장치를 통해 데이터를 기록하거나 읽을 수 있다.

임의의 주소에 있는 데이터에 접근하는 것을 가리켜 "임의 접근 (Random Access) 방식"이라고 한다.

System.IO.Stream 클래스는 그 자체로 입력 스트림, 출력 스트림의 역할을 모두 할 수 있으며 파일을 읽고 쓰는 방식 역시 순차 접근 방식과 임의 접근 방식 모두를 지원한다.
단, Stream 클래스는 추상 클래스이기 때문에 이 클래스의 인스턴스를 직접 만들어 사용할 수는 없고 이 클래스로부터 파생된 클래스를 이용해야 한다. Stream 클래스가 이렇게 만들어진 이유는 스트림이 다루는 다양한 매체나 장치들에 대한 파일 입출력을 스트림 모델 하나로 다룰 수 있도록 하기 위함이다. 가량 Stream의 파생 클래스인 FileStream은 저장 장치와 데이터를 주고받도록 구현되어 있지만, 역시 Stream을 상속하는 NetworkStream은 네트워크를 통해 데이터를 주고 받도록 구현되어 있다.

다음 그림은 Stream 클래스와 이를 상속하는 다양한 파생 클래스들의 계보를 나타내는 그림이다.

Stream 클래스와 이를 상속하는 파생 클래스들의 계보

FileStream 클래스의 인스턴스는 다음과 같이 생성하면 된다.

// 새 파일 생성
Stream stream = new FileStream("a.dat", FileMode.Create);

// 파일 열기
Stream stream = new FileStream("a.dat", FileMode.Open);

// 파일이 존재하면 열고 존재하지 않으면 새로운 파일 생성
Stream stream = new FileStream("a.dat", FileMode.OpenOrCreate);

// 파일을 비워서 열기
Stream stream = new FileStream("a.dat", FileMode.Truncate);

// 덧붙이기 모드로 열기
Stream stream = new FileStream("a.dat", FileMode.Append);

FileStream 클래스는 파일에 데이터를 기록하기 위해 Stream 클래스로부터 물려받은 다음 두 가지 메소드를 오버라이딩하고 있다.

public override void Write(
    byte[] array,    // 쓸 데이터가 담겨 있는 byte 배열
    int offset,      // byte 배열 내의 시작 오프셋
    int count        // 기록할 데이터의 총 길이 (단위는 바이트)
);

public override void WriteByte(byte value);

각종 데이터 형식을 byte 배열로 변환해주는 BitConvert 클래스가 있다. 이 클래스는 임의의 형식의 데이터를 byte의 배열로 변환해주기도 하지만, byte의 배열에 담겨 있는 데이터를 다시 임의의 형식으로 변환해줄 수도 있다.

BitConverter를 이용하여 long 형식의 데이터를 파일에 기록하는 예이다.

long someValue = 0x123456789ABCDEF0;

// 1) 파일 스트림 생성
Stream outStream = new FileStream("a.dat", FileModel.Create);

// 2) long형식인 someValue를 byte 배열로 변환
byte[] wBytes = BitConverter.GetBytes(someValue);

// 3) 변환한 byte 배열을 파일 스트림을 통해 파일에 기록
outStream.Write(wBytes, 0, wBytes.Length);

// 4) 파일 스트림 닫기
outStream.Close();

FileStream을 통해 파일에서 데이터를 읽어오는 방법

FileStream은 파일에서 데이터를 읽기 위해 Stream으로부터 물려받은 다음 두 개의 메소드를 구현하고 있다.

public override int Read(
    byte[] array,    // 읽은 데이터를 담을 byte 배열
    int offset,      // byte 배열 내의 시작 오프셋
    int count        // 읽을 데이터의 최대 바이트 수
);

public override int ReadByte();

파일에서 데이터를 읽는 Read()와 ReadByte() 메소드는 이름만 다를 뿐 Write()와 WriteByte() 메소드하고 똑같다.

byte[] rBytes = new byte[8];

// 1) 파일 스트림 생성
Stream inStream = new FileStream("a.dat", FileMode.Open);

// 2) rBytes의 길이만큼(8바이트) 데이터를 읽어 rBytes에 저장
inStream.Read(rBytes, 0, rBytes.Length);

// 3) BitConverter를 이용하여 rBytes에 담겨 있는 값을 long 형식으로 변환
long readValue = BitConverter.ToInt64(rBytes, 0);

// 4) 파일 스트림 닫기
inStream.Close();

FileStream 예제

using System;
using System.IO;

namespace CSharpExample
{
    internal class MainApp
    {
        static int Main(string[] args)
        {
            long someValue = 0x123456789ABCDEF0;
            Console.WriteLine("{0,-1} : 0x{1:X16}", "Original Data", someValue);

            Stream outStream = new FileStream("a.dat", FileMode.Create);
            // someValue의 8바이트를 바이트 배열에 나눠 넣는다.
            byte[] wBytes = BitConverter.GetBytes(someValue);

            Console.Write("{0,-13} : ", "Byte array");
            foreach (byte b in wBytes)
                Console.Write("{0:X2} ", b);

            Console.WriteLine();

            outStream.Write(wBytes, 0, wBytes.Length);
            outStream.Close();

            Stream inStream = new FileStream("a.dat", FileMode.Open);
            byte[] rBytes = new byte[8];

            int i = 0;
            while (inStream.Position < inStream.Length)
                rBytes[i++] = (byte)inStream.ReadByte();

            long readValue = BitConverter.ToInt64(rBytes, 0);
            inStream.Close();

            Console.WriteLine("{0,-13} : 01{1:X16} ", "Read Data", readValue);

            return 0;
        }
    }
}


/* 결과

Original Data : 0x123456789ABCDEF0
Byte array    : F0 DE BC 9A 78 56 34 12
Read Data     : 01123456789ABCDEF0

*/
long 형식으로부터 변환된 바이트 배열의 저장 순서

16진수 123456789ABCDEF0을 바이트 단위로 쪼개면 12, 34, 56, 78, 9A, BC, DE, F0의 순서로 배열에 들어가야 할 텐데 위의 예제 프로그램의 결과를 보면 이 순서가 뒤집혀서 출력되고 있는 것을 볼 수 있다. 파일에 저장된 데이터도 딱 이순서로 저장된다. 
이것은 CLR이 지원하는 바이트 오더가 데이터의 낮은 주소부터 기록하는 "리틀 엔디안(Little Endian)" 방식이기 때문에 나타난 현상이다. 자바의 가상머신은 "빅 엔디안(Big Endian)" 바이트 오더를 지원한다.

C# 프로그램에서 만든 파일을 다른 시스템에서 읽도록 하려면 바이트 오더의 차이를 반드시 고려해야 한다. 그 반대도 마찬가지다. 그리고 네트워크를 통해 전송하는 데이터에 대해서도 같은 고려가 필요하다.


Stream 클래스에는 Position이라는 프로퍼티가 있다. Stream 클래스는 상속하는 FileStream클래스도 이 프로퍼티를 가지고 있다.
Position 프로퍼티는 현재 스트림의 읽는 위치 또는 쓰는 위치를 나타낸다. 가령 Position이 3이라면 파일의 3번째 바이트에서 쓰거나 읽을 준비가 되어 있는 상태이다.

FileStream 객체를 생성할 때 Position이 0이 되고, WriteByte() 메소드를 호출할 때마다 데이터를 기록한 후 자동으로 Position이 1씩 증가한다. 이것은 Write() 메소드를 호출할 때도, 그리고 Read() 메소드나 ReadByte() 메소드를 호출할 때도 마찬가지다. 단 Write()나 ReadByte()는 쓰거나 읽은 바이트 수만큼 Position이 증가한다. 
따라서 여러 개의 데이터를 기록하는 일은 그냥 Write()나 WriteByte() 메소드를 차례차례 호출하는 것으로 충분하다. 이렇게 파일을 순차적으로 쓰거나 읽는 방식을 "순차 접근 (Sequential Access)" 이라고 한다.

한편, 파일 내의 임의의 위치에 Position이 위치하도록 할 수도 있다. 이것을 "임의 접근 (Random Access)" 방식이라고 한다. Seek() 메소드를 호출하거나 Position 프로퍼티에 직접 원한느 값을 대입하면 지정한 위치로 점프하여 읽기/쓰기를 위한 준비를 할 수 있다.

Stream outStream = new FileStream("a.dat", FileMode.Create);
// ...
// 현재 위치에서 5바이트 뒤로 이동
outStream.Seek(5, SeekOrigin.Current);
outStream.WriteByte(0x05);

순자적 접근 방식과 임의 접근 방식의 예제

using System;
using System.IO;

namespace CSharpExample
{
    internal class MainApp
    {
        static int Main(string[] args)
        {
            Stream outStream = new FileStream("a.dat", FileMode.Create);
            Console.WriteLine($"Position : {outStream.Position}");

            outStream.WriteByte(0x01);
            Console.WriteLine($"Position : {outStream.Position}");

            outStream.WriteByte(0x02);
            Console.WriteLine($"Position : {outStream.Position}");

            outStream.WriteByte(0x03);
            Console.WriteLine($"Position : {outStream.Position}");

            outStream.Seek(5, SeekOrigin.Current);
            Console.WriteLine($"Position : {outStream.Position}");

            outStream.WriteByte(0x04);
            Console.WriteLine($"Position : {outStream.Position}");

            outStream.Close();

            return 0;
        }
    }
}


/* 결과

Position : 0
Position : 1
Position : 2
Position : 3
Position : 8
Position : 9

*/

0번지에는 0x01, 1번지에는 0x02, 2번지에는 0x03이 기록되어 있고, 3, 4, 5, 6, 7 다섯 개의 번지를 건너뛰어 8번지에 0x04가 들어간 것을 확인할 수 있다.

using System;
using System.IO;

namespace CSharpExample
{
    internal class MainApp
    {
        static int Main(string[] args)
        {
            if (!File.Exists("a.dat"))
                return -1;

            Stream inStream = new FileStream("a.dat", FileMode.Open);
            byte? result = null;

            Console.WriteLine($"Position : {inStream.Position}");
            result = (byte)inStream.ReadByte();
            Console.WriteLine($"Byte : {result}");

            Console.WriteLine($"Position : {inStream.Position}");
            result = (byte)inStream.ReadByte();
            Console.WriteLine($"Byte : {result}");

            Console.WriteLine($"Position : {inStream.Position}");
            result = (byte)inStream.ReadByte();
            Console.WriteLine($"Byte : {result}");

            inStream.Seek(5, SeekOrigin.Current);

            Console.WriteLine($"Position : {inStream.Position}");
            result = (byte)inStream.ReadByte();
            Console.WriteLine($"Byte : {result}");

            return 0;
        }
    }
}


/* 결과

Position : 0
Byte : 1
Position : 1
Byte : 2
Position : 2
Byte : 3
Position : 8
Byte : 4

*/
Comments