상세 컨텐츠

본문 제목

[C#] 파일과 디렉터리 다루기

.NET/C#

by 클리엘 클리엘 2021. 10. 21. 17:08

본문

728x90

1. 파일 및 디렉터리

 

.NET 에서는 파일과 디렉터리를 다룰 수 있도록 아래와 같은 클래스를 제공하고 있습니다.

File 파일을 다룰 수 있는 정적메서드 사용
FileInfo 정적메서드 대신 인스턴스메서드 사용
Directory 디렉토리를 다룰 수 있는 정적메서드 사용
DirectoryInfo 정적메서드 대신 인스턴스메서드 사용

정적 메서드를 사용할지 인스턴스를 통해 사용할지의 결정사항은 정확히 정해진 바 없으나 보통 파일이나 디렉터리의 복사, 이동 등 한두 가지 작업만 하는 경우라면 간단하게 정적 메서드를 사용하며 하나의 파일이나 디렉터리에 대해 여러 가지 작업을 수행하는 경우는 인스턴스를 사용합니다.

 

정적 메서드나 인스턴스 메서드나 메서드의 이름은 약간씩 다르지만 결국하는 일은 모두 비슷합니다. 우선 처음부터 파일을 생성하는 방법부터 알아보자면 파일은 Create이름의 메서드를 사용합니다.

//File
FileStream fs = File.Create("sample.txt");

//FileInfo
FileInfo fi = new FileInfo("sample.txt");
FileStream fs = fi.Create();

파일 복사는 Copy이름의 메서드이며

//File
File.Copy("sample.txt", "sample2.txt");

//FileInfo
FileInfo fi = new FileInfo("sample.txt");
FileInfo fi2 =  fi.CopyTo("sample2.txt");

삭제는 Delete이름을 가진 메서드를 사용합니다.

//File
File.Delete("sample.txt");

//FileInfo
FileInfo fi = new FileInfo("sample.txt");
fi.Delete();

파일의 이동은 Move, 파일의 존재 여부는 Exists이며

//File
File.Move("sample.txt", "sample2.txt");

//FileInfo
FileInfo fi = new FileInfo("sample.txt");
fi.MoveTo("sample2.txt");
//File
if (File.Exists("sample.txt"))
{
    //파일 있음
}

//FileInfo
FileInfo fi = new FileInfo("sample.txt");
if (fi.Exists) {
    //파일 있음
}

마지막으로 파일에 관한 정보는 GetAttributes를 통해 확인합니다.

//File
WriteLine(File.GetAttributes("sample.txt"));

//FileInfo
FileInfo fi = new FileInfo("sample.txt");
WriteLine(fi.Attributes);

디렉터리 또한 파일과 유사한 방법으로 원하는 작업을 처리할 수 있습니다.

//디렉토리 생성
//Drectory
DirectoryInfo di = Directory.CreateDirectory("sample");

//DirectoryInfo
DirectoryInfo di = new DirectoryInfo("sample");
di.Create();

//디렉토리 삭제
//Directory
Directory.Delete("sample");

//DirectoryInfo
DirectoryInfo di = new DirectoryInfo("sample");
di.Delete();

//디렉토리 이동
//Directory
Directory.Move("sample", "sample2");

//DirectoryInfo
DirectoryInfo di = new DirectoryInfo("sample");
di.MoveTo("sample2");

//디렉토리 존재확인
//Directory
if (Directory.Exists("sample"))
{
    //디렉토리 잇음
}

//DirectoryInfo
DirectoryInfo di = new DirectoryInfo("sample");
if (di.Exists)
{
    //디렉토리 있음
}

//하위 디렉토리
//Directory
string[] directories = Directory.GetDirectories("sample");

//DirectoryInfo
DirectoryInfo di = new DirectoryInfo("sample");
DirectoryInfo[] directories = di.GetDirectories();

//하위 파일
//Directory
string[] directories = Directory.GetFiles("sample");

//DirectoryInfo
DirectoryInfo di = new DirectoryInfo("sample");
FileInfo[] directories = di.GetFiles();

2. 파일 입출력

 

특정 파일의 내용을 읽거나 쓰는 경우에는 파일 스트림을 사용합니다. 스트림은 흐른다는 뜻으로 파일을 쓰는 경우 메모리와 하드디스크 사이에 파일 스트림을 연결하여 이 스트림을 통해 파일에 특정 내용을 기록하게 됩니다. 물론 파일을 읽는 반대의 경우도 마찬가지입니다.

 

스트림이라는 개념 자체가 데이터 이동을 위한 것이므로 파일 스트림이라면 파일로의 데이터 흐름을, 네트워크 스트림이라면 네트워크를 통한 데이터의 이동을 의미합니다. 데이터가 이동하는 대상만 다를 뿐 기본적인 개념은 같은 것입니다.

 

스트림으로 데이터를 흘려보내는 경우 처음부터 끝까지 데이터를 순서대로 흘려보내는 방법을 '순차 접근'이라고 하며  하드디스크와 같은 저장매체처럼 헤드의 움직임에 따라 원하는 데이터만을 이동하여 읽어 들이는 방법을 '임의 접근'이라고 합니다. 파일 스트림에는 System.IO.Stream 클래스가 사용되며 파일의 입력과 출력, 순차 접근과 임의 접근방식 등을 모두 지원합니다. FileStream을 통해 새로운 파일을 생성하는 방법은 다음과 같습니다.

Stream sf = new FileStream("sample.txt", FileMode.Create);

FileStream클래스는 System.IO.Stream클래스로부터 상속받은 클래스입니다. System.IO.Stream클래스 자체가 추상 클래스이므로 직접 사용할 수 없고 해당 클래스로부터 파생된 클래스가 사용됩니다. 이렇게 하는 이유는 데이터 흐름을 다루는 게 파일뿐만 아니라 네트워크나 메모리 등 다양하기 때문입니다. 스트림에 관한 기본적인 모델만을 갖추고 있고 이를 파생하여 각각의 환경에 맞는 데이터 흐름을 다루고자 하는 것입니다.

 

FileStream을 통해 인스턴스를 생성할 때 FileMode라는 생성자 매개변수를 전달하고 있는데 파일을 어떤 형태로 생성할지에 대한 것이며 다음과 같은 항목을 설정할 수 있습니다.

Create 새로운 파일을 생성합니다.
Open 기존 파일을 열어봅니다.
OpenOrCreate 파일열기를 시도하고 파일이 없으면 새로 생성합니다.
Truncate 기존 파일이 있는 경우 파일을 비웁니다.
Append 기존 파일에 새로운 내용을 추가합니다.

위와 같이 파일에 대한 인스턴스를 생성하고 나면 파일을 쓰기 위해 Write()라는 메서드를 사용합니다. 하지만 메서드를 살펴보면 "안녕하세요."와 같은 문자열을 입력할 수 없고 바이트 배열을 통해 메서드를 사용할 수 있도록 되어 있습니다.

 

따라서 기록하고자 하는 내용을 Byte로 변환하고 변환된 값을 다시 Write()메서드로 전달함으로써 데이터를 기록합니다.

class Program
{
    static void Main(string[] args)
    {
        Stream sf = new FileStream("sample.txt", FileMode.Create);
        byte[] content = Encoding.UTF8.GetBytes("안녕하세요.");

        sf.Write(content);
        sf.close();
    }
}

참고로 문자열 이외에 숫자나 bool형식 등을 byte배열로 바꾸고자 하는 경우에는 BitConvert클래스의 GetBytes()메서드를 사용할 수도 있고 그 반대의 경우도 가능합니다.

 

파일에 대한 작업이 모두 끝나면 마지막에는 Close()메서드를 호출하여 파일을 잡고 있는 자원을 해제합니다. 만약 Close() 메서드의 호출을 신경 쓰고 싶지 않다면 Using을 사용해도 됩니다.

class Program
{
    static void Main(string[] args)
    {
        using(Stream sf = new FileStream("sample.txt", FileMode.Create))
		{
        	byte[] content = Encoding.UTF8.GetBytes("안녕하세요.");

        	sf.Write(content);
        }
    }
}

Using은 현재 자신의 블록 마지막에서 자동으로 Disponse() 메서드를 호출해 줍니다. 따라서 Close()와 같이 별도로 자원 해제에 필요한 메서드를 호출해 주지 않아도 됩니다. 참고로 Using에 사용할 수 있는 대상은 Disponse()를 구현하는 모든 객체에서 사용될 수 있습니다.

 

using은 다음과 같은 구현도 가능한데 이런 경우는 using이 사용된 블록이 벗어나는 경우에 Diponse() 메서드를 호출합니다.

class Program
{
    static void Main(string[] args)
    {
        using Stream sf = new FileStream("sample.txt", FileMode.Create);
        byte[] content = Encoding.UTF8.GetBytes("안녕하세요.");

        sf.Write(content);
    }
}

반대로 데이터를 읽어오는 경우는 Read()메서드를 사용합니다. 이 메서드 역시 byte배열로 읽은 데이터를 반환합니다. 따라서 읽어 들인 데이터를 원하는 형식으로 변환하는 과정을 거쳐야 합니다.

class Program
{
    static void Main(string[] args)
    {
        Stream sf = new FileStream("sample.txt", FileMode.Open);

        byte[] content = new byte[16];

        sf.Read(content, 0, content.Length);

        WriteLine(Encoding.Default.GetString(content));

        sf.Close();
    }
}

● 순차 접근

 

위에서 파일을 작성하거나 읽어올 때 모두 데이터를 처음부터 끝까지 쓰거나 읽는 순차 접근방식을 사용했습니다. 하지만 Stream의 Seek()메서드를 사용하면 임의로 원하는 지점의 바이트부터 읽거나 쓸 수 있는 임의 접근방식을 구현할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        Stream sf = new FileStream("sample.txt", FileMode.Open);

        byte[] content = new byte[14];

        sf.Seek(3, SeekOrigin.Current); //처음 2바이트를 건너뛰고 3바이트부터 읽을것.
        sf.Read(content, 0, content.Length);

        WriteLine(Encoding.Default.GetString(content)); //녕하세요.

        sf.Close();
    }
}

참고로 Stream클래스에는 Position이라는 속성이 마련되어 있는데 Position속성에서 나타내는 값은 스트림이 데이터를 읽을 때나 쓸 때 현재 바라보는 데이터의 위치를 나타내는 것입니다. 따라서 1byte씩 읽어나 쓸 때마다 Position값이 1씩 증가하는 것을 알 수 있습니다. 위에서 Seek() 메서드를 사용해 임의의 위치로 시작점을 옮겼는데 이렇게 하면 Position의 위치도 같이 바뀌게 되며 Position속성을 통해 현재 위치 값을 확인할 수 있습니다.

 

● BinaryWriter / BinaryReader

 

파일을 작성할 때 Stream의 Write() 메서드와 파일을 읽을 때 Read() 메서드를 사용했는데 byte배열을 직접 다뤄야 하는 불편함이 존재했습니다. 이 단점을 해소하고자 사용하는 클래스가 BinaryWrite와 BinaryReader클래스입니다. 이들 클래스는 2진 데이터를 기록하거나 스트림으로부터 이진 데이터를 읽기 위한 클래스로 FileStream클래스와 함께 사용한다면 파일에 대한 읽기/쓰기를, NetworkStream클래스와 함께 사용한다면 네트워크로의 이진 데이터를 보내거나 받는 용도로 사용할 수 있습니다. 즉, 데이터 읽기/쓰기를 위한 도우미 클래스이며 주어진 데이터를 알아서 바이트로 변환해 주는 역할을 합니다.

 

다음은 BinaryWriter클래스를 사용해 데이터를 쓰는 방법입니다.

class Program
{
    static void Main(string[] args)
    {
        Stream sf = new FileStream("sample.txt", FileMode.Create);
        using BinaryWriter bw = new BinaryWriter(sf);
        bw.Write("안녕하세요.");
    }
}

이전 방식에 비해 너무나도 간단하게 파일을 생성하였습니다. Write에는 문자열뿐만 아니라 십진수와 같은 숫자 등 다양한 데이터 형식을 넣어줄 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        Stream sf = new FileStream("sample.txt", FileMode.Open);
        using BinaryReader bw = new BinaryReader(sf);
        WriteLine(bw.ReadString());
    }
}

파일을 읽는 것 또한 동일하게 사용할 수 있습니다. 단지 파일을 여는 생성자 매개변수 옵션과 메서드만 달라질 뿐입니다.

 

● StreamWriter / StreamReader

 

텍스트를 저장하고 읽기 위한 클래스입니다. 이전 예제에서도 텍스트를 저장하고 읽기를 시도했지만 순수 텍스트만을 위한 것이 아닌 바이트 단위의 데이터를 우회적으로 사용한 것에 불과합니다.

 

사용방법은 이전 예제와 매우 유사합니다. 먼저 파일에 쓰는 경우입니다.

class Program
{
    static void Main(string[] args)
    {
        Stream sf = new FileStream("sample.txt", FileMode.Create);
        using StreamWriter bw = new StreamWriter(sf);
        
        bw.WriteLine("안녕하세요.");
        bw.WriteLine("반갑습니다.");
    }
}

파일을 읽는 경우에는 StreamReader클래스를 사용합니다.

class Program
{
    static void Main(string[] args)
    {
        Stream sf = new FileStream("sample.txt", FileMode.Open);
        using StreamReader br = new StreamReader(sf);
        
        while (br.EndOfStream == false) //파일의 끝네 도달할때까지
        {
             WriteLine(br.ReadLine()); //한줄씩 내용을 읽어들이니다.
        }
    }
}

EndOfStream속성은 파일의 끝에 도달하면 true가 됩니다.

 

● BinaryFormatter

 

마이크로 소프트는 .NET 5.0부터 보안상의 이유로 해당 클래스를 사용하지 말 것을 권고하고 있습니다. 대신 JsonSerializer나 XmlSerializer를 사용하는 것이 좋습니다.

 

● JsonSerializer

 

사용자가 작성한 클래스 등과 같은 복합 데이터 형식을 Json형식으로 직렬 화하는 경우 JsonSerializer클래스가 사용됩니다.

class Program
{
    static void Main(string[] args)
    {
        using Stream sf = new FileStream("sample.txt", FileMode.Create);

        Car c = new Car();
        c.Speed = 80;

        JsonSerializer.SerializeAsync(sf, c);
    }
}

class Car
{
    public int Speed { get; set; }
    public string Color { get; set; }
}

위 예제는 Stream클래스와 함께 직렬 화한 데이터를 파일에 쓰는 방법을 보여주고 있습니다. 직렬화 하는 데는 SerializeAsync() 같은 비동기 메서드뿐만 아니라 Serializer()처럼 일반적인 메서드도 사용할 수 있습니다. 반대로 직렬화 데이터를 역직렬화 하려면 Deserialize() 메서드를 사용합니다.

class Program
{
    static void Main(string[] args)
    {
        using Stream sf = new FileStream("sample.txt", FileMode.Open);
        using StreamReader br = new StreamReader(sf);

        string jsonString = br.ReadLine();

        Car c = JsonSerializer.Deserialize<Car>(jsonString);

        WriteLine(c.Speed); //80
    }
}

방금 전 파일에 저장한 직렬화 데이터를 파일에서 다시 읽어 역직렬화를 통해 복원하였습니다.

728x90

'.NET > C#' 카테고리의 다른 글

[C#] TCP/IP 통신  (0) 2021.10.28
[C#] Thread(스레드)와 Task(태스크)  (0) 2021.10.24
[C#] 파일과 디렉터리 다루기  (0) 2021.10.21
[C#] dynamic 형식  (0) 2021.10.21
[C#] 리플렉션과 애트리뷰트  (0) 2021.10.20
[C#] LINQ  (0) 2021.10.19

관련글 더보기

댓글 영역