[C#] 42. 스트림(Stream)과 바이너리(byte[]), 인코딩(Encoding) 그리고 using 사용법과 IDisposable 인터페이스


Study/C#  2021. 10. 4. 15:05

안녕하세요. 명월입니다.


이 글은 C#에서 스트림(Stream)과 바이너리(byte[]), 인코딩(Encoding) 그리고 using 사용법과 IDisposable 인터페이스에 대한 글입니다.


이전 글에서 파일다루기(IO)에 대해서 설명했습니다.

link - [C#] 41. 파일 다루기(IO)와 파일 메타 데이터(FileInfo) 사용하기


여기서 String 데이터를 파일에 작성할 때 Encoding을 사용해서 byte[] 배열로 변환하고 데이터를 작성하고, 다시 파일의 내용을 읽어올 때는 byte[] 배열로 읽어와서 Encoding을 사용해서 String으로 변환하고 콘솔에 표시하는 것까지 예제로 설명했습니다.

우리가 파일을 읽어오고 작성할 때, FileStream이라는 클래스를 사용해서 작성하고 읽어 왔습니다.


먼저 Stream에 대한 설명인데, 스트림이란 일련의 데이터 배열을 뜻하는 말입니다. 즉, 한개의 데이터로는 의미가 없고 데이터의 집합 혹은 배열이 하나의 데이터로써 의미를 지닌다는 뜻입니다.

우리가 String이란 데이터를 byte로 변환하였기 때문에, UTF-8의 변환으로 인해 영문자 하나가 하나의 배열로 표현이 되었지만, 만약 영문이 아닌 한글인 경우는 몇 바이트가 될까요?

using System;
using System.IO;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 파일 메타 데이터 취득
      var file = new FileInfo("d:\\work\\test.txt");
      // FileStream 생성
      // 생성할 파일 이름, 옵션 (파일 열기), 파일 접근 권한(읽기)
      var stream = new FileStream("d:\\work\\test.txt", FileMode.Open, FileAccess.Read);
      try
      {
        // 파일 메타 데이터로 파일 크기를 취득해 와서 byte 배열 생성
        var binary = new byte[file.Length];
        // stream으로 파일 읽기
        stream.Read(binary, 0, binary.Length);
        // 데이터를 16진수로 표현하기
        foreach (var b in binary)
        {
          // 콘솔 출력
          Console.Write("{0:X2} ", b);
        }
        // 개행
        Console.WriteLine();
        // 글자수 콘솔 출력
        Console.WriteLine("Size = " + file.Length);
      }
      finally
      {
        // 스트림 닫기
        stream.Close();
      }

      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

위 예제에서 가장 마지막 0x2E(46)은 ascii 코드에서 마침표입니다. 그 외의 15바이트는 한글이네요. 즉, 한글 한글자당 3바이트의 용량을 가지고 있습니다.

여기서 이 '안'에 해당하는 데이터는 'EC 95 88' 입니다만, 하나의 byte는 값의 의미가 없습니다. 데이터의 의미를 가지려면 3byte가 합쳐져야 String에서 '안'이라는 데이터로 인식이 되는 것입니다.

이것이 스트림입니다. 하나의 데이터를 표현하기 위해, 일련의 값의 배열을 뜻하는 것입니다.


여기서 설명하기 쉽게 String의 데이터를 표현했지만, 이미지나 동영상 등등의 많은 데이터의 파일들이 있습니다만, 내용은 전부 byte로 되어 있습니다.

즉, 이 데이터들은 하나의 byte 타입의 데이터로는 의미가 없지만 byte들의 값들이 모여서 이미지가 되고 동영상이 되는 것입니다. 이것을 우리는 스트림이라고 합니다.

예로 우리가 이미지 프로그램에서 이미지를 모니터에 출력을 하기 위한 약속된 byte의 집합, 데이터 스트림이 필요하다고 이야기하는 것입니다. 동영상도 같은 의미입니다. 화면에 연속된 이미지를 출력하고 그에 맞게 스피커에 소리를 내는 데이터, 약속된 byte의 집합인 동영상 데이터라 하고 데이터 스트림이라고 표현합니다.


이런 데이터를 다루는 클래스들은 대부분 Stream을 다루는 클래스를 가지고 있습니다.

IO도 그렇고, 통신을 통한 데이터 교환을 하는 소켓에도 Stream 클래스가 있습니다. 기타 하드웨어끼리 데이터를 주고받는 것 뿐아니라 메모리에 byte[]의 형식이 아닌 스트림 형식 그대로 할당하는 MemoryStream도 있습니다.


이 스트림은 커넥션(Connection)이 존재하는 데, 데이터의 점유라고 생각하면 됩니다. 데이터 스트림은 중간에 데이터가 규약에 맞게 수정되지 않으면 읽을 수 없는 데이터가 되어 버리기 때문입니다.(Check in, Check out 기능이라고 생각하면?)

그래서 항상 이 스트림은 사용이 끝나면 Close로 커넥션(Connection)을 닫아야 합니다.


물론 프로그램이 종료되면 잠겨있던 모든 커넥션은 자동으로 종료가 됩니다만, 서버와 같이 24시간 기동되는 프로그램의 경우는 이런 리소스 커넥션도 잘 관리해야 합니다.

그래서 예전에는 try ~ finally로 많이 표현을 했습니다만, C#에서는 그것보다 훨신 더 심플한 using 키워드로 커넥션을 관리할 수 있습니다.

using System;
using System.IO;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 파일 메타 데이터 취득
      var file = new FileInfo("d:\\work\\test.txt");
      // FileStream 생성
      // 생성할 파일 이름, 옵션 (파일 열기), 파일 접근 권한(읽기)
      // using은 IDisposable 인터페이스를 상속한 클래스로써, using의 스택 영역이 종료되면 자동으로 Close 호출됩니다.
      using (var stream = new FileStream("d:\\work\\test.txt", FileMode.Open, FileAccess.Read))
      {
        // 파일 메타 데이터로 파일 크기를 취득해 와서 byte 배열 생성
        var binary = new byte[file.Length];
        // stream으로 파일 읽기
        stream.Read(binary, 0, binary.Length);
        // 데이터를 16진수로 표현하기
        foreach (var b in binary)
        {
          // 콘솔 출력
          Console.Write("{0:X2} ", b);
        }
        // 개행
        Console.WriteLine();
        // 글자수 콘솔 출력
        Console.WriteLine("Size = " + file.Length);
      }

      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

결과는 같은 결과입니다만 try ~ finally 보다는 뭔가 더 심플해 보입니다.

using 키워드는 IDisposable 인터페이스를 상속받은 클래스에서 스택이 종료되면 자동으로 Dispose 함수가 호출이 됩니다.

using System;

namespace Example
{
  // IDisposable 인터페이스 상속
  class Test : IDisposable
  {
    // Close 함수
    public void Close()
    {
      // 콘솔 출력
      Console.WriteLine("Close!!!");
    }
    // IDisposable 인터페이스의 Dispose 함수 재정의
    public void Dispose()
    {
      // Close 함수 호출
      Close();
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // using 키워드, Test 인스턴스 생성
      using(var test = new Test())
      {
        // 콘솔 출력
        Console.WriteLine("Hello world");
      }

      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

위 예제는 using 키워드를 사용하기 위해 Test 클래스를 만들어 봤습니다.

Test 클래스는 IDisposable 인터페이스를 상속받고 Dispose 함수를 재정의합니다. 대충, FileStream 클래스는 using안에서 위의 형태로 움직인다고 생각하면 됩니다.


다시 돌아와서 인코딩에 대해서 설명하겠습니다.

위에서 스트림이란 일련의 값 배열이라고 설명했습니다. 그러니깐 String이란 데이터를 byte[]로 변환을 해야 하는 데, ToCharArray로 변환하는 방법과 Encoding.UTF8.GetBytes로 변환하는 방법이 있습니다.

using System;
using System.Text;

namespace Example
{
  class Program
  {
    // 출력 함수
    static void Print(dynamic val)
    {
      // 배열을 반복문으로
      foreach (var b in val)
      {
        // 콘솔 출력
        Console.Write("{0:X2} ", (byte)b);
      }
      // 개행
      Console.WriteLine();
    }
    // 실행 함수
    static void Main(string[] args)
    {
      // 출력 ToCharArray
      Print("Hello world".ToCharArray());
      // 출력 Encoding.UTF8.GetBytes
      Print(Encoding.UTF8.GetBytes("Hello world"));
      // 개행
      Console.WriteLine();
      // 출력 ToCharArray
      Print("안녕하세요".ToCharArray());
      // 출력 Encoding.UTF8.GetBytes
      Print(Encoding.UTF8.GetBytes("안녕하세요"));

      // 아무 키나 누르면 종료
      Console.WriteLine("Press Any key...");
      Console.ReadLine();
    }
  }
}

참고로 char와 byte의 관계는 unsigned char, 부호없는 char 형식이 byte 형식입니다. 그러니 char와 byte은 같은 값을 취급하는 자료형입니다.

String에서 ToCharArray 함수를 써서 char로 변환을 할 수 있는데 영문이나 ascii 코드로 표현할 수 있는 값은 Encoding으로 변환 한 것과 비슷한 값으로 출력이 됩니다.

그러나 한글은 ascii코드로 표현이 안됩니다. 즉, C#에서 뿐아니라 메모장 등의 다른 프로그램에서도 읽을 수 있도록 하기 위해서는 인코딩이 필요합니다. 위에서는 UTF-8의 Encoding 타입으로 변환했습니다.

메모장에서 문자 타입을 확인해 보면 UTF-8로 작성되어 있는 것을 확인할 수 있습니다.

그래서 일반적으로 문자를 byte[]로 변환할 때는 Encoding 클래스를 사용해서 byte[] 배열로 변환하여 FileStream을 사용합니다.


여기까지 C#에서 스트림(Stream)과 바이너리(byte[]), 인코딩(Encoding) 그리고 using 사용법과 IDisposable 인터페이스에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.