2. 스트림(Stream) 다루기
stream은 byte의 배열입니다. 비록 File은 array처럼 다뤄지기는 하지만 File내부의 byte배열 위치를 통해 임의 접근이 가능하며 이는 순차적인 순서로 처리될 수 있는 byte로서 File을 처리할 수 있는 유용한 방법이 될 수 있습니다.
stream은 또한 터미널이나 램덤엑세스를 제공하지 않고 위치를 찾을 수 없는(이동할 수 없는) 소켓과 포트 같은 네트워크 리소스의 입력과 출력에도 사용됩니다. 이때에도 stream자체가 어디에서 왔는지와는 상관없이 임의의 byte를 처리할 수 있는 Code를 작성할 수 있습니다. stream을 대상으로 한 읽기/쓰기를 수행하는 Code자체도 간단하며 byte가 실제 저장되는 곳도 제어할 수 있습니다.
(1) Stream의 추상화및 구체화
.NET의 Stream이라는 추상 클래스는 Stream에 대한 모든 Type을 표현합니다. 알고 있겠지만 추상 클래스는 new 키워드를 통해 인스턴스를 생성할 수 없고 단지 상속만 가능한 클래스입니다.
FileStream, MemoryStream, BufferedStream, GZipStream, SslStream등 Stream 클래스로부터 상속받아 비슷한 방식으로 동작하는 많은 클래스들이 존재하며 이들 클래스는 모두 Stream클래스가 가진 아래와 같은 멤버를 가지게 됩니다.
CanRead, CanWrite | Stream으로 부터 데이터를 쓰거나 읽을 수 있는지의 여부를 판단합니다. |
Length, Position | byte의 길이와 Stream에서 현재 위치를 판단합니다. 다만 Stream을 상속한 몇몇 클래스에서는 예외를 발생시킬 수 있습니다. |
Dispose | 스트림을 닫고 자원을 해제합니다. |
Flush | Stream이 buffer를 가진 경우라면 buffer에 있는 byte를 Stream에 쓰고 buffer를 비웁니다. |
CanSeek | Seek()메서드를 사용할 수 있는지의 여부를 판단합니다. |
Seek | byte의 현재 position을 매개변수에 지정한 특정 position으로 이동하도록 합니다. |
Read, ReadAsync | Stream으로 부터 특정한 크기의 byte를 byte array로 읽어들이고 위치를 그 만큼 position을 이동하도록 합니다. |
ReadByte | Stream으로 부터 다음 byte를 읽고 position을 그 만큼 이동하도록 합니다. |
Write, WriteAsyn | byte array를 Stream에 쓰도록 합니다. |
WriteByte | byte를 Stream에 쓰도록 합니다. |
● Storage Stream
이미 언급했듯이 Strem byte는 저장될 수 있고 이들 저장되는 위치를 나타내기 위한 아래 3개의 스트림 클래스가 있습니다.
System.IO.FileStream | FileSystem에 byte를 저장합니다. |
System.IO.MemoryStream | Memory에 byte를 저장합니다. |
System.Net.Sockets.NetworkStream | Network환경에서 byte를 다루기 위해 사용합니다. |
FileStream은 Windows환경하에서 훨씬 더 높은 성능과 신뢰성을 갖추도록 .NET6에서 재작성되었습니다.
● Function Stream
몇몇 function stream은 그들 자체로만은 존재할 수 없고 다른 Stream과 연결되어 특정한 기능을 부여하게 됩니다.
System.Security.Cryptography.CryptoStream | Stream에 대한 암호화/복호화를 구현합니다. |
System.IO.Compression.GZipStream System.IO.Compression.DeflateStream |
Stream에 대한 압축및 압축해제를 구현합니다. |
System.Net.Security.AuthenticatedStream | Stream을 통해 자격증명을 전송합니다. |
● Stream helper
비록 낮은 수준에서 Stream으로 작업해야 하는 경우가 발생할 수 있지만 대부분의 경우 helper클래스를 통해 더 쉽게 Stream과 관련된 기능을 구현할 수 있으며 대표적으로 아래와 같은 helper클래스를 사용할 수 있습니다.
System.IO.StreamReader | 기본 Stream에서 텍스트 문자열을 읽을 수 있습니다. |
System.IO.StreamWriter | 기본 Stream에서 텍스트 문자열을 쓸 수 있습니다. |
System.IO.BinaryReader | Stream에서 .NET Type을 읽을 수 있습니다. 예를 들어 ReadDecimal()메서드의 경우 기본 Stream에서 decimal크기로 16 byte읽어들이며 ReadInt32()메서드의 경우에는 int크기로 4 byte을 기본 Stream에서 읽어들입니다. |
System.IO.BinaryWriter | 기본 Stream에서 .NET Type을 쓸 수 있습니다. |
System.Xml.XmlReader | XML 형식을 사용해 기본 Stream을 읽을 수 있습니다. |
System.Xml.XmlWriter | XML 형식을 사용해 기본 Stream을 쓸 수 있습니다. |
(2) Text Stream
Text를 작성하는 Stream의 경우에는 아래와 같은 방법으로 구현할 수 있습니다.
using System;
using static System.Environment;
using static System.IO.Path;
namespace myapp
{
class Program
{
static string[] students = new[] { "홍길동", "홍길남", "홍길영", "홍길석" };
static void Main(string[] args)
{
string textFile = Combine(CurrentDirectory, "students.txt");
StreamWriter text = File.CreateText(textFile);
foreach (string item in students)
text.WriteLine(item);
text.Close();
Console.WriteLine("파일 작성 완료");
Console.WriteLine(File.ReadAllText(textFile));
}
}
}
위 예제를 실행하면 students의 요소를 하나씩 가져와 해당 students.txt파일에 한 줄씩 한 사람의 이름을 기록하게 됩니다.
(3) XML Stream
Stream으로 XML 형식을 만들때는 아래 메서드를 사용할 수 있습니다.
WriteStartElement와 WriteEndElement | 하나의 요소가 자식요소를 가지는 경우 사용되며 항상 이 2개의 메서드가 같이 사용됩니다. |
WriteElementString | 자식요소를 가지지 않는 요소를 생성하는 경우 사용되는 메서드입니다. |
아래 예제는 배열로 부터 이름을 가져와 XML 파일의 요소로 작성하는 기능을 수행합니다.
using System;
using static System.Environment;
using static System.IO.Path;
using System.Xml;
namespace myapp
{
class Program
{
static string[] students = new[] { "홍길동", "홍길남", "홍길영", "홍길석" };
static void Main(string[] args)
{
string xmlFile = Combine(CurrentDirectory, "students.xml");
FileStream xmlFileStream = File.Create(xmlFile);
XmlWriter xml = XmlWriter.Create(xmlFileStream, new XmlWriterSettings { Indent = true });
xml.WriteStartDocument();
xml.WriteStartElement("students");
foreach (string item in students)
xml.WriteElementString("student", item);
xml.WriteEndElement();
xml.Close();
xmlFileStream.Close();
Console.WriteLine(File.ReadAllText(xmlFile));
}
}
}
// <?xml version="1.0" encoding="utf-8"?>
// <students>
// <student>홍길동</student>
// <student>홍길남</student>
// <student>홍길영</student>
// <student>홍길석</student>
// </students>
(4) 파일 자원 해제
파일에 대한 읽기/쓰기 작업을 수행하기 위해 특정 파일을 열게 되면 이것은 외부의 리소스를 사용하게 되는 것이고 이러한 비관리 리소스는 필요한 작업이 종료되면 리소스가 해제되어야 합니다. Stream과 관련된 모든 Helper Type은 IDisposable인터페이스를 구현하고 있고 따라서 자원 해제를 위한 Dispose() 메서드를 가지고 있는데 이 메서드가 반드시 호출될 수 있도록 보증하려면 다음과 같이 try ~ catch문에서 finally에 메서드를 호출하는 구문을 작성하거나
static void Main(string[] args)
{
FileStream? xmlFileStream = null;
XmlWriter? xml = null;
try
{
string xmlFile = Combine(CurrentDirectory, "students.xml");
xmlFileStream = File.Create(xmlFile);
xml = XmlWriter.Create(xmlFileStream, new XmlWriterSettings { Indent = true });
xml.WriteStartDocument();
xml.WriteStartElement("students");
foreach (string item in students)
xml.WriteElementString("student", item);
xml.WriteEndElement();
xml.Close();
xmlFileStream.Close();
Console.WriteLine(File.ReadAllText(xmlFile));
}
catch (Exception ex)
{
}
finally
{
if (xml != null)
xml.Dispose();
if (xmlFileStream != null)
xmlFileStream.Dispose();
}
}
아래와 같이 using구문을 사용할 수 있습니다.
class Program
{
static string[] students = new[] { "홍길동", "홍길남", "홍길영", "홍길석" };
static void Main(string[] args)
{
string xmlFile = Combine(CurrentDirectory, "students.xml");
using (FileStream xmlFileStream = File.Create(xmlFile))
{
using (XmlWriter xml = XmlWriter.Create(xmlFileStream, new XmlWriterSettings { Indent = true }))
{
xml.WriteStartDocument();
xml.WriteStartElement("students");
foreach (string item in students)
xml.WriteElementString("student", item);
xml.WriteEndElement();
xml.Close();
xmlFileStream.Close();
Console.WriteLine(File.ReadAllText(xmlFile));
}
}
}
}
using은 이전 예제에서 처럼 객체에 대한 null확인 후 Dispose()메서드를 호출하는 부분을 생략할 수 있습니다. 사실 using은 compiler에 의해서 catch가 없는 try-finally구문으로 바뀌게 되는데 어찌 되었건 구문을 좀 더 간소화할 수 있으므로 try-finally를 직접 작성하기보다는 using을 대신 사용할 것이 더 편리하고 코드를 간결하게 유지할 수 있을 것입니다.
참고로 using은 괄호를 생략하여 아래처럼 구문의 들여쓰기를 좀 더 유연하게 바꿀 수 있습니다.
class Program
{
static string[] students = new[] { "홍길동", "홍길남", "홍길영", "홍길석" };
static void Main(string[] args)
{
string xmlFile = Combine(CurrentDirectory, "students.xml");
using FileStream xmlFileStream = File.Create(xmlFile);
using XmlWriter xml = XmlWriter.Create(xmlFileStream, new XmlWriterSettings { Indent = true });
xml.WriteStartDocument();
xml.WriteStartElement("students");
foreach (string item in students)
xml.WriteElementString("student", item);
xml.WriteEndElement();
xml.Close();
xmlFileStream.Close();
Console.WriteLine(File.ReadAllText(xmlFile));
}
}
(5) Stream 압축
상기 예제에서 사용된 XML은 표현방식이 비교적 장황한 편이라 그 만큼 많은 크기를 가지게 됩니다. 다른 형식의 파일도 마찬가지인데 Stream에서는 이들 파일의 용량을 줄일 수 있는 알고리즘을 제공하고 있습니다.
● GZIP
GZIP은 GZipStream 클래스를 통해 구현됩니다.
using System;
using static System.Environment;
using static System.IO.Path;
using System.Xml;
using System.IO.Compression;
namespace myapp
{
class Program
{
static string[] students = new[] { "홍길동", "홍길남", "홍길영", "홍길석" };
static void Main(string[] args)
{
string xmlFile = Combine(CurrentDirectory, "students.gzip");
FileStream file = File.Create(xmlFile);
Stream compressor = new GZipStream(file, CompressionMode.Compress);
using (compressor)
{
using (XmlWriter xml = XmlWriter.Create(compressor))
{
xml.WriteStartDocument();
xml.WriteStartElement("students");
foreach (string item in students)
xml.WriteElementString("student", item);
}
}
Console.WriteLine($"압축 : {new FileInfo(xmlFile).Length}");
//압축된 파일을 읽으려면 압축을 해제해야 함
file = File.Open(xmlFile, FileMode.Open);
Stream decompressor = new GZipStream(file, CompressionMode.Decompress);
using (decompressor)
{
using (XmlReader reader = XmlReader.Create(decompressor))
{
while (reader.Read())
{
if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "student"))
{
reader.Read();
Console.WriteLine($"{reader.Value}");
}
}
}
}
}
}
}
예제는 GZipStream클래스를 통해 이전과 같은 내용의 요소를 포함하는 XML을 압축하여 생성하고 이를 읽기위해 다시 GZipStream클래스를 사용하는 과정을 보여주고 있습니다.
● Brotli
.NET Core 2.1부터 마이크로소프트는 Brotli이라는 압축 알고리즘을 포함하였습니다. 성능적으로는 GZIP알고리즘과 비슷하지만 20%가량의 더 높은 압축률을 가진 것으로 알려져 있습니다.
사용방법은 GZipStream과 다르지 않고 그저 위 예제에서 사용된 GZipStream대신 BrotliStream으로 바꿔주기만 하면 됩니다.
'.NET > C#' 카테고리의 다른 글
[C#] File 다루기 - 4. 객체의 직렬화(Serialization) (0) | 2022.06.26 |
---|---|
[C#] File 다루기 - 3. 인코딩(Encoding)과 디코딩(Decoding) (0) | 2022.06.26 |
[C#] File 다루기 - 1. 파일 시스템(Filesystem) (0) | 2022.06.24 |
[C#] 인터페이스(Interface)와 상속(Inheriting) - 7. Code분석(StyleCop) (0) | 2022.06.24 |
[C#] 인터페이스(Interface)와 상속(Inheriting) - 6. 상속(Inheriting) (0) | 2022.06.24 |