1. 파일 다루기란?
.NET은 파일과 디렉토리 정보를 손쉽게 다룰 수 있도록 System.IO 네임스페이스를 지원
2. 파일 다루기
2.1 파일 정보와 디렉토리 정보 다루기
File : 파일 생성, 복사, 삭제, 이동, 조회를 처리하는 정적 메소드 제공
FileInfo : File 클래스와 하는 일은 동일하지만 정적 메소드 대신 인스턴스 메소드 제공 ( 여러 작업 )
Directory : 디렉토리의 생성, 삭제, 이동, 조회를 처리하는 정적 메소드 제공
DirectoryInfo : Directory 클래스와 하는 일은 동일하지만 정적 메소드 대신 인스턴스 메소드를 제공 ( 여러 작업 )
* File/FileInfo 와 Directory/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 |