
현재 구현된 API 에서 Exception 이 발생하게 되면 응답으로 500 Internal Server Error 가 전달 되는 것을 이전 Create API Operation(2) - Search by condition 에서 확인했었습니다.
Operaion 에서 발생 할 수 있는 모든 예외를 처리하는 것도 쉬운 일은 아니며, 공통 적으로 발생할 수 있는 예외에 대한 처리 코드를 모든 Operation 에 추가하는 것도 비효율적입니다. (500 Internal Server Error 발생은 API 개발자 입장에서 발생 시키지 말아야할 오류 입니다.)
Springboot 에서는 원하는 모든 API Operation 에서 발생 하는 예외에 대하여 공통으로 처리할 수 있는 방법을 ControllerAdvice 를 통해서 제공하고 있습니다.
Link : https://docs.spring.io/spring-boot/reference/web/servlet.html#web.servlet.spring-mvc.error-handling
ControllerAdvice 는 이름에서도 유추 할 수 있듯이 Controller 에서 발생하는 예외를 AOP 와 유사한 방식으로 catch 하는 Annotation 입니다. Spring AOP 에 대해서 알고있는 분이라면 Controller에 AfterThrowing 설정을 추가 한건가? 정도 예상하실 수 있을 거에요. AfterThrowing 과 유사하게 동작하긴 하지만, 이를 사용하는 것은 아닙니다.
Controller 에서 Exception 이 발생하면, ExceptionHandlerExceptionResolver 가 Exception 을 가로채고, 발생한 Exception 에 대한 예외처리 메서드가 있는지 @ExceptionHandler 를 검색하여 해당 메서드를 실행합니다. AOP 와 유사한 방식으로 동작하지만 실제 구현은 Spring MVC 의 예외처리 매커니즘을 기반으로 합니다.
공통적으로 Exception 이 발생했을 때 응답할 record 를 생성합니다. 기존 공통 응답 구조체들과 같이 status, message 를 추가하고, 여기에 상세 메시지를 전달할 detailMessage 도 추가합니다.
common.response.ErrorResponse
@Builder
public record ErrorResponse(
String status,
String message,
String detailMessage
) {
}
이제 위에서 설명한 ControllerAdvice 와 ExceptionHandler 를 사용하여 Controller 에서 발생하는 Exception 을 처리할 GlobalExceptionHandler 를 생성합니다.
이에 앞서, message.properties 에 실패 관련 status와 message 를 추가합니다.
message.properties, message_en_US.properties, message_zh_CN.properties


common.exception.GlobalExceptionHandler
@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
private final MessageConfig messageConfig;
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleEntityNotFound(EntityNotFoundException e) {
log.error(e.getMessage(), e);
return ResponseEntity.ok()
.body(ErrorResponse.builder()
.status(messageConfig.getMessage("FAIL.CODE"))
.message(messageConfig.getMessage("FAIL.NO.DATA.MESSAGE"))
.detailMessage(e.getMessage())
.build());
}
}
http://localhost:13713/my-api/members/member2 를 요청하여 EntityNotFoundException 을 발생시켜봅니다.
{
status: "API_FAIL",
message: "데이터가 존재하지 않습니다.",
detailMessage: "회원 ID 가 존재하지 않습니다. -> member2"
}
이전에 500 Internal Server Error 가 발생한 것과 다르게 HTTP Status 가 200 OK 로 ErrorResponse 가 전달된 것을 확인하실 수 있습니다.
GlobalExceptionHandler 개선 - 응답 생성 메서드 분리
AOP 는 OOP(Oriented Object Programming : 객체 지향 프로그래밍) 의 단점을 보완하여 특정 시점에 공통된 기능을 동작하게 해, 코드의 효율성을 높일 수 있는 Spring 의 기능입니다. Controller 를 감싼 Proxy Pattern 으로 동작하며, dispatcher-servlet 이후에 동작하기 때문에 Request, Response, Parameter, Spring bean 에 모두 접근 가능합니다.
@Before, @After, @AfterReturning, @AfterThrowing, @Around 를 통해서 Controller 의 특정 시점에 method 를 실행할 수 있습니다.
| 구분 | 시점 |
|---|---|
| @Before | Controller 실행 전 |
| @After | Controller 실행 후 |
| @AfterReturning | Controller 정상 응답 후 |
| @AfterThrowing | Controller 에서 Exception 이 발생한 후 |
| @Around | 모든 시점 |
같은 동작을 하는 두개의 Annotation 이 있습니다. 차이는 적용대상과 응답타입에 있습니다. @ControllerAdvice 는 모든 Controller에 적용됩니다. (@Controller, @RestController) 하지만 @RestControllerAdvice 는 @RestController 에만 적용이 됩니다. 적용되는 Controller 차이에서도 알수 있듯이 @ControllerAdvice 는 view와 Json 모두 전달이 가능하고 @RestControllerAdvice 는 view 를 전달 할 수 없습니다.
| 구분 | 적용 대상 | 응답 타입 |
|---|---|---|
| @ControllerAdvice | @Controller, @RestController | View, JSON, XML |
| @RestControllerAdvice | @RestController | JSON, XML |
ExceptionResolver 는 Springboot에서 예외를 처리하고 적절한 응답을 반환하는 중요한 역할을 합니다. Spring 에서는 HandlerExceptionResolver interface 를 구현한 3개의 ExceptionResolver 를 제공하고 위에서 사용한 @ExceptionHandler 를 사용하는 ExceptionHandlerExceptionResolver 가 그 중 하나입니다.
@ResponseStatus 이 있으면 해당 Annocation에 정의된 HTTP status code를 지정합니다.ExceptionHandlerExceptionResolver -> ResponseStatusExceptionResolver -> DefaultHandlerExceptionResolver 순서로 동작합니다.