1. 소개
예를 들어 두 숫자를 나누는 계산기를 만든다고 해보자. 다 잘 되다가 누군가 0으로 나누려고 하면:
int a = 10;
int b = 0;
int result = a / b; // 펑!
이 순간 프로그램이 "크래시"되고, 사용자는 무서운 에러 메시지를 보게 돼. 이게 바로 예외 상황이야 — "정상"적인 프로그램 흐름에서는 일어나면 안 되는 일이고, 특별한 처리가 필요해.
예외는 .NET이 네 프로그램에 뭔가 비정상적인 일이 생겼다고 알려주고, 실행을 멈추거나 특별하게 처리해야 한다는 신호를 주는 특별한 메커니즘이야.
런타임 에러(예: 0으로 나누기, 없는 파일에 접근, 또는 — 익숙한 — null 참조 해제)가 발생하면 .NET이 "예외 객체"를 "던져"(throw) 줘. 그러면 이걸 "잡을"(catch) 수 있는 핸들러를 찾게 돼. 그 핸들러가 뭘 해야 할지 알고 있겠지.
왜 "에러 코드"를 반환하지 않을까?
에러 코드를 반환할 수도 있지 — 옛날 언어들(Pascal, C, 그리고 몇몇 현대 API들)도 그렇게 해. 근데 이건 불편하고 위험해: 에러 코드를 체크하는 걸 까먹기 쉽고(다들 잘 될 거라 믿으니까), 어디서 문제가 생겼는지 찾기도 엄청 힘들어. 예외를 쓰면 모든 에러를 한 곳에서 잡고 유연하게 처리할 수 있어서, 코드가 온통 if-check로 더러워지지 않아.
2. 예외 "던지기"
예외는 런타임 에러가 나면 자동으로 발생할 수도 있고, throw 키워드로 직접 던질 수도 있어:
int[] arr = new int[2];
arr[10] = 5; // 예외가 자동으로 발생함 System.IndexOutOfRangeException
throw new Exception("완전 대참사!"); // Exception 예외를 직접 던짐
네 프로그램이 완벽하게 설계됐다고 생각해도, 항상 예상 못한 일이 생길 수 있어: 사용자가 파일을 닫거나, 인터넷이 끊기거나, 누가 컴퓨터 전원을 꺼버릴 수도 있지(거의 그런 느낌).
다행히도, .NET은 예외를 "던지는" 기능뿐 아니라, 예외를 잡고 처리하는 방법도 제공해 — 이건 좀 있다가 다뤄볼게.
3. 예외의 생애주기: throw에서 catch까지
코드에서 뭔가 안 좋은 일이 생기면, .NET이 "비상 절차"를 시작해:
- 예외 객체를 만든다 (예: IndexOutOfRangeException).
- 그걸 던진다 (throw 키워드로).
- 핸들러를 찾는다: 콜스택을 아래에서 위로(현재 메서드에서 호출한 쪽으로) 훑으면서, 던진 예외 타입에 맞는 catch 블록을 찾음.
- 핸들러를 찾으면 — catch 블록 안에서 실행이 계속돼.
- 아무 핸들러도 없으면 — 프로그램이 비정상 종료되고, 에러 디테일이 담긴 "콜스택"을 보여줘.
이건 마치 계단에서 공이 튀는 것처럼 — 누가 받아줄 때까지 콜스택을 계속 튀어다녀.
4. .NET의 예외 계층 구조
"부모-자식" 트리
.NET(대부분의 OOP 언어처럼)에서는 예외가 클래스 형태로 구현돼 있고, 서로 상속받으면서 계층 구조를 이뤄.
모든 예외의 공통 부모는 System.Exception이야.
System.Object
└─ System.Exception
├─ System.SystemException
│ ├─ System.NullReferenceException
│ ├─ System.IndexOutOfRangeException
│ ├─ System.DivideByZeroException
│ ├─ System.OutOfMemoryException
│ └─ ... 그리고 기타 등등
├─ System.IO.IOException
│ ├─ System.IO.FileNotFoundException
│ ├─ System.IO.DirectoryNotFoundException
│ └─ ...
├─ System.ArgumentException
│ ├─ System.ArgumentNullException
│ └─ System.ArgumentOutOfRangeException
└─ (네가 직접 만든 Exception 클래스들)
왜 클래스일까?
클래스 계층 구조 덕분에 한 번에 여러 "종류"의 문제를 잡을 수 있어 — 예를 들어 함수 인자 관련 에러(ArgumentException 및 그 자식들) 전부를 한 번에 catch할 수 있지. 그리고 네가 직접 새로운 에러 타입을 추가할 수도 있어(이건 다음 강의에서 다룰 거야).
.NET에서 자주 만나는 예외들
- NullReferenceException — null인 객체의 메서드나 프로퍼티에 접근하려고 할 때. 익숙하지?
- DivideByZeroException — 0으로 나누기.
- IndexOutOfRangeException — 배열 범위를 벗어남.
- ArgumentException, ArgumentNullException, ArgumentOutOfRangeException — 잘못된 메서드 파라미터.
- FileNotFoundException, IOException — 파일 작업 중 문제.
- InvalidOperationException — 객체의 현재 상태에서 연산이 불가능할 때.
- FormatException — 데이터 포맷이 잘못됨(예: "abcd"를 숫자로 바꾸려 할 때).
5. 예시: 다양한 예외가 "터지는" 코드
직접 코드를 보면서 어떤 예외가 발생하는지 확인해보자.
예시 1: NullReferenceException
string? str = null;
Console.WriteLine(str.Length); // 여기서 "펑!" — str이 null임
출력:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
예시 2: DivideByZeroException
int a = 42;
int b = 0;
int c = a / b; // 위험! 0으로 나눔
출력:
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
예시 3: IndexOutOfRangeException
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // 인덱스 5는 없음
여기서 인덱스가 범위를 벗어나서, 출력은 다음과 같아:
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
예시 4: FileNotFoundException
using System.IO;
string content = File.ReadAllText("secret.txt");
"secret.txt"라는 파일이 없으면, 대충 이런 메시지가 나와:
Unhandled exception. System.IO.FileNotFoundException: Could not find file '/Users/zapp/RiderProjects/ConsoleApp1/ConsoleApp1/bin/Debug/net9.0/secret.txt'.
예시 5: FormatException
string input = "십삼";
int number = int.Parse(input); // 문자열이 숫자가 아님
출력:
Unhandled exception. System.FormatException: The input string '십삼' was not in a correct format.
6. 이걸 어떻게 써먹을까?
예외가 어떻게 동작하는지 이해하는 건 진짜 중요해. 이건 그냥 "이론을 위한 이론"이 아니라, 네 프로그램의 안정성에 직접적으로 영향을 주는 실전 스킬이야. 에러를 제대로 처리할 줄 알면, 프로그램이 첫 번째 에러에서 "펑!" 하고 죽지 않고, 사용자에게 뭘 잘못했는지 친절하게 알려줄 수 있어. 그럼 훨씬 더 신뢰할 수 있고, 프로페셔널하고, 사람 친화적인 프로그램이 되지.
그리고 이건 단순히 편의성만의 문제가 아니야. 면접에서도 예외 처리 메커니즘, Exception 타입, 그리고 .NET에서 이게 어떻게 동작하는지 자주 물어봐. 그러니까 이걸 잘 이해하면 취업에도 플러스야.
게다가, 실제 프로젝트에서 예외는 계속 만나게 될 거야. 파일, 네트워크, 데이터베이스, 외부 라이브러리, 프레임워크 — 전부 예외 시스템을 적극적으로 써. 그러니까 미리 제대로 알아두면 나중에 당황할 일 없지.
7. 예외 처리에서 흔히 하는 실수
실수 1: 예외 자체를 무시함.
어떤 초보(가끔은 고수도)들은 예외를 귀찮거나 쓸데없는 걸로 생각해. 아예 무시하거나, 더 심하게는 코드를 catch { }로 감싸놓고 아무것도 안 해. 그러면 프로그램이 "조용히" 이상한 상태로 계속 돌아가고, 문제 원인을 찾을 수가 없어.
실수 2: 예외와 일반 에러를 헷갈림.
모든 에러를 try-catch로 잡아야 하는 건 아니야. 예를 들어 사용자가 잘못된 데이터를 입력했다면, FormatException에 기대지 말고 직접 먼저 체크하는 게 좋아. 예외는 진짜 예외적인 상황에 쓰는 거고, 일상적인 건 if로 처리하는 게 맞아.
실수 3: 예외 계층 구조를 잘못 이해함.
너무 넓은 클래스(Exception)로 잡으려고 하면, 원래 처리하지 말아야 할 에러까지 다 잡아버려. 반대로 너무 좁은 타입(IndexOutOfRangeException, NullReferenceException)만 잡으면, 다른 예외는 놓칠 수 있어. .NET의 예외 계층 구조를 잘 이해하고, 뭘 처리할지 명확히 해야 해.
그리고 제일 중요한 건: C#에서 예외는 사고나 "끝장" 신호가 아니라, 그냥 특별한 실행 흐름으로 전환하는 메커니즘이야. 잘만 쓰면 진짜 강력한 도구야.
GO TO FULL VERSION