CodeGym /행동 /C# SELF /파일 다룰 때 자주 나오는 예외들

파일 다룰 때 자주 나오는 예외들

C# SELF
레벨 38 , 레슨 0
사용 가능

1. 소개: 왜 파일들이 "말썽"을 부릴까?

가끔 문서를 열었는데 시스템이 갑자기 알려줘요: "파일을 찾을 수 없습니다". 또는 뭔가 저장하려다가 "접근이 거부되었습니다"라는 메시지를 받기도 하고요. 이런 경우가 바로 파일 시스템 자체에서 문제가 생긴 상황이에요. 인코딩 문제로 글자가 깨지는 것과는 달리, 여기선 파일 시스템 자체의 상황이 원인입니다.

파일 시스템을 큰 도서관으로, 애플리케이션을 사서라고 생각해보세요. 파일을 요청하면 여러 응답이 올 수 있어요:

  • 책(파일)이 없다 — 단순히 존재하지 않습니다.
  • 책이 있어야 할 구역 자체가 없다 — 필요한 디렉터리가 없음.
  • 책이 "잠겨있다"거나 다른 사람에게 대출되어 있다 — 파일이 다른 프로세스에서 사용 중이거나 접근 권한이 없음.
  • 선반이 꽉 찼다 — 새 파일을 생성할 디스크 공간이 부족함.
  • 또는 예를 들어 DVD 반납 코너에 책을 넣으려 한다 — 지원되지 않는 작업을 시도함.

C#에서는 이런 상황들이 예외로 표현됩니다. 개발자의 임무는 프로그램이 그냥 "터지게" 내버려 두는 게 아니라, 예상 가능한 문제를 미리 생각해서 우아하게 처리하는 거예요. 사용자에게 이해하기 힘든 오류 메시지 덜컥 보여주고 싶진 않겠죠.

우리는 이미 try-catch 구조를 알고 있어요. 이건 예외를 "잡아서" 프로그램이 강제 종료되는 대신 적절한 조치를 취하게 해주는 안전망입니다.

// 이건 우리 익숙한 예제, 57강에서의 복습
try
{
    // 여기에 예외를 일으킬 수 있는 코드를 작성해
    // 예: 파일을 읽으려는 시도
}
catch (Exception ex) // 모든 예외를 잡음
{
    // 여기서 에러를 처리함
    Console.WriteLine($"앗, 오류가 발생했어요: {ex.Message}");
}

오늘은 파일 작업 시 발생하는 특정 예외들에 대해 더 깊게 들어갈게요. 그러면 파일 시스템이 "말썽"을 부려도 프로그램이 더 견고하게 동작하게 만들 수 있습니다.

예외는 버그가 아니라 SOS 신호야!

중요한 점: 예외는 항상 당신 코드의 버그를 뜻하지 않아요. 종종 외부 환경과의 상호작용에서 무언가 잘못되었다는 신호입니다. 파일 시스템은 그런 외부 환경의 전형적인 예예요. 파일을 읽는 코드가 완벽해도 사용자가 파일을 프로그램이 읽기 전에 삭제해버리면 예외가 발생할 수 있어요. 그건 정상적인 상황입니다. 개발자의 역할은 이런 상황에 프로그램이 적절히 반응하도록 만드는 거예요.

이제 파일 작업에서 자주 보는 "SOS 신호"들을 살펴봅시다.

2. FileNotFoundException: 파일이 아예 없는 경우

파일 작업할 때 가장 흔한 예외 중 하나입니다. 지정한 경로에 파일이 존재하지 않을 때, 파일을 열거나 읽으려 하다가 발생합니다.

실생활 예: 친구한테 "C# 14 초보자용 프로그래밍" 책 좀 가져다 달라고 했는데 친구가 "그런 책 없는데?"라고 하면 그 상황과 똑같아요. 프로그램이 운영체제에 "settings.txt 파일 줘"라고 요청했는데 운영체제가 "미안, 그런 게 없어"라고 응답하는 셈입니다.

작업 관리자 앱에서 abracadabra.txt 파일을 읽으려 한다고 해봅시다. 파일이 없다면 사용자에게 알려줘야지 그냥 터지면 안 됩니다.

try
{
    using var reader = new StreamReader("abracadabra.txt");
    Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("파일을 찾을 수 없습니다: " + ex.FileName);
}

일상에서는 버스 정류장에 갔는데 버스가 안 오고, 마침 택시도 없는 그런 느낌이에요.

팁: 종종 잘못된 작업 디렉터리에서 실행해서 발생하기도 합니다(다른 디렉터리에서 실행 중인 걸 잊는 경우).

3. DirectoryNotFoundException: 폴더가 사라진 경우

이 예외는 FileNotFoundException과 비슷하지만 파일 자체가 아니라 그 파일이 있어야 할 디렉터리에 관한 문제예요. 예를 들어 경로가 "C:\MyDocuments\MyProject\Data\report.txt"인데 Data 폴더가 없다면 DirectoryNotFoundException이 발생합니다.

우리 앱에서 설정을 data 하위 폴더에 저장하려고 하고 "./data/app_settings.txt" 같은 경로를 쓴다면, 그 폴더가 없을 때 읽거나 쓸 때 이 문제가 생길 수 있어요.

DirectoryNotFoundExceptionFileNotFoundException처럼 별도로 잡을 수도 있고, 나중에 설명할 더 일반적인 IOException의 일부로 잡힐 수도 있습니다.

try
{
    using var writer = new StreamWriter(@"C:\very\strange\path\file.txt");
    writer.WriteLine("Hello world");
}
catch (DirectoryNotFoundException ex)
{
    Console.WriteLine("디렉터리를 찾을 수 없습니다!");
}

흔한 실수: 폴더는 언제든 삭제될 수 있고(예: 임시 파일 정리), 아니면 쓰기 경로가 잘못 설정된 경우가 많습니다.

4. UnauthorizedAccessException: 접근 불가!

책을 선반에 꽂으려는데 "직원 전용" 표지가 붙어있는 상황을 떠올려보세요. 그게 바로 UnauthorizedAccessException입니다! 프로그램에 해당 파일이나 디렉터리에 접근할 권한이 없을 때 발생합니다. 원인으로는:

  • 사용자 권한 부족: 일반 사용자가 쓸 수 없는 폴더(예: C:\Windows)에 파일을 쓰려고 할 때.
  • 파일이 읽기 전용으로 표시되어 있음: 읽기 전용 속성인 파일을 수정하려고 할 때.
  • 파일이 시스템 파일이거나 숨김 속성임: 그에 따른 특별 권한이 필요할 수 있습니다.

기업 환경이나 보호된 디렉터리에 설치할 때 자주 발생하는 문제예요.

예를 들어 일반 권한으로는 접근할 수 없는 시스템 폴더에 파일을 쓰려고 해봅시다. (주의: 이런 코드는 시스템 디렉터리를 어지럽힐 수 있으니 샌드박스 등에서 조심히 실행하세요.)

try
{
    using var writer = new StreamWriter("/system/settings.conf");
    writer.WriteLine("학생들에게 모든 권리를!");
}
catch (UnauthorizedAccessException ex)
{
    Console.WriteLine("파일이나 디렉터리에 대한 접근 권한이 없습니다!");
}

관리자 권한 없이 이 코드를 실행하면 보통 "이 폴더에 대한 접근이 금지되었습니다" 같은 메시지를 보게 됩니다. 관리자 권한으로 실행하면 파일이 생성될 수 있겠죠. 이 예제는 UnauthorizedAccessException를 처리해서 사용자에게 왜 동작이 실패했는지 알려주는 것이 얼마나 중요한지 보여줍니다.

5. IOException: 파일 시스템의 범용 "아차" 예외

IOException은 입출력 작업과 관련된 가장 일반적인 예외예요. 장치나 파일 시스템 자체에서 문제가 생겨서 더 구체적인 예외(FileNotFoundExceptionUnauthorizedAccessException 등)에 해당하지 않을 때 주로 발생합니다.

일반적으로 IOException이 발생할 수 있는 시나리오는:

  • 파일이 다른 프로그램에서 이미 사용 중: 예: 파일을 삭제하려는데 메모장이나 다른 프로세스가 열어두고 있는 경우.
  • 디스크가 꽉 참: 파일을 쓰기 위한 공간이 부족한 경우.
  • 손상된 파일이나 파일 시스템: 드물지만 발생할 수 있습니다.
  • 네트워크 문제: 파일이 네트워크 드라이브에 있고 연결이 끊긴 경우.
  • 파일명이나 경로가 너무 길음: (이 경우 PathTooLongException일 수 있지만 때때로 IOException으로 잡히기도 합니다.)

IOException은 여러 문제의 "만능키" 같은 존재예요. 이 예외를 잡을 때는 Message 속성을 확인해서 무슨 문제가 있었는지 자세히 보는 것이 좋습니다.

try
{
    using var file = new FileStream("busyfile.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
    // 파일을 어떤 힘으로든 열어둠
    // 동시에 다른 곳에서:
    using var writer = new StreamWriter("busyfile.txt");
    writer.WriteLine("쓰기 시도...");
}
catch (IOException ex)
{
    Console.WriteLine("입출력 오류: " + ex.Message);
}

중요: IOException은 많은 파일 관련 예외들의 기본 클래스입니다.

6. 그 밖의, 하지만 중요한 문제들

PathTooLongException

덜 흔하지만 여전히 만나는 문제예요: 경로나 파일/디렉터리 이름이 운영체제에서 허용하는 길이를 초과한 경우입니다. 예를 들어 파일 이름에 "전쟁과 평화"의 줄거리를 통째로 넣으려 하면 Windows가 난처해할 거예요.

Windows의 역사적 제한은 전체 경로에 대해 260자입니다. 최신 OS와 .NET은 "긴 경로"를 허용할 수 있지만 항상 기본으로 켜져 있진 않습니다.

try
{
    string veryLongPath = new string('a', 300); // 300자!
    using var writer = new StreamWriter(veryLongPath + ".txt");
    writer.WriteLine("파일 이름이 너무 깁니다!");
}
catch (PathTooLongException ex)
{
    Console.WriteLine("파일 이름이나 경로가 너무 깁니다!");
}

NotSupportedException

이건 드물지만 좀 기이한 경우예요: StreamReader나 FileStream 같은 생성자에 잘못된 경로 문자열을 넘겼을 때, 예를 들어 금지된 문자가 들어가 있거나 C:::\wow???\file.txt 같은 엉뚱한 경로를 사용했을 때 발생합니다.

7. 유용한 팁들

어떤 예외가 어떤 상황을 나타내는가

예외 발생 원인 상황 예시
FileNotFoundException
지정한 경로에 파일이 없음 존재하지 않는 파일을 열려는 경우
DirectoryNotFoundException
디렉터리를 찾을 수 없음 삭제된 폴더 안의 파일을 열려는 경우
UnauthorizedAccessException
파일/폴더에 대한 권한이 없음 보호된 디렉터리에 쓰기 시도
IOException
일반적인 입출력 오류 파일이 다른 프로세스에 의해 사용 중
PathTooLongException
경로나 파일명이 너무 길음 너무 긴 파일/폴더 이름
NotSupportedException
경로 형식이 잘못됨 금지된 문자가 포함된 경로

자주 발생하는 실수와 특별한 경우

예: 파일이 다른 프로세스에 의해 점유된 경우

진짜 해커처럼 Notepad에서 텍스트 파일을 열어놓고 닫지 않은 상태에서, 프로그램이 같은 파일에 쓰려고 하면 IOException (또는 공유 위반 같은 메시지)이 옵니다.

예: 접근 권한이 없는 경우

관리자 권한 없이 C:\Windows에 저장하려 하면 UnauthorizedAccessException을 보게 됩니다. 읽기 전용으로 연 파일을 수정하려 할 때도 같은 일이 일어납니다.

예: 잘못된 경로

Windows에서는 파일명에 <>:"/\|?* 같은 문자를 쓸 수 없습니다. 이런 문자로 파일을 만들려고 하면 NotSupportedException이나 ArgumentException이 발생합니다.

예: 디스크에 여유 공간이 없음

이 경우도 IOException이 발생합니다 — 예를 들어 디스크가 가득 차 있으면 쓰기가 실패합니다(그래서 가끔 Downloads 폴더 정리를 해주는 게 좋습니다).

실수 피하는 법: 에러 감지에 대한 베스트 프랙티스

  • 파일 존재 여부는 열기 시도 전에 File.ExistsDirectory.Exists로 확인하세요. 단, 확인 후에도 파일이 사라질 수 있으니(race condition) 주의해야 합니다.
  • 예외를 통째로 무시하지 마세요(단순히 catch { } 하지 마세요), 로거를 쓰든 사용자에게 표시하든 최소한 기록은 남기세요.
  • 가능하면 구체적인 예외(FileNotFoundException, DirectoryNotFoundException 등)를 잡고, 일반적인 Exception만으로 처리하지 마세요.
  • 크로스플랫폼 앱이라면 권한, 경로 포맷, 파일명 길이 등이 OS마다 다름을 고려하세요.
  • 텍스트 파일을 다룰 땐 항상 인코딩을 명시적으로 설정하세요 — 안 그러면 깜짝 놀랄 일이 생깁니다.
  • 대량 파일 작업 시에는 각 단계에서 실패 가능성을 고려한 "bulk" 처리 전략을 사용하세요.

대표 예외 요약 치트시트

예외 언제 발생? 어떻게 예방/처리?
FileNotFoundException
지정한 경로에 파일이 없음 File.Exists로 확인하거나 파일을 생성
DirectoryNotFoundException
경로에 존재하지 않는 디렉터리가 포함됨 경로를 확인하고 Directory.CreateDirectory로 디렉터리 생성
UnauthorizedAccessException
파일/폴더에 대한 권한이 없거나 읽기 전용, 다른 프로세스가 점유 적절한 사용자로 실행, ACL 확인, 파일을 제대로 닫기
IOException
입출력 전반의 오류, 파일 점유, 디스크 공간 부족 try-catch 사용, 파일을 열어두지 않기
PathTooLongException
경로나 파일명이 너무 김 경로를 줄이거나 상대 경로 사용
NotSupportedException
경로 형식이 잘못됨 경로 문자열에 금지된 문자가 없는지 검사

에러 대응 플로우차트

flowchart TD
    A[파일 작업 시도] --> B{예외가 발생했나?}
    B -- 아니오 --> C[작업 성공]
    B -- 예 --> D{어떤 예외인가?}
    D -- FileNotFound --> E[사용자에게 올바른 파일을 지정하거나 파일 생성 요청]
    D -- DirectoryNotFound --> F[존재하지 않는 디렉터리 생성]
    D -- UnauthorizedAccess --> G[사용자에게 권한을 가진 계정으로 재시작 요청]
    D -- IOException --> H[누가 파일을 점유했는지 확인하고 디스크 상태 점검]
    D -- PathTooLong --> I[경로를 줄이기]
    D -- NotSupported --> J[경로 형식 점검]
    D -- Other --> K[메시지 표시하고 로그 확인]

8. 실전 앱에서의 예시

학습용 프로젝트를 예로 들어볼게요. 유저 노트를 파일에 저장하고 다시 읽어오는 작은 프로그램이 있다고 합시다.

string notesPath = "notes.txt";

Console.Write("메모를 입력하세요: ");
string note = Console.ReadLine();

try
{
    // 메모 저장
    using var writer = new StreamWriter(notesPath, true, Encoding.UTF8);
    writer.WriteLine(note);

    // 모든 메모 읽기
    Console.WriteLine("당신의 메모:");
    using var reader = new StreamReader(notesPath, Encoding.UTF8);
    Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException)
{
    Console.WriteLine("메모 파일을 찾을 수 없습니다. 수동으로 파일을 만들어 보세요.");
}
catch (DirectoryNotFoundException)
{
    Console.WriteLine("메모 파일 경로가 잘못되었습니다. 폴더가 존재하는지 확인하세요.");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("파일을 읽거나 쓸 권한이 없습니다. 관리자 권한으로 프로그램을 실행해보세요.");
}
catch (IOException ex)
{
    Console.WriteLine("입출력 오류가 발생했습니다: " + ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine("예상치 못한 오류: " + ex.Message);
}

이 예제는 어떤 앱이든 겪을 수 있는 전형적인 상황을 보여줍니다: 파일에 데이터를 저장하려다 다시 읽는 과정에서 발생할 수 있는 다양한 문제들. 각 catch 블록은 특정 오류 클래스를 처리해서 앱이 바로 죽지 않고 사용자에게 무슨 일이 일어났는지 알려주도록 해줍니다. 이런 접근은 앱을 더 신뢰할 수 있고 친절하게 만듭니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION