전체 페이지뷰

2017년 1월 16일 월요일

C# File I/O

이번에는 System.IO 네임스페이스의 클래스들을 이용해서 파일의 정보를 다루고 읽고 쓰는 법을 알아봅니다.
Systme.IO 네임스페이스에는 파일과 데이터 스트림을 읽고 쓸 수 있게 해주는 수많은 클래스들이 제공됩니다.(참고 MSDN)

이 중에서 먼저 파일, 디렉터리 정보를 다루는 방법부터 알아봅니다.

File : 단일 파일에 대한 만들기, 복사, 삭제, 이동 및 열기를 위한 정적 메서드를 제공하고 FileStream 개체 만들기를 지원합니다.
FileInfo : FIle과 유사하나 정적메소드가 아닌 인스턴스 메소드를 제공합니다.
Directory : 디렉터리와 하위 디렉터리에서 만들기, 이동 및 열거를 위한 정적 메서드를 제공합니다.
DirectoryInfo: 역시 Directory와 유사하나 정적이 아닌 인스턴스 메소드를 제공합니다.


File and FileInfo

모든 메소드와 속성을 다 살펴볼 수는 없고 대표적인 메소드와 속성을 살펴봅니다. 
*MSDN은 정말 방대합니다. 도저히 다 알고 있을 수도 없으므로 작업을 하기 전에 늘 참고하여 내가 원하는 기능을 가진 클래스가 있는가를 알아보는 것이 중요하겠습니다.

기능 File FileInfo
생성 Create() Create()
복사 Copy() CopyTo()
삭제 Delete() Delete()
아동 Move() MoveTo()
존재여부확인 Exists() Exists
속성조회 GetAttributes() Attributes

File은 static이므로 메소드만이 존재하고, 인스턴스를 만드는 FileInfo에는 메소드와 속성이 존재합니다. 위의 표에 ()가 없는 것은 속성입니다.
각각 코딩 스타일을 살펴봅니다.

생성

File :
FileStream fs = File.Create("a.dat");

FileInfo:
FileInfo file = new FileInfo("a.dat");
FileStream fs = file.Create();

복사

File:
File.Copy("a.dat", "b.dat");

FileInfo:
FileInfo f1 = new FileInfo("a.dat");
FileInfo f2 = f1.CopyTo("b.dat");

삭제

File:
File.Delete("a.dat");

Fileinfo:
FileInfo file = new Fileinfo("a.dat");
file.Delete();

이동

File:
File.Move("a.dat", "b,dat");

FileInfo:
FileInfo file = new FileInfo("a.dat");
file.MoveTo("b.dat");

존재 여부 확인

File:
If (File.Exists("a.dat"))
    // ....

FileInfo:
FileInfo file = new FIleInfo("a.dat");
if (file.Exists)
    // ...

속성 조회

File:
Console.WriteLine(File.GetAttributes("a.dat"));

FileInfo:
FileInfo file = new FIleInfo("a.dat");
Console.WriteLine(file.Attributes);


Directory and DirectoryInfo

이번에는 Directory와 DirectoryInfo의 대표적 메소드, 속성을 알아봅니다.

기능 Directory Directoryinfo
생성 CreateDirectory() Create()
삭제 Delete() Delete()
이동 Move() MoveTo()
존재여부확인 Exists() Exists
속성조회 GetAttributes() Attributes
하위디렉토리 조회 GetDirectories() GetDirectories()
하위파일조회 GetFiles() GetFiles()


생성

Directory:
DirectoryInfo dir = Directory.CreateDirectory("a");

DirectoryInfo:
DirectoryInfo dir = new Directoryinfo("a");
dir.Create();

삭제

Directory:
Directory.Delete("a");

DirectoryInfo:
DirectoryInfo dir = new Directoryinfo("a");
dir.Delete();

이동

Directory:
Directory.Move("a", "b");

DirectoryInfo:
DirectoryInfo dir = new Directoryinfo("a");
dir.MoveTo("b");

존재 여부 확인

Directory:
if (Directory.Exists("a"));
    // ...

DirectoryInfo:
DirectoryInfo dir = new Directoryinfo("a");
if (dir.Exists);
    // ...

속성 조회

Directory:
Console.WriteLine(Directory.GetAttributes("a"));

DirectoryInfo:
DirectoryInfo dir = new Directoryinfo("a");
Console.WriteLine(dir.Attributes);

하위 디렉터리 조회

Directory:
string[] dirs = Directory.GetDirectories("a");

DirectoryInfo:
DirectoryInfo dir = new Directoryinfo("a");
DirectoryInfo[] dirs = dir.GetDirectories();

하위 파일 조회

Directory:
string[] files = Directory.GetFiles("a");

DirectoryInfo:
DirectoryInfo dir = new Directoryinfo("a");
FileInfo[] files = dir.GetFiles();


Stream

스트림은 말 그대로 데이터의 흐름입니다. 저장소와 저장소 사이에 파이프라인을 붙이는 것이라고 봐도 무방할 것 같습니다. 예를 들어 메모리에서 하드디스크로 데이터를 옮길 때에 이 둘 사이에 스트림을 형성합니다. 그리고 메모리의 데이터를 바이트 단위로 하드로 옮깁니다.

이 과정을 다루는 클래스는 System.IO.Stream에 정의되어 있습니다. 이 클래스는 추상 클래스이므로 직접 인스턴스를 만들어 사용할 수는 없고, 이를 상속받은 파생클래스로 작업이 이뤄집니다. FileStream, BufferedStream, MemoryStream 등이 그것입니다.

FileStream의 인스턴스를 만드는 법은 다음과 같습니다.

Stream fs1 = new FileStream("a.dat", FileMode.Create);  //새파일 생성
Stream fs2 = new FileStream("b.dat", FileMode.Open);  // 파일 열기
Stream fs3 = new FileStream("c.dat", FileMode.OpenOrCreate); //열거나 없으면 생성
Stream fs4 = new FileStream("d.dat", FileMode.Truncate); //파일 비워서 열기
Stream fs5 = new FileStream("e.dat", FileMode.Append);  //덧붙이기 모드로 열기

인스턴스를 만들었으면 파일을 읽고 쓸 차례입니다.
쓰는데 이용되는 메소드로 Write() WriteByte()가 있습니다.
이 메소드에 쓰이는 매개변수는 byte또는 byte[]입니다. 파일을 읽고 쓰는 기본 단위가 byte라는 것입니다. 이래서는 C#에서 쓰이는 많은 데이터형을 처리하기가 어렵습니다.

따라서 BitConverter이라는 클래스를 이용하여 데이터를 byte형으로 바꾸거나 byte에 담긴 데이터를 다른 형식으로 변환해줄 수가 있습니다(BitConverter Class).

다음은  int형식의 데이터를 변환하여 파일에 쓰는 예입니다.

int value = -16;
Stream fs = new FileStream("a.dat", FileMode.Create);  //파일 스트림 생성
Byte[] bytes = BitConverter.GetBytes(value);   //값을 byte배열로 변환
fs.Write(bytes, 0, bytes.Length); // byte 배열을 파일에 기록
fs.Close(); // 파일스트림 닫기

읽기 역시 Read(), ReadByte() 메소드로 유사한 과정을 거칩니다.

byte[] bytes = new byte[8];
Stream fs = new FileStream("a.dat", FileMode.Open);  //파일 스트림 생성
fs.Read(bytes, 0, bytes.Length);  // 데이터를 읽어 bytes에 저장
int value = BitConverter.ToInt32(bytes, 0);  // bytes의 값을 int로 변환
fs.Close();

이런 식입니다.

예제를 하나 만들어 사용방법을 알아봅니다.
using System;
using System.IO;
namespace IOExample
{
    class Program
    {
        static void Main(string[] args)
        {
            int value = 123454321;
            Console.WriteLine("{0,-1}: {1}""Original value",value);
            string path = @"c:\temp\a.dat";
            //같은 이름 파일이 존재하면 지운다.
            if (File.Exists(path))
            {
                File.Delete(path);
            }
            // 지정 경로에 파일 생성
            Stream writeStream = new FileStream(path, FileMode.Create);
            byte[] wBytes = BitConverter.GetBytes(value);  //바이트로 변환
            Console.Write("{0,-14}: ""Byte array");
            foreach (byte b in wBytes) //바이트값 출력
                Console.Write("{0:2} ", b);
            Console.WriteLine();
            writeStream.Write(wBytes, 0, wBytes.Length);
            writeStream.Close();
            // 읽기 
            Stream readStream = new FileStream(path, FileMode.Open);
            byte[] rBytes = new byte[4];
            int i = 0;
            while (readStream.Position < readStream.Length)
                rBytes[i++= (byte)readStream.ReadByte();
            int convertedValue = BitConverter.ToInt32(rBytes, 0);
            Console.WriteLine("{0,-13} : {1}""Data read", convertedValue);
            readStream.Close();
        }
    }
}
cs

위의 예제에서 readStream에 Position이라는 속성이 사용되었습니다. 바이트 단위로 현재 읽어들이고 있는 위치를 뜻하는 속성입니다. Write()나 Read()를 사용할 때는 순차 접근 방식이 사용되므로 포지션을 옮겨가며 한 바이트씩 읽어들이게 됩니다.

Positon 속성이 있다는 얘기는 그 Positon을 변경하면 앞에서부터 순차적인 아닌 random Access가 가능하다는 뜻이겠죠. Seek()을 이용하면 파일 내의 임의의 위치로 옯겨가는 일이 가능해집니다.

readStream.Seek(5, Seekorigin.Current);
이렇게 하면 현재 위치로부터 5바이트 뒤로 이동하라는 뜻이 됩니다.


BinaryWriter, BinaryReader

FileStream은 모든 기능을 가지고 있지만 사용하기에 매우 불편합니다. byte형식의 데이터만 다룰 수 있다는 점이 그러합니다. 이를 돕기 위해 만들어진 것이 BinaryWriter와 BinaryReader입니다. BinaryWriter/Reader 클래스에 FileStream 인스턴스를 넘겨주면 2진 데이터의 형태로 BitConverter 없이 데이터 처리가 가능합니다. 만일 FileStream이 아닌 NetworkStream 인스턴스를 넘겨주면 네트워크로 보낼수도 있습니다.

BinaryWriter의 사용 방법을 살펴보겠습니다.

BinaryWriter writer = new BinaryWriter( new FileStream("a.dat", FileMode.Create));
writer.Write(123);
writer.Write("Wow!");
writer.Write(1.35);  // 모든 형식을 오버로딩하고 있음
writer.Close();

BinaryReader도 대동소이합니다.

BinaryReader reader = new BinaryReader( new FileStream("a.dat", FileMode.Open));
int a = reader.ReadInt32();
string b = reader.ReadString();
double c = reader.ReadDouble();  // 형식별로 다른 이름의 메소드를 제공
reader.Close();

역시 예제를 만들어 보겠습니다.
using System;
using System.IO;
namespace BinaryExample
{
    class Program
    {
        static void Main(string[] args)
        {
            BinaryWriter writer = new BinaryWriter(new FileStream("a.dat", FileMode.Create));
            writer.Write(int.MaxValue);
            writer.Write("Wow!");
            writer.Write(uint.MaxValue);
            writer.Write(double.MaxValue);
            writer.Close();
            BinaryReader reader = new BinaryReader(new FileStream("a.dat", FileMode.Open));
            Console.WriteLine("File Size : {0} bytes",reader.BaseStream.Length);
            Console.WriteLine("{0}",reader.ReadInt32());
            Console.WriteLine("{0}",reader.ReadString());
            Console.WriteLine("{0}",reader.ReadUInt32());
            Console.WriteLine("{0}",reader.ReadDouble());
            reader.Close();
        }
    }
}
cs

결과)
File Size : 21 bytes
2147483647
Wow!
4294967295
1.79769313486232E+308

StreamWriter, StreamReader

텍스트 파일을 다루는 클래스입니다. Binary의 경우와 거의 동일하므로 바로 예제를 만들어 보겠습니다.

using System;
using System.IO;
namespace BinaryExample
{
    class Program
    {
        static void Main(string[] args)
        {
            string path = @"c:\temp\a.txt";
            if (File.Exists(path))
            {
                File.Delete(path);
            }
            
            StreamWriter writer = new StreamWriter(new FileStream(path, FileMode.Create));
            writer.WriteLine(int.MaxValue);
            writer.WriteLine("Wow!");
            writer.WriteLine(uint.MaxValue);
            writer.WriteLine(double.MaxValue);
            writer.Close();
            StreamReader reader = new StreamReader(new FileStream(path, FileMode.Open));
            Console.WriteLine("File Size : {0} bytes",reader.BaseStream.Length);
            
            while (reader.EndOfStream == false)
            {
                Console.WriteLine(reader.ReadLine());
            }
            reader.Close();
        }
    }
}
cs


Serialization

기본 형식을 읽고 쓰는 방법은 알았는데 클래스나 구조체같이 사용자가 정의한 데이터형은 어떻게 할까요? 

C#에서는 직렬화(Serialization)이라는 방법으로 객체의 상태를 바이너리(뿐만 아니라 XML, JSON으로도 가능)로 바꿔줍니다.

그 방법은 아주 간단하여 저장하고자 하는 클래스 앞에 [Serializable]이라는 Attribute를 붙여주기만 하면 됩니다.

그러면 Stream클래스와 BinaryFormatter를 이용해 저장할 수 있는 형식이 됩니다.

[Serializable]
class MyClass 
{
    //
}

그 이후 다음과 같은 형식으로 사용 가능합니다.

Stream fs = new FileStream("a.dat", FileMode.Create);
BinaryFormatter serializer = new BinaryFormatter();

MyClass obj = new MyClass();

serializer.Serialize(fs, obj);
fs.Close();

BinaryFormatter는 System.Runtime.Serialization.Formatters.Binary 네임스페이스에 정의되어 있습니다.

읽어 들이는 과정도 유사합니다.

Stream fs1 = nee FileStream("a.dat", FileMode.Open);
BinaryFormatter deserializer = new BinaryFormatter();

MyClass obj = (MyClass)deserializer.Deserialize(fs1);
fs1.Close();

하면 됩니다.

만일 클래스 중간에 제외하고 싶은 필드가 있다면 그  필드 앞에 [NonSerialized]로 수식해주면 됩니다.

댓글 없음:

댓글 쓰기