Menu

C# 파일 쓰기와 읽기: File, StreamWriter, StreamReader

C#에서 파일에 쓰고 파일을 읽는 방법: 파일 전체를 위한 File.WriteAllText, ReadAllText, AppendAllText, WriteAllLines, ReadAllLines, 큰 파일을 위한 StreamWriter와 StreamReader, Path.Combine을 쓰는 경로, 디렉터리, 인코딩, 그리고 파일 코드가 처리해야 하는 예외를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

System.IO 네임스페이스에는 파일 작업에 필요한 모든 것이 있습니다. 대부분의 작업에는 정적 File 클래스의 호출 한 번이면 충분합니다. 파일을 열고, 읽거나 쓰고, 닫습니다. 큰 파일이나 세밀한 제어에는 StreamReader와 StreamWriter를 쓰세요. 아래의 모든 예제에 using System.IO;를 추가하세요.

파일 전체 쓰기와 읽기

File.WriteAllText는 주어진 텍스트로 파일을 만듭니다(또는 기존 파일을 덮어씁니다). File.ReadAllText는 파일 전체를 문자열 하나로 반환합니다. File.AppendAllText는 끝에 추가합니다:

출력:

True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False

"notes.txt" 같은 상대 경로는 프로세스의 현재 디렉터리를 기준으로 해석되며, 이것이 항상 프로그램이 있는 폴더는 아닙니다(서비스, 예약된 작업, 테스트 실행기는 다른 곳에서 시작할 수 있습니다). 애플리케이션과 함께 배포되는 파일은 AppContext.BaseDirectory로부터 경로를 만드세요.

WriteAllText는 묻지 않고 덮어씁니다. 이전 내용을 잃는 것이 문제라면 먼저 File.Exists를 확인하거나, 임시 파일에 쓴 뒤 File.Move로 제자리에 옮기세요.

줄 단위: WriteAllLines, ReadAllLines, ReadLines

한 줄에 하나씩 레코드가 있는 파일은 흔해서 전용 메서드가 있습니다. WriteAllLines는 각 문자열 뒤에 줄바꿈을 붙여 쓰고, ReadAllLines는 string[]를 반환합니다:

출력:

4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00

두 읽기 메서드의 차이는 크기에서 중요합니다. ReadAllLines는 반환하기 전에 파일 전체를 메모리로 읽습니다. ReadLines는 순회하면서 읽는 지연 IEnumerable<string>을 반환하므로, 5GB 로그에 대한 foreach가 몇 킬로바이트만 씁니다. LINQ와 함께 쓰면 ReadLines(...).Where(...).Take(10)은 일치 항목 열 개를 얻은 뒤 읽기를 멈춥니다.

Split(',')로 CSV를 파싱하는 것은 직접 만든 단순한 파일에는 동작합니다. 실제 CSV에는 쉼표를 담은 따옴표 필드가 있으므로, 그런 경우에는 CsvHelper 같은 라이브러리를 쓰세요.

decimal.Parse의 InvariantCulture에 주목하세요. 이것이 없으면 독일어나 포르투갈어로 설정된 컴퓨터에서 "4.50"을 파싱할 때 점을 천 단위 구분 기호로 읽습니다.

StreamWriter와 StreamReader

File 메서드는 호출할 때마다 파일을 열고 닫습니다. 시간에 걸쳐 여러 조각을 쓰거나, LINQ 없이 메모리에 담기에 너무 큰 파일을 읽으려면 스트림을 한 번 여세요. 예외가 던져져도 닫히도록 항상 using으로 감싸세요. 닫히지 않은 작성기는 버퍼를 끝내 비우지 않을 수 있고, 파일이 비거나 잘린 채로 남습니다.

출력:

2: WARN  disk 85% full
4: ERROR connection refused

ReadLine은 파일 끝에서 null을 반환하며, 이것이 while 루프를 끝냅니다. StreamWriter는 출력을 버퍼링하고 버퍼가 차거나, Flush()를 호출하거나, dispose될 때 디스크에 씁니다. using 블록이 마지막 것을 보장합니다.

C# 8부터는 using 선언이 추가 중괄호 없이 둘러싼 블록 끝에서 스트림을 dispose합니다:

using var writer = new StreamWriter(path);   // C# 8: disposed when the method returns
writer.WriteLine("INFO  server started");

경로와 디렉터리

경로는 문자열 이어 붙이기 대신 Path.Combine으로 만드세요. 운영체제에 맞는 구분 기호(Windows에서는 \, Linux와 macOS에서는 /)를 넣어 주고 중복시키지 않습니다. Path 클래스는 경로를 분해하기도 하고, Directory는 폴더를 만들고 나열합니다:

출력:

february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False

함정이 하나 있습니다. Path.Combine의 뒤쪽 인수가 루트 경로이면(/나 \로 시작하거나, Windows에서 드라이브 문자로 시작하면) 그 앞의 모든 것이 버려지므로 Path.Combine("reports", "/2026")은 /2026입니다. 상대 경로 조각을 넘기거나, 아무것도 버리지 않는 Path.Join(.NET Core 3.0 이상)을 쓰세요.

Directory.GetFiles는 파일 시스템이 주는 순서대로 파일을 반환하며, 이 순서는 Windows와 Linux에서 다르므로 순서가 중요하면 결과를 정렬하세요. Directory.EnumerateFiles는 지연 버전으로, ReadLines와 ReadAllLines의 관계와 같습니다. 하위 폴더를 포함하려면 SearchOption.AllDirectories를 넘기세요.

오류 처리

파일 작업은 프로그램 밖의 이유로 실패합니다. 파일이 없거나, 폴더가 없거나, 다른 프로세스가 열고 있거나, 디스크가 가득 찼거나, 권한이 잘못된 경우입니다. 각각 자기 예외 타입이 있습니다. FileNotFoundException과 DirectoryNotFoundException은 IOException에서 파생되므로 그보다 먼저 잡으세요. UnauthorizedAccessException은 그렇지 않으므로 자기 catch가 필요합니다:

출력:

missing file, using defaults
missing folder, using defaults
theme=dark

읽기 전의 File.Exists가 더 간단해 보이지만 try의 필요성을 없애 주지는 않습니다. 확인과 읽기 사이의 순간에 파일이 사라지거나 다른 프로그램에 잠길 수 있습니다. Exists는 무엇을 할지 정하는 데 쓰고, 실제로 일어나는 일을 견디는 데는 catch를 쓰세요.

인코딩

File.WriteAllText, WriteAllLines, StreamWriter는 기본적으로 바이트 순서 표시(BOM) 없는 UTF-8로 쓰고, 읽기 메서드는 바이트 순서 표시가 있으면 그것으로 UTF-8, UTF-16, UTF-32를 감지합니다. 파일이 특정 형식이어야 한다면 Encoding을 넘기세요:

출력:

plain.txt   16 bytes, reads back unchanged: True
bom.txt     19 bytes, reads back unchanged: True
utf16.txt   30 bytes, reads back unchanged: True

é와 ã는 UTF-8에서 각각 2바이트를 차지하므로 14글자에 16바이트가 필요하며, 읽기 메서드가 바이트 순서 표시를 알아보므로 세 파일 모두 올바르게 다시 읽힙니다. Encoding.UTF8은 기본값처럼 보이지만 바이트 순서 표시를 추가하며, 일부 도구(예전 CSV 가져오기 도구, 셸 스크립트)는 이를 첫 줄 시작의 깨진 문자로 보여 줍니다. 바이트 순서 표시 없는 명시적 UTF-8에는 new UTF8Encoding(false)를 쓰세요. 잘못된 인코딩으로 파일을 읽어도 예외는 나지 않고 대체 문자가 만들어지므로, 직접 만들지 않은 파일은 어떤 인코딩인지 알아내세요.

비동기 파일 I/O

웹 앱과 UI 코드에서 디스크 I/O로 스레드를 막으면 그 스레드를 낭비합니다. .NET Core 2.0 이상에는 File 메서드의 비동기 버전이 있습니다:

await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");

StreamReader.ReadLineAsync와 StreamWriter.WriteLineAsync도 스트림에서 같은 방식으로 동작합니다.

흔한 실수

  • 스트림을 dispose하지 않기. using이 없으면 버퍼의 텍스트가 디스크에 끝내 도달하지 않을 수 있고, 가비지 컬렉터가 실행될 때까지 파일이 잠긴 채로 남습니다.
  • + "\\" +로 경로 만들기. Linux와 macOS에서 깨집니다. Path.Combine을 쓰세요.
  • ReadAllText나 ReadAllLines로 거대한 파일 불러오기. ReadLines나 StreamReader로 스트리밍하세요.
  • 현재 디렉터리에 의존하기. 프로그램이 어떻게 시작되었는지에 따라 달라집니다. 절대 경로나 AppContext.BaseDirectory를 쓰세요.
  • 컴퓨터의 문화권으로 파일의 숫자 파싱하기. 데이터 파일에는 CultureInfo.InvariantCulture를 넘기세요.
  • File.Exists가 예외를 막아 주리라 기대하기. 확인과 사용 사이에 파일이 바뀔 수 있습니다.

자주 묻는 질문

C#에서 파일에 텍스트를 쓰려면 어떻게 하나요?

File.WriteAllText("notes.txt", text);는 파일을 만들거나 이미 있으면 덮어쓰고, 문자열을 UTF-8로 쓰고, 파일을 닫습니다. 줄 모음에는 File.WriteAllLines(path, lines)를, 교체하지 않고 끝에 추가하려면 File.AppendAllText(path, text)를 쓰세요. 셋 모두 System.IO에 있습니다.

C#에서 텍스트 파일은 어떻게 읽나요?

string text = File.ReadAllText("notes.txt");는 파일 전체를 문자열 하나로 읽고, string[] lines = File.ReadAllLines(path);는 줄로 나눕니다. 큰 파일에는 모든 것을 불러오는 대신 한 번에 한 줄씩 읽는 File.ReadLines(path)를 foreach에서 쓰거나 StreamReader를 쓰세요.

C#에서 파일에 이어서 쓰려면 어떻게 하나요?

File.AppendAllText(path, text)는 파일 끝에 텍스트를 추가하며 파일이 없으면 만듭니다. File.AppendAllLines(path, lines)는 줄에 대해 같은 일을 합니다. 스트림이라면 추가 모드로 여세요: new StreamWriter(path, append: true). 텍스트를 이어 쓸 때는 Environment.NewLine이나 \n을 직접 추가해야 한다는 점을 기억하세요.

C#에서 파일을 한 줄씩 읽으려면 어떻게 하나요?

foreach (string line in File.ReadLines(path)) { ... }는 파일을 한 번에 한 줄씩 스트리밍하므로, 기가바이트 크기의 로그에도 메모리 사용량이 일정하게 유지됩니다. 예전 방식은 using 블록 안의 StreamReader와 while ((line = reader.ReadLine()) != null)입니다. 큰 파일에는 ReadAllLines를 피하세요. 모든 줄을 먼저 배열에 불러옵니다.

C#에서 파일이 있는지 어떻게 확인하나요?

File.Exists(path)는 파일이 있고 프로그램이 볼 수 있으면 true를 반환하고, Directory.Exists(path)는 폴더에 대해 같은 일을 합니다. 예외를 피하려고 이에 의존하지는 마세요. 확인과 읽기 사이에 파일이 삭제되거나 잠길 수 있으므로, 실제 파일 작업 주위에서 여전히 FileNotFoundException과 IOException을 처리하세요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기