1. 소개
파일 작업의 안전성은 바이러스나 해킹 방지뿐만 아니라 오류, 잠금, 접근 권한, 잘못된 경로 등 여러 골칫거리를 잘 처리하는 것을 의미합니다. 부주의하면 데이터 손실, 프로그램 정지, 이상한 버그나 악명 높은 예외인 IOException을 만나게 됩니다.
우리가 잘 다루고 싶어하는 전형적인 상황들:
- 파일이 없는데 읽으려고 한다(또는 반대로: 이미 생성돼 있는데 '없을 때만 생성' 모드로 쓰려고 한다).
- 다른 프로그램이 파일을 열어 잠겨 있다.
- 사용자에게 파일이나 폴더에 대한 읽기/쓰기 권한이 없다.
- 파일 경로가 잘못됐거나 금지된 문자를 포함한다.
- 작업이 예기치 않게 중단된다(예: 디스크 공간 부족).
- 나쁜 습관: 열린 파일을 닫지 않고 놔두는 것.
다행히 .NET은 이런 문제들을 해결할 도구를 제공합니다. 간단하지만 안전벨트처럼 항상 사용하는 게 중요합니다.
2. 파일을 안전하게 다루는 기본 원칙
파일 존재와 권한을 미리 확인하자
파일을 읽기 전에, 특히 경로를 사용자로부터 받는다면 파일이 존재하는지(File.Exists 등) 확인하세요:
string path = "test.txt";
if (!File.Exists(path))
{
Console.WriteLine("오류: 파일을 찾을 수 없습니다.");
return;
}
쓰기 전에 디렉터리가 있는지, 쓰기 권한이 있는지 확인하거나 예외를 상위로 던져 처리하도록 설계하세요.
스트림을 절대 열어 두지 말자
스트림을 자동으로 닫으려면 using 키워드를 사용하세요 — 이게 기본적인 베스트 프랙티스입니다:
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// 여기서 파일은 이미 닫혀 있음 — 오류가 나도 안전!
이렇게 하면 잠김이나 리소스 누수를 피할 수 있습니다.
항상 예외를 잡아라
모든 파일 작업은 예외를 던질 수 있습니다. 파일이 방금 있었더라도 접근 순간에 삭제되거나 이동될 수 있으니 try-catch를 사용합시다:
try
{
using var reader = new StreamReader("data.txt");
string line = reader.ReadLine();
Console.WriteLine(line);
}
catch (FileNotFoundException)
{
Console.WriteLine("파일을 찾을 수 없습니다.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("파일에 대한 접근 권한이 없습니다.");
}
catch (IOException ex)
{
Console.WriteLine($"입출력 오류: {ex.Message}");
}
사용자 입력을 믿지 마라
사용자가 경로를 입력하면 실수하거나 프로그램을 깨뜨리려 할 수도 있습니다. 경로를 검증하세요 (예: Path.GetInvalidPathChars() 참고):
try
{
string userPath = Console.ReadLine()!;
if (string.IsNullOrWhiteSpace(userPath))
{
Console.WriteLine("경로는 비어 있을 수 없습니다!");
return;
}
// 추가: 금지된 문자 체크
foreach (char c in Path.GetInvalidPathChars())
if (userPath.Contains(c))
{
Console.WriteLine("경로에 허용되지 않는 문자가 포함되어 있습니다.");
return;
}
// 이제 파일을 안전하게 다룰 수 있음
}
catch (Exception ex)
{
Console.WriteLine("경로 검증 중 오류: " + ex.Message);
}
3. 실전 예제: '성인스럽게' 파일 읽기
간단한 학습 프로젝트를 강화해 봅시다. 사용자 입력으로 파일 이름을 받아 내용을 출력하는데, 오류 처리와 인코딩도 고려합니다.
Console.Write("파일 경로를 입력하세요: ");
string? path = Console.ReadLine();
// 경로 검증
if (string.IsNullOrWhiteSpace(path))
{
Console.WriteLine("경로는 비어 있을 수 없습니다!");
return;
}
foreach (char c in Path.GetInvalidPathChars())
if (path.Contains(c))
{
Console.WriteLine("경로에 허용되지 않는 문자가 포함되어 있습니다.");
return;
}
// 파일 읽기 시도
try
{
if (!File.Exists(path))
{
Console.WriteLine("파일을 찾을 수 없습니다.");
return;
}
// 명시적으로 인코딩 지정 (예: UTF-8)
using var reader = new StreamReader(path, Encoding.UTF8);
string content = reader.ReadToEnd();
Console.WriteLine("파일 내용:");
Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("파일을 읽을 권한이 없습니다.");
}
catch (IOException ex)
{
Console.WriteLine("입출력 오류: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("예상치 못한 오류: " + ex.Message);
}
이 코드는 이름 입력과 읽기 사이에 파일이 사라져도 죽지 않습니다: 예외가 잡힙니다. using 블록을 나가면 스트림은 자동으로 닫힙니다 — 오류가 나도 마찬가지입니다.
4. 쓰기 시: 데이터 손실을 피하자
파일을 쓰는(특히 덮어쓰기 모드, false 같은 경우) 동안 중요한 데이터를 실수로 지울 위험이 있습니다. 몇 가지 팁:
기존 파일을 덮어쓰려는지 확인하자
파일이 이미 존재하면 사용자에게 묻는 것도 좋습니다:
if (File.Exists(path))
{
Console.WriteLine("주의: 파일이 이미 존재합니다. 덮어쓰시겠어요? (y/n)");
string answer = Console.ReadLine()!;
if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
return;
}
필요한 곳엔 append 모드를 사용하자
using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": 새로운 로그 항목.");
이렇게 하면 기존 내용이 사라지지 않습니다.
5. 레이스 컨디션과 충돌 방지
여러 프로그램(예: 여러분의 C# 앱과 Notepad++)이 동시에 파일을 열 수 있습니다. 이로 인해 오류가 발생할 수 있습니다. 기본적으로 StreamReader와 StreamWriter는 FileShare 기반의 공유 모드를 사용합니다 — 누가 읽고 쓸 수 있을지 결정하죠.
명시적으로 제어할 수 있습니다:
using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// 읽기...
- FileShare.Read: 다른 프로세스는 읽기만 가능.
- FileShare.None: 공유 금지 — 파일을 단독으로 점유.
파일을 동시에 읽고 쓰게 허용하려면 적절한 공유 모드를 쓰되, 결과를 잘 이해하고 있어야 합니다.
6. 예기치 못한 예외와 대처
모든 권장사항을 지켜도 디스크 공간 부족, 고장난 USB, 바이러스 등으로 문제가 생길 수 있습니다. 몇 가지 '이색' 예외와 처리 방법:
- PathTooLongException — 경로가 너무 길다 (구형 Windows에서는 260자 제한).
- DirectoryNotFoundException — 지정한 디렉터리를 찾을 수 없음.
- DriveNotFoundException — 예: 경로가 "Z:\\file.txt"인데 Z 드라이브가 없음.
- NotSupportedException — 경로에 허용되지 않는 조합이 포함된 경우 등.
이런 예외는 별도로 처리하거나 최소한 로깅해 두는 것이 좋습니다.
7. 원자적 쓰기를 위해 임시 파일 사용
자주 발생하는 문제: 파일을 쓰다가 프로그램이 중간에 죽으면 파일이 깨집니다. 프로페셔널한 앱들은 '원자적' 쓰기 전략을 씁니다:
- 먼저 임시 파일에 내용을 씀 (예: "file.txt.tmp").
- 임시 파일을 대상 파일로 이동하거나 대체함(파일 시스템 수준에서 보통 원자적임) — File.Replace 사용.
- 이렇게 하면 새 파일이 완전히 교체되거나 전혀 변경되지 않아서 반쪽짜리 손상이 없음.
예제:
string tempPath = path + ".tmp";
try
{
using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
// 임시 파일에 전부 기록
writer.Write(contentForSave);
// 성공적으로 쓰면 메인 파일을 교체
File.Replace(tempPath, path, null); // 임시 파일을 대상 파일로 옮기며 대체 (원자적 작업)
}
catch (Exception ex)
{
Console.WriteLine("파일 저장 중 오류: " + ex.Message);
// 필요하면 tempPath 파일을 삭제
}
많은 편집기와 오피스 프로그램이 이렇게 해서 데이터 일관성을 지킵니다.
8. 안전한 접근을 위한 래퍼 클래스 사용
.NET은 파일 작업에 유용한 헬퍼 메서드를 제공합니다. 예를 들어 File.ReadAllText나 File.WriteAllText는 파일을 열고 읽고 닫는 작업을 자동으로 해 줍니다. 다만 이들도 항상 try-catch로 감싸야 합니다:
try
{
string text = File.ReadAllText("settings.json", Encoding.UTF8);
// 데이터 처리...
}
catch (Exception ex)
{
Console.WriteLine("파일 작업 중 오류: " + ex.Message);
}
큰 파일을 다룰 때는 스트림을 사용해 부분적으로 읽어 메모리를 과도하게 쓰지 않도록 하세요.
GO TO FULL VERSION