핵심 정리
try
{
var data = File.ReadAllText(path);
Process(data);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"파일 없음: {ex.Message}");
}
catch (IOException ex)
{
Console.WriteLine($"IO 오류: {ex.Message}");
throw; // 원래 stack trace 유지하며 재던지기
}
finally
{
Console.WriteLine("항상 실행됨"); // 성공/실패 무관
}기본 흐름
어떤 예외 처리 형태가 있나
예외 카드는 아래 네 가지를 먼저 구분하면 흐름이 정리됩니다.
try { Work(); }
catch (IOException ex) { Log(ex); }
finally { Cleanup(); }
throw;
throw new InvalidOperationException("실패", innerException);try / catch: 오류를 잡아 처리finally: 성공/실패와 무관한 정리throw;: 원래 예외 재던지기throw new ...: 새 예외로 감싸기
try/catch/finally 실행 순서
try 블록에서 예외가 발생하면 즉시 나머지를 중단하고 일치하는 catch 블록으로 이동합니다. finally 는 예외 발생 여부와 무관하게 항상 마지막에 실행됩니다.
try
{
Console.WriteLine("1. try");
throw new Exception("오류");
Console.WriteLine("2. 실행 안 됨"); // 도달 불가
}
catch (Exception ex)
{
Console.WriteLine("3. catch");
}
finally
{
Console.WriteLine("4. finally"); // 항상 실행
}
// 출력: 1. try → 3. catch → 4. finallycatch에서 예외가 다시 발생해도 finally는 실행됩니다.
예외 계층 구조 — 좁은 타입을 먼저
C#의 예외는 모두 Exception을 상속합니다. catch 블록은 위에서 아래로 평가되므로, 좁은(구체적인) 예외를 먼저, 넓은 예외를 나중에 배치해야 합니다.
try { /* ... */ }
catch (FileNotFoundException ex) // ✅ 가장 구체적 — 먼저
{
// 파일을 찾을 수 없는 경우 처리
}
catch (IOException ex) // ✅ 그 다음
{
// 일반 IO 오류 처리
}
catch (Exception ex) // ✅ 가장 넓음 — 마지막
{
// 예상치 못한 오류 처리
}catch (Exception)을 먼저 두면 하위 타입도 모두 잡히므로 아래 catch 블록이 절대 실행되지 않습니다. 컴파일러가 오류를 냅니다.
throw vs throw ex — stack trace 차이
예외를 다시 던질 때 throw; 는 원래 stack trace를 그대로 유지합니다. throw ex; 는 현재 위치에서 새 예외처럼 stack trace를 재설정합니다. 디버깅에서 원인을 추적하려면 throw;를 써야 합니다.
catch (Exception ex)
{
Log(ex);
throw; // ✅ 원래 stack trace 유지
// throw ex; ← ❌ stack trace가 여기서부터 시작되어 원인 추적 어려움
}
// 새로운 예외로 감쌀 때는 inner exception으로 전달
catch (Exception ex)
{
throw new ServiceException("처리 실패", ex); // ex가 InnerException
}// ❌ stack trace가 여기서 끊긴다
catch (Exception ex)
{
Log(ex);
throw ex;
}사용자 정의 예외
비즈니스 도메인의 오류를 명확히 표현하려면 Exception을 상속해 사용자 정의 예외를 만듭니다.
public class InsufficientBalanceException : Exception
{
public decimal RequiredAmount { get; }
public InsufficientBalanceException(decimal required)
: base($"잔액 부족: {required}원 필요")
{
RequiredAmount = required;
}
}
// 사용
throw new InsufficientBalanceException(5000m);예외 vs TryParse 패턴
예외는 정상 흐름 밖의 상황에 씁니다. 사용자 입력처럼 실패가 자주 예상되는 경우라면 예외보다 bool 반환 패턴이 더 적합합니다.
// ❌ 사용자 입력을 예외로 처리 — 성능/설계 모두 비효율
try { int n = int.Parse(userInput); }
catch { /* 흔한 실패를 예외로 처리 */ }
// ✅ TryParse 패턴
if (int.TryParse(userInput, out int n))
Process(n);
else
ShowError("숫자를 입력하세요");// ❌ 복구하지 못하는데 catch에서 삼켜 버림
try
{
SaveToDatabase(order);
}
catch (Exception)
{
return;
}
// ✅ 복구 전략이 없으면 상위로 올린다
try
{
SaveToDatabase(order);
}
catch (SqlException ex)
{
_logger.LogError(ex, "주문 저장 실패");
throw;
}체크포인트
| 상황 | 권장 방법 |
|---|---|
| 파일, 네트워크, DB | try/catch (IOException, HttpRequestException, ...) |
| 실패가 흔한 파싱 | TryParse, bool 반환 패턴 |
| 예외 재던지기 | throw; (stack trace 유지) |
| 예외 감싸기 | throw new WrapperEx("msg", innerEx) |
| 항상 실행할 정리 | finally 또는 using |
| 도메인 오류 표현 | Exception 상속 사용자 정의 예외 |
주의할 점
모든 예외를 다 잡는 catch (Exception) 은 처리할 수 없는 오류까지 삼켜버립니다. "이 예외를 여기서 실제로 복구할 수 있는가?"에 답이 없으면 잡아서 조용히 넘기기보다 상위 계층으로 올리는 편이 안전합니다.
finally 블록 안에서 예외를 던지면 원래 예외가 사라집니다. finally는 정리 작업만 하고 새로운 예외를 발생시키지 않는 편이 좋습니다.
참고 링크
2 sources