[ 공 부 ]/[ C# ]

18장. 파일 다루기

HiStar__ 2025. 1. 29. 18:20

[2025-01-29] - 게시글 최초 작성


1. 파일 다루기란?

.NET은 파일과 디렉토리 정보를 손쉽게 다룰 수 있도록 System.IO 네임스페이스를 지원


2. 파일 다루기

2.1 파일 정보와 디렉토리 정보 다루기

 File : 파일 생성, 복사, 삭제, 이동, 조회를 처리하는 정적 메소드 제공 
 FileInfo : File 클래스와 하는 일은 동일하지만 정적 메소드 대신 인스턴스 메소드 제공 ( 여러 작업 )
 Directory : 디렉토리의 생성, 삭제, 이동, 조회를 처리하는 정적 메소드 제공
 DirectoryInfo : Directory 클래스와 하는 일은 동일하지만 정적 메소드 대신 인스턴스 메소드를 제공 ( 여러 작업 )

 

* File/FileInfoDirectory/DirectoryInfo의 차이점

 - File/FileInfo의 경우, 하위 디렉토리 조회, 하위 파일 조회를 할 수 없다.

 - Directiory/DirectoryInfo의 경우,  복사를 할 수 없다.

 

// 디렉토리/파일 생성하기
using System;
using System.IO;

namespace ThisIsCSharp
{    
    class MainApp
    {
        static void OnWrongPathType(string type)
        {
            Console.WriteLine($"{type} is Wrong Type");
            return;
        }
        static void Main(string[] args)
        {
            if(args.Length == 0)
            {
                Console.WriteLine("Usage : Touch.exe <Path> [Type:File/Directory]");
                return;
            }

            string path = args[0];
            string type = "File";
            if(args.Length > 1)
            {
                type = args[1];
            }

            if(File.Exists(path) || Directory.Exists(path))
            {
                if(type == "File")
                {
                    File.SetLastWriteTime(path, DateTime.Now);
                }
                else if(type == "Directory")
                {
                    Directory.SetLastWriteTime(path, DateTime.Now);
                }
                else
                {
                    OnWrongPathType(path);
                    return;
                }
                Console.WriteLine($"Updated {path} {type}");
            }
            else
            {
                if(type == "File")
                {
                    File.Create(path).Close();
                }
                else if(type == "Directory")
                {
                    Directory.CreateDirectory(path);
                }
                else
                {
                    OnWrongPathType(path);
                    return;
                }

                Console.WriteLine($"Created {path} {type}");
            }
        }
    }
}


2.2 파일을 읽고 쓰기 위한 기본 정보

  스트림 ( System.IO.Stream )
  1. 데이터가 흐르는 통로
  ex) 데이터를 옮길 때는 스트림을 만들어 둘 사이를 연결한 후에 메모리에 있는 데이터를 바이트 단위로 옮긴다.
  2. 처음부터 끝까지 순서대로 읽고 쓰는 것이 보통이다 ( 순차 접근 )
      하드디스크는 임의의 주소에 있는 데이터에 접근 하는 것이 가능하다 ( 임의 접근 )

 

 System.IO.Stream 클래스는 추상 클래스이기 때문에 이 클래스의 인스턴스를 직접 만들어 사용 불가능.
 파생 클래스를 사용해야 한다.
 FileStream, NetworkStream, GZipStream, BufferedStream ... [msdn] System.IO.Stream

 

Stream 클래스 (System.IO)

바이트 시퀀스의 제네릭 뷰를 제공합니다. 추상 클래스입니다.

learn.microsoft.com

 

 CLR이 지원하는 바이트 오더의 경우 리틀 엔디안 방식
using System;
using System.IO;

namespace ThisIsCSharp
{    
    /* BitConverter 클래스 
     * 임의 형식의 데이터를 byte의 배열로 변환
     * byte의 배열에 담겨 있는 데이터를 다시 임의 형식으로 변환 
     */

    class MainApp
    {
        static void Main(string[] args)
        {
            long someValue = 0x1234_5678_9ABC_DEF0;
            Console.WriteLine($"{"Original Data",-1} : 0x{someValue:X16}");

            Stream outStream = new FileStream("a.dat", FileMode.Create);
            byte[] wBytes = BitConverter.GetBytes(someValue);       // Byte 단위로 넣는다

            Console.Write($"{"Byte Array", -13}");

            foreach(byte b in wBytes)
            {
                Console.Write($"{b:X2} ");
            }
            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);

            Console.WriteLine($"{"Read Data",-13} : 0x{readValue:X16}");
            inStream.Close();
        }
    }
}

// 임의 접근 예제
using System;
using System.IO;

namespace ThisIsCSharp
{    
    class MainApp
    {
        static void Write(string file)
        {
            Stream outStream = new FileStream(file, 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}");

            // Seek() 메소드를 통하여 임의의 주소로 이동
            outStream.Seek(5, SeekOrigin.Current);
            Console.WriteLine($"Position : {outStream.Position}");

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

            outStream.Close();
        }

        static void Read(string file)
        {
            Stream outStream = new FileStream(file, FileMode.Open);
            Console.WriteLine($"Position : {outStream.Position}");

            Console.WriteLine($"Position : {outStream.Position} Data : {outStream.ReadByte()}");
            Console.WriteLine($"Position : {outStream.Position} Data : {outStream.ReadByte()}");
            Console.WriteLine($"Position : {outStream.Position} Data : {outStream.ReadByte()}");

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

            outStream.Close();
        }

        static void Main(string[] args)
        {
            Write("a.dat");
            Read("a.dat");
        }
    }
}

2.3 using 선언

 네임스페이스를 참조, 파일이나 소켓을 필요한 자원을 다룰 때 사용하지만,

 파일 스트림 닫기의 실수를 줄일 때도 사용 가능! 
 using 선언을 통해 생성된 객체는 코드 블록이 끝나면서 Dispose()를 호출!

using System;
using System.IO;
using FS = System.IO.FileStream;        // 별칭 지시문

namespace ThisIsCSharp
{    
    class MainApp
    {
        static void Main(string[] args)
        {
            long someValue = 0x1234_5678_9ABC_DEF0;
            Console.WriteLine($"{"Original Data",-1} : 0x{someValue:X16}");

            // 방법 1
            using (Stream outStream = new FS("a.dat", FileMode.Create))
            {
                byte[] wBytes = BitConverter.GetBytes(someValue);

                Console.Write($"{"Byte Array",-13}");

                foreach (byte b in wBytes)
                {
                    Console.Write($"{b:X2} ");
                }
                Console.WriteLine();

                outStream.Write(wBytes, 0, wBytes.Length);
            }
            // outStream.Close();  생략 가능

            Stream inStream = new FS("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);

            Console.WriteLine($"{"Read Data",-13} : 0x{readValue:X16}");
            // inStream.Close();    생략 가능
        }
    }
}


2.4 이진 데이터 처리를 위한 BinaryWriter / BinaryReader

 BinaryWriter : 스트림에 이진 데이터를 기록하기 위한 목적

 BinaryReader : 스트림으로부터 이진 데이터를  읽어들이기 위한 목적

using System;
using System.IO;

namespace ThisIsCSharp
{    
    class MainApp
    {
        static void Main(string[] args)
        {
            using (BinaryWriter bw = new BinaryWriter(new FileStream("a.data", FileMode.Create)))
            {
                // Write는 많은 데이터 형에 대한 오버로딩
                bw.Write(int.MaxValue);
                bw.Write("Good Morning!");
                bw.Write(uint.MaxValue);
                bw.Write("안녕하세요!");
                bw.Write(double.MaxValue);
            }


            // using 선언은 8.0 부터 가능하다.
            using (BinaryReader br = new BinaryReader(new FileStream("a.data", FileMode.Open)))
            {
                Console.WriteLine($"File Size : {br.BaseStream.Length} bytes");
                Console.WriteLine($"{br.ReadInt32()}");
                Console.WriteLine($"{br.ReadString()}");
                Console.WriteLine($"{br.ReadUInt32()}");
                Console.WriteLine($"{br.ReadString()}");
                Console.WriteLine($"{br.ReadDouble()}");
            }
        }
    }
}

BinaryWriter의 Write는 첫 번째 바이트를 문자열의 길이로 사용하지만,
가변 길이 정수(VarInt)를 사용해 255자를 넘어서는 문자도 기록 가능하다. 

2.5 텍스트 파일 처리를 위한 StreamWriter / StreamReader

 ASCII 인코딩에서는 각 바이트가 문자 하나를 나타내기 때문에 바이트 오더 문제에서 벗어날 수 있다.

using System;
using System.IO;

namespace ThisIsCSharp
{    
    class MainApp
    {
        static void Main(string[] args)
        {
            using (StreamWriter sw = new StreamWriter(new FileStream("a.txt", FileMode.Create)))
            {
                // Write는 많은 데이터 형에 대한 오버로딩
                sw.WriteLine(int.MaxValue);
                sw.WriteLine("Good Morning!");
                sw.WriteLine(uint.MaxValue);
                sw.WriteLine("안녕하세요!");
                sw.WriteLine(double.MaxValue);
            }


            // using 선언은 8.0 부터 가능하다.
            using (StreamReader sr = new StreamReader(new FileStream("a.txt", FileMode.Open)))
            {
                Console.WriteLine($"File Size : {sr.BaseStream.Length} bytes");
                while(sr.EndOfStream == false)
                {
                    Console.WriteLine($"{sr.ReadLine()}");
                }
            }
        }
    }
}

2.6 객체 직렬화

 프로그래머가 직접 정의한 클래스나 구조체 같은 복합 데이터 형식을 지원하기 위하여 직렬화 라는 메커니즘을 제공
 직렬화란, 객체의 상태를 메모리나 영구 저장 장치에 저장이 가능한 0과 1의 순서로 바꿈.

 * BinaryFormatter의 보안 취약점을 발견하여 금지!
 가장 보통의 JsonSerializer를 사용한다. [ msdn ] JsonSerializer

 

JsonSerializer 클래스 (System.Text.Json)

개체 또는 값 형식을 JSON으로 직렬화하고 JSON을 개체 또는 값 형식으로 역직렬화하는 기능을 제공합니다.

learn.microsoft.com

using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ThisIsCSharp
{    
    /* 상태를 저장하지 않고 싶은 프로퍼티
     * [JsonIgnore] : 직렬화 할 때도 저장되지 않고, 역직렬화 시에도 복원되지 않음.
     */
    
    class NameCard
    {
        public string Name { get; set; }
        public string Phone { get; set; }
        [JsonIgnore] // Age는 직렬화 되지 않아서 0으로 저장된다.
        public int Age { get; set; }
    }


    class MainApp
    {
        static void Main(string[] args)
        {
            var fileName = "a.json";

            using (Stream ws = new FileStream(fileName, FileMode.Create))
            {
                NameCard nc = new NameCard()
                {
                    Name = "박상현",
                    Phone = "010-1234-4567",
                    Age = 35
                };

                string jsonString = JsonSerializer.Serialize<NameCard>(nc);
                byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonString);
                ws.Write(jsonBytes, 0, jsonBytes.Length);
            }

            // using 선언은 8.0 부터 가능하다.
            using (Stream rs = new FileStream(fileName, FileMode.Open))
            {
                byte[] jsonBytes = new byte[rs.Length];
                rs.Read(jsonBytes, 0, jsonBytes.Length);
                string jsonString = System.Text.Encoding.UTF8.GetString(jsonBytes);

                NameCard nc2 = JsonSerializer.Deserialize<NameCard>(jsonString);

                Console.WriteLine($"Name : {nc2.Name}");
                Console.WriteLine($"Phone : {nc2.Phone}");
                Console.WriteLine($"Age : {nc2.Age}");
            }
        }
    }
}

 

// 컬렉션 직렬화
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ThisIsCSharp
{    
    class NameCard
    {
        public string Name { get; set; }
        public string Phone { get; set; }
        [JsonIgnore] // Age는 직렬화 되지 않아서 0으로 저장된다.
        public int Age { get; set; }
    }


    class MainApp
    {
        static void Main(string[] args)
        {
            var fileName = "a.json";

            using (Stream ws = new FileStream(fileName, FileMode.Create))
            {
                var list = new List<NameCard>();
                
                list.Add(new NameCard() { Name = "김김김", Phone = "010-1111-1111", Age = 11 });
                list.Add(new NameCard() { Name = "이이이", Phone = "010-2222-2222", Age = 22 });
                list.Add(new NameCard() { Name = "박박박", Phone = "010-3333-3333", Age = 33 });

                string jsonString = JsonSerializer.Serialize<List<NameCard>>(list);
                byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonString);
                ws.Write(jsonBytes, 0, jsonBytes.Length);
            }

            // using 선언은 8.0 부터 가능하다.
            using (Stream rs = new FileStream(fileName, FileMode.Open))
            {
                byte[] jsonBytes = new byte[rs.Length];
                rs.Read(jsonBytes, 0, jsonBytes.Length);
                string jsonString = System.Text.Encoding.UTF8.GetString(jsonBytes);

                var list2 = JsonSerializer.Deserialize<List<NameCard>>(jsonString);

                foreach (var nc in list2)
                {
                    Console.WriteLine($"Name : {nc.Name}, Phone : {nc.Phone}, Age : {nc.Age}");
                }
            }
        }
    }
}

3. 참고자료

'[ 공 부 ] > [ C# ]' 카테고리의 다른 글

19장. Task  (0) 2025.01.30
19장. 스레드  (0) 2025.01.29
17장 Dynamic 형식  (0) 2025.01.28
16장. 애트리뷰트  (0) 2025.01.28
16장. 리플렉션  (0) 2025.01.27