이 글에서 얻는 것
- @ControllerAdvice로 전역 예외 처리를 구현할 수 있습니다.
- @ExceptionHandler로 예외 타입별로 응답을 구분할 수 있습니다.
- 일관된 에러 응답 포맷을 설계할 수 있습니다.
- 커스텀 예외를 만들고 적절히 처리할 수 있습니다.
0) 예외 처리는 “사용자 경험"이다
문제 상황
❌ 예외 처리 없이:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// 예외 발생 시 Spring이 기본 에러 페이지 반환
return userService.findById(id); // UserNotFoundException!
}
}
결과:
{
"timestamp": "2025-12-16T10:30:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/users/999"
}
문제점:
- 사용자에게 의미 없는 500 에러
- 어떤 문제인지 알 수 없음
- API마다 에러 형식이 다름
- 로그에 스택 트레이스만 쌓임
해결: 전역 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
ErrorResponse error = new ErrorResponse(
"USER_NOT_FOUND",
e.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
결과:
{
"errorCode": "USER_NOT_FOUND",
"message": "사용자를 찾을 수 없습니다: ID=999",
"timestamp": "2025-12-16T10:30:00"
}
1) @ControllerAdvice와 @RestControllerAdvice
1-1) @ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
// 모든 컨트롤러의 예외를 처리
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
// ...
}
}
특징:
- 모든
@Controller에 적용 @ExceptionHandler와 함께 사용- View 이름 반환 가능
1-2) @RestControllerAdvice
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ErrorResponse handleException(Exception e) {
// 자동으로 JSON 변환
return new ErrorResponse(e.getMessage());
}
}
차이점:
@ControllerAdvice:
- View 이름 반환
- @ResponseBody 필요
@RestControllerAdvice:
- 객체 반환 → 자동 JSON 변환
- REST API에 적합
1-3) 특정 패키지/컨트롤러에만 적용
// 특정 패키지에만 적용
@RestControllerAdvice(basePackages = "com.myapp.api")
public class ApiExceptionHandler {
// ...
}
// 특정 컨트롤러에만 적용
@RestControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
public class UserOrderExceptionHandler {
// ...
}
// 특정 애노테이션이 있는 컨트롤러에만
@RestControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler {
// ...
}
2) @ExceptionHandler
2-1) 기본 사용
@RestControllerAdvice
public class GlobalExceptionHandler {
// 특정 예외 처리
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException e) {
ErrorResponse error = ErrorResponse.builder()
.errorCode("USER_NOT_FOUND")
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
// 여러 예외를 동일하게 처리
@ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})
public ResponseEntity<ErrorResponse> handleBadRequest(RuntimeException e) {
ErrorResponse error = ErrorResponse.builder()
.errorCode("BAD_REQUEST")
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(error);
}
// 모든 예외 처리 (마지막 방어선)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Unexpected error occurred", e);
ErrorResponse error = ErrorResponse.builder()
.errorCode("INTERNAL_SERVER_ERROR")
.message("서버 오류가 발생했습니다.")
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
2-2) HttpServletRequest 접근
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(
UserNotFoundException e,
HttpServletRequest request) {
log.warn("User not found: {} - {}", request.getRequestURI(), e.getMessage());
ErrorResponse error = ErrorResponse.builder()
.errorCode("USER_NOT_FOUND")
.message(e.getMessage())
.path(request.getRequestURI())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
💬 댓글