C# 예외 처리 마스터하기: 전략과 모범 사례
예외(Exception)는 프로그램 실행 중에 발생하는 문제입니다. C# 예외는 프로그램이 실행되는 동안 발생하는 예외적 상황(예: 0으로 나누기)에 대한 응답입니다.
예외는 프로그램의 한 부분에서 다른 부분으로 제어를 전송하는 방법을 제공합니다. C# 예외 처리는 네 가지 키워드(try, catch, finally, throw)를 기반으로 합니다.
- try: 특정 예외가 활성화되는 코드 블록 식별. 하나 이상의 catch 블록이 따름
- catch: 문제 처리하고 싶은 위치에서 예외 처리기로 예외 잡기
- finally: 예외 발생 여부와 관계없이 실행할 문장 집합. 예: 파일 열었으면 예외 발생해도 닫아야 함
- throw: 문제가 발생하면 예외 던지기
구문 (Syntax)
try
{
// 예외 발생 가능한 문장들
}
catch (ExceptionType1 e1)
{
// 예외 처리 코드
}
catch (ExceptionType2 e2)
{
// 다른 예외 처리
}
catch (ExceptionTypeN eN)
{
// 여러 예외 처리 가능
}
finally
{
// 항상 실행되는 정리 코드
}
여러 catch 블록으로 서로 다른 타입의 예외를 각각 처리할 수 있습니다.
C# 예외 클래스 계층
C# 예외는 클래스로 표현되며, 대부분 System.Exception 클래스에서 직/간접 파생됩니다. 주요 파생 클래스:
System.ApplicationException: 응용 프로그램에서 생성된 예외 지원. 프로그래머 정의 예외의 기본 클래스
System.SystemException: 모든 미리 정의된 시스템 예외의 기본 클래스
주요 미리 정의 예외 클래스 (System.SystemException 파생)
| 번호 | 예외 클래스 및 설명 |
| 1 | System.IO.IOException - I/O 오류 처리 |
| 2 | System.IndexOutOfRangeException - 배열 인덱스 범위 초과 |
| 3 | System.ArrayTypeMismatchException - 배열 타입 불일치 |
| 4 | System.NullReferenceException - null 객체 참조 |
| 5 | System.DivideByZeroException - 0으로 나누기 |
| 6 | System.InvalidCastException - 타입 캐스팅 실패 |
| 7 | System.OutOfMemoryException - 메모리 부족 |
| 8 | System.StackOverflowException - 스택 오버플로 |
예외 처리 예제: 0으로 나누기
using System;
namespace ErrorHandlingApplication
{
class DivNumbers
{
int result;
public DivNumbers() { result = 0; }
public void Division(int num1, int num2)
{
try
{
result = num1 / num2;
}
catch (DivideByZeroException e)
{
Console.WriteLine("예외 발생: {0}", e.Message);
}
finally
{
Console.WriteLine("결과: {0}", result);
}
}
static void Main(string[] args)
{
DivNumbers d = new DivNumbers();
d.Division(25, 0);
Console.ReadKey();
}
}
}
출력: 예외 발생: Attempted to divide by zero. / 결과: 0
사용자 정의 예외 (Custom Exceptions)
Exception 클래스에서 파생하여 직접 예외를 정의할 수 있습니다.
using System;
namespace UserDefinedException
{
// 사용자 정의 예외 클래스
public class TempIsZeroException : Exception
{
public TempIsZeroException(string message) : base(message) { }
}
public class Temperature
{
int temperature = 0;
public void ShowTemp()
{
if (temperature == 0)
{
throw new TempIsZeroException("Zero Temperature found");
}
else
{
Console.WriteLine("Temperature: {0}", temperature);
}
}
}
class TestTemperature
{
static void Main(string[] args)
{
Temperature temp = new Temperature();
try
{
temp.ShowTemp();
}
catch (TempIsZeroException e)
{
Console.WriteLine("TempIsZeroException: {0}", e.Message);
}
Console.ReadKey();
}
}
}
출력: TempIsZeroException: Zero Temperature found
예외 필터 (Exception Filters) - C# 6.0+
when 키워드로 catch 조건을 세분화할 수 있습니다.
try { ... }
catch (SqlException ex) when (ex.Number == 2627)
{
// 중복 키 에러만 처리
}
catch (SqlException ex) when (ex.Number == 547)
{
// 외래 키 위반만 처리
}
catch (SqlException ex)
{
// 기타 SQL 예외
}
예외 재던지기 (Rethrowing)
catch (Exception e)
{
// 로깅 등 처리 후
throw; // 원래 예외 그대로 다시 던짐 (스택 트레이스 보존)
// throw e; // 스택 트레이스 초기화됨 - 비권장
}
모범 사례 (Best Practices)
| 원칙 | 설명 |
| 예외는 예외적인 상황에만 | 제어 흐름 제어용으로 사용하지 말 것 |
| 구체적인 예외 타입 잡기 | catch (Exception) 남용 금지. 구체적 타입 우선 |
| catch 블록 비우지 않기 | 최소한 로깅이라도 수행 |
| finally로 리소스 정리 | 파일, DB 연결, 네트워크 스트림 등 해제 |
using 문 활용 | IDisposable 구현 객체는 using 블록으로 자동 정리 |
사용자 정의 예외는 Exception 접미사 | 예: InsufficientFundsException |
| 예외 메시지는 사용자/개발자 모두 이해 가능하게 | 기술적 세부사항과 사용자 친화적 메시지 분리 |
| 스택 트레이스 보존 | 재던질 때 throw; 사용, throw e; 금지 |
using 문으로 안전한 리소스 관리
// try-finally 패턴을 using으로 간소화
using (FileStream fs = new FileStream("test.txt", FileMode.Open))
using (StreamReader sr = new StreamReader(fs))
{
string content = sr.ReadToEnd();
Console.WriteLine(content);
} // 자동으로 Dispose() 호출됨 (예외 발생해도)
핵심: 예외 처리는 오류 복구가 아닌 '안전한 실패'와 '리소스 정리'가 주 목적입니다. 복구 가능한 오류는 결과 타입(Option/Result 패턴)으로, 복구 불가능한 오류는 예외로 처리하세요.