자바 예외처리 완전 정복(try-catch, throws, 커스텀 예외) — Java Exception Handling
개념 요약
- 예외(Exception) 계층: 검사 예외(Checked)와 비검사 예외(Unchecked, RuntimeException)로 나뉨.
Throwable→Exception/Error트리. - 처리 방식:
try-catch-finally,throws선언,try-with-resources(자동 자원 해제), 멀티 캐치(catch (A | B e)). - 전파(Propagation)와 래핑(Wrapping): 상위로 던지기(
throw), 원인 보존(new XxxException("msg", cause)), 재던지기(rethrow). - 커스텀 예외(Custom Exception): 도메인 에러를 의도적으로 표현. 규칙: 메시지/원인 포함 생성자 제공.
- 베스트 프랙티스: “복구 가능하면 잡고(recover), 불가능하면 전파”. 로그 중복 금지, 예외에 의미 있는 메시지와 컨텍스트 담기.
배경
- 자바에서 예외는 정상 흐름을 벗어난 오류 상황을 객체로 다루는 메커니즘이야. 강제 처리되는 검사 예외와 선택 처리되는 비검사 예외가 있어.
- I/O, 네트워크 같은 외부 자원은 실패 가능성이 높아서 예외 처리 설계가 중요해.
- 실제 서비스에서는 “어디서 잡고 어디서 넘길지”의 경계 설정이 품질을 좌우해.
왜 필요한가
- 실패를 안전하게 격리하고, 자원 누수를 막고(파일/DB 커넥션), 사용자에게 이해 가능한 에러로 전달하기 위해.
- 원인 파악(디버깅)과 관측성(로그/메트릭)을 높이고, 복구 가능한 시나리오를 코드로 표현하기 위해.
예제 코드
1) try-catch-finally + 커스텀 예외 + 원인 래핑
// 도메인용 커스텀 검사 예외
class InvalidOrderException extends Exception {
public InvalidOrderException(String message) { super(message); }
public InvalidOrderException(String message, Throwable cause) { super(message, cause); }
}
class OrderService {
// 외부 시스템 호출을 흉내 낸 메서드
private String fetchCustomerName(long customerId) throws Exception {
if (customerId <= 0) throw new IllegalArgumentException("customerId must be positive");
// 네트워크/DB 실패 가정
if (customerId == 13) throw new RuntimeException("Downstream timeout");
return "Alice";
}
public String placeOrder(long customerId, int quantity) throws InvalidOrderException {
// 입력 검증
if (quantity <= 0) {
throw new InvalidOrderException("수량은 1개 이상이어야 해요: quantity=" + quantity);
}
try {
String name = fetchCustomerName(customerId); // 예외 발생 가능
// 비즈니스 로직...
return "Order accepted for " + name + " x " + quantity;
} catch (IllegalArgumentException e) {
// 개발자 실수/계약 위반 → 즉시 의미 있는 검사 예외로 변환
throw new InvalidOrderException("잘못된 고객 ID: " + customerId, e);
} catch (RuntimeException e) {
// 외부 시스템 실패를 도메인 예외로 래핑하여 상위 계층에 전달
throw new InvalidOrderException("고객 정보를 불러오지 못했어요(customerId=" + customerId + ")", e);
} finally {
// 자원 정리(여기서는 예시용 로그)
System.out.println("placeOrder() finished (cleanup)");
}
}
}
public class App1 {
public static void main(String[] args) {
OrderService s = new OrderService();
try {
System.out.println(s.placeOrder(10L, 2));
System.out.println(s.placeOrder(13L, 1)); // Downstream 실패 케이스
} catch (InvalidOrderException e) {
// 단 한 곳에서 의미 있게 로깅/사용자 응답 변환(중복 로그 지양)
System.err.println("[ORDER-ERR] " + e.getMessage());
Throwable cause = e.getCause();
if (cause != null) cause.printStackTrace();
}
}
}
2) try-with-resources + 멀티 캐치 + 재던지기(rethrow)
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
class ConfigLoadException extends RuntimeException {
public ConfigLoadException(String msg, Throwable cause) { super(msg, cause); }
}
public class App2 {
// 설정 파일을 읽어 첫 줄을 반환
static String loadConfigFirstLine(String path) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
// 자원은 블록 종료 시 자동 close (예외 발생해도 보장)
return br.readLine();
} catch (IOException | SecurityException e) {
// 서로 다른 예외를 한 번에 처리(멀티 캐치)
// 필요한 컨텍스트를 메시지에 추가하고 비검사 예외로 래핑하여 전파
throw new ConfigLoadException("설정 로드 실패: path=" + path, e);
}
}
public static void main(String[] args) {
// 존재하지 않는 파일로 실패 유도
System.out.println(loadConfigFirstLine("config/app.conf"));
}
}
동작 원리
- 검사 예외(Checked Exception): 메서드 시그니처에
throws로 선언하거나try-catch로 반드시 처리해야 해. 컴파일 타임에 강제하므로 호출자가 복구 전략을 고민하게 만들지. - 비검사 예외(Unchecked = RuntimeException 하위): 선언/처리 강제 없음. 주로 프로그래밍 오류(계약 위반, Null, IllegalArgument)나 복구 불가 상황에 사용.
- finally/try-with-resources: 자원 정리 보장 장치.
try-with-resources는AutoCloseable구현체를 자동으로close()호출해 누수를 예방해. - 멀티 캐치:
catch (A | B e)로 예외 형제가 여러 개일 때 중복 코드를 줄여. 단, 상위 타입과 하위 타입을 동시에 나열하면 컴파일 에러. - 래핑과 전파: 낮은 계층의 기술적 예외를 도메인 예외로 변환해 상위 계층에 “비즈니스 의미”를 전달. 이때 원인(cause)을 반드시 보존해서 디버깅 가능하게 만들기.
- 로깅 전략: “한 번만, 의미 있게”. 하위에서 잡고 다시 던질 때 이미 로그를 찍었다면 상위에서 중복 로깅 금지(노이즈 감소, 원인 탐색 용이).
- 경계 설정: 컨트롤러/핸들러(최상위)에서 사용자 메시지 변환, 서비스/도메인에서는 의미 있는 예외 모델링, 리포지토리/게이트웨이는 기술 예외를 도메인 예외로 래핑.
대안과 선택 기준
- 결과형 반환(Either/Result, Optional): 예외 대신 실패를 값으로 표현. 빈번한 실패가 “정상 흐름”일 때(예: 조회 결과 없음) 적합. 단, 자바 표준 라이브러리에는 내장 Result가 없어 Vavr 등 서드파티 사용 검토.
- 검사 vs 비검사 선택: 호출자가 복구해야 하는 시나리오(재시도/대안 경로)가 명확하면 검사 예외, 그렇지 않으면 비검사 예외가 보통 더 단순.
요약
- 무엇: 자바 예외는 실패를 객체로 다루는 메커니즘.
try-catch-finally/throws/try-with-resources로 처리하고, 멀티 캐치와 래핑으로 중복/누수를 줄여. - 왜: 자원 누수 방지, 복구 가능한 실패 처리, 의미 있는 사용자/로그 메시지 제공, 디버깅 가능성 확보를 위해.
- 어떻게: 하위에서 기술 예외 → 도메인 예외로 변환, 상위에서 일관되게 응답 변환. 로깅은 한 번만, 메시지는 구체적으로, cause 보존 필수.
- 이어서 보면 좋은 주제: 스프링 @ControllerAdvice/ExceptionHandler로 전역 예외 처리, 레코드/Sealed 클래스 기반 에러 모델링.
'Backend > Java' 카테고리의 다른 글
| [JAVA] 클래스 (0) | 2025.09.15 |
|---|---|
| [JAVA] 반복문 (0) | 2025.09.08 |
| [JAVA] 변수 (0) | 2025.09.08 |
| [Java] 기본형 vs 참조형 (1) | 2025.02.11 |
| 스파르타 코딩 클럽 SQL 1주차 (0) | 2022.06.28 |