Topic (오늘의 주제)
Spring의 우아한 예외 처리: @ExceptionHandler와 @ControllerAdvice
Why (왜 사용하는가? 왜 중요한가?)
- 실무: 컨트롤러마다 중복되는 try-catch 블록을 제거하고, 클라이언트에게 **일관된 포맷(JSON)**의 에러 응답을 내려주기 위해 필수적입니다.
- 구조적 의미: **관심사의 분리(SoC)**를 실현합니다. 비즈니스 로직(성공 케이스)과 예외 처리 로직(실패 케이스)을 완전히 분리하여 코드 가독성을 높입니다.
- 면접 의도: 단순한 자바 예외 처리(try-catch)를 넘어, 프레임워크가 제공하는 AOP 기반의 전역 예외 처리 전략을 설계할 수 있는지 확인합니다.
Core Concept (핵심 개념 정리)
스프링의 예외 처리는 발전 과정에 따라 크게 3가지 단계(방식)로 나뉩니다.
| 요소 | 내용 |
| 1. @ExceptionHandler | 개념: 특정 Controller 클래스 내부에서 발생하는 예외만 잡아내는 애노테이션. 특징: 해당 컨트롤러 안에서만 유효하므로, 여러 컨트롤러에 똑같은 예외 처리 코드를 복사해야 하는 단점이 있음. |
| 2. @ControllerAdvice | 개념: 전역(Global) 예외 처리를 위한 AOP 기반 애노테이션. 동작: 모든 컨트롤러에서 발생한 예외를 가로채서(Interception) 한 곳에서 처리할 수 있게 함. 구조: 보통 @ControllerAdvice 클래스 안에 @ExceptionHandler 메서드들을 모아둠. |
| 3. @RestControllerAdvice | 개념: @ControllerAdvice + @ResponseBody의 합체. 특징: API 서버(REST API) 개발 시 주로 사용하며, 에러 응답을 View(HTML)가 아닌 JSON 객체로 바로 반환함. |
| 동작 흐름 | 예외 발생(throw) → HandlerExceptionResolver가 탐색 → 적합한 @ExceptionHandler가 있는지 확인 → 있으면 해당 메서드 실행 및 응답 반환. |
| 비교 | 기존 방식: 메서드마다 try-catch로 감싸서 지저분함. Spring 방식: 로직 구현부는 예외를 던지기만 하고(throw), 별도의 GlobalExceptionHandler 클래스가 받아서 처리. |
Interview Answer Version (면접 답변식 요약)
"Spring의 예외 처리는 @ControllerAdvice (또는 @RestControllerAdvice)를 활용한 전역 처리 방식을 주로 사용합니다.
컨트롤러 별로 중복되는 @ExceptionHandler를 한곳에 모아 관리함으로써 비즈니스 로직과 예외 처리 코드를 분리할 수 있고, 클라이언트에게 **일관된 에러 응답 포맷(Custom Error Response)**을 제공할 수 있기 때문입니다.
과거에는 HandlerExceptionResolver를 직접 구현하기도 했으나, 현재는 애노테이션 기반 처리가 표준입니다."
Practical Tip (사용시 주의할 점 or 활용 예)
1. 구체적인 예외를 먼저 선언 (우선순위 문제)
예외 처리도 구체적인 것(자식)이 먼저, 포괄적인 것(부모)이 나중에 처리되어야 합니다.
- Bad Case: Exception.class를 처리하는 핸들러가 위에 있으면, 구체적인 예외 핸들러가 무시될 수 있음(순서 의존적일 경우) 혹은 스프링 내부 로직에 의해 덜 구체적인 것이 선택될 위험.
- Good Case: 구체적인 예외를 명시하고, 마지막에 Exception으로 안전망 설치.
Java
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. 구체적인 예외 (ex: 비즈니스 로직 상의 잘못된 요청)
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleBadReq(IllegalArgumentException e) {
return ResponseEntity.status(400).body(new ErrorResponse("BAD_REQUEST", e.getMessage()));
}
// 2. 최후의 보루 (알 수 없는 에러)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Unhandled Exception", e); // 로깅 필수!
return ResponseEntity.status(500).body(new ErrorResponse("INTERNAL_SERVER_ERROR", "서버 에러 발생"));
}
}
2. Custom Exception과 ErrorResponse 객체 정의
그냥 String 메시지만 보내지 말고, 프론트엔드와 약속된 **JSON 구조(DTO)**를 정의해서 내려줘야 합니다.
- 추천 구조: timestamp, status, error_code (내부 관리용 코드), message
3. @Transactional과 예외
- Checked Exception (ex: Exception, IOException) 발생 시 기본적으로 롤백되지 않습니다.
- Unchecked Exception (ex: RuntimeException) 발생 시에만 롤백됩니다.
- 만약 Checked Exception에서도 롤백하고 싶다면 @Transactional(rollbackFor = Exception.class) 옵션을 줘야 합니다.
예상 꼬리질문 정리
- Q: @ControllerAdvice와 @RestControllerAdvice의 차이는 무엇인가요?
- 핵심 키워드: @ResponseBody의 유무. 전자는 ViewResolver를 타서 HTML 등을 반환하려 하고, 후자는 MessageConverter를 타서 JSON 데이터를 반환합니다.
- Q: 필터(Filter)나 인터셉터(Interceptor)에서 발생한 예외는 @ControllerAdvice가 잡을 수 있나요?
- 핵심 키워드: 못 잡습니다. @ControllerAdvice는 DispatcherServlet 내부(컨트롤러 진입 후)의 예외를 처리합니다. 필터(DispatcherServlet 진입 전)의 예외는 별도의 필터 내 예외 처리 로직이 필요합니다.
- Q: Checked Exception과 Unchecked Exception의 트랜잭션 롤백 처리 차이에 대해 설명해주세요.
- 핵심 키워드: RuntimeException(Unchecked)은 자동 롤백, Exception(Checked)은 커밋됨(롤백 안 됨).
'Archive > Daily Dev Q&A' 카테고리의 다른 글
| Daily Dev Q&A: Spring Security의 인증과 인가 (0) | 2025.12.31 |
|---|---|
| Daily Dev Q&A: @SpringBootApplication은 필수인가? (1) | 2025.12.30 |
| Daily Dev Q&A: Spring Stereotype Annotations (0) | 2025.12.29 |
| Daily Dev Q&A: Spring Bean (0) | 2025.12.26 |
| Daily Dev Q&A: Spring Boot (0) | 2025.12.26 |