Spring Boot REST API 회원관리 - (3) @RestControllerAdvice를 사용하여 전역 예외처리하기
애플리케이션에서 예외 처리를 일관성 있게 관리하는 것은 중요하다. 이를 위해 @RestControllerAdvice를 사용할 수 있다.
@RestControllerAdvice?
모든 @RestController 에서 발생하는 예외를 한곳에서 처리할 수 있게 해주는 어노테이션이다. 이 어노테이션을 이용하면 개별 컨트롤러에서 예외처리를 중복하여 작성할 필요 없이 한곳에서 관리할 수 있다.
공통된 응답값을 주기 위한 클래스 작성
예외 발생시 일관성 있는 응답값을 주기 위한 클래스를 작성하였다. 오류 발생 timestamp, 메세지, 세부 사항 정보를 넘겨줄 생각이다.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExceptionResponse {
private Date timestamp;
private String message;
private String detail;
}
InvalidRequestException 클래스 생성
보통 클라이언트가 서버에 잘못된 요청을 보낸 경우 '400 Bad Request' 상태코드를 반환한다. 이 경우를 처리하기 위해 'InvalidRequestException' 클래스를 생성하였다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidRequestException extends RuntimeException{
public InvalidRequestException(String message) {
super(message);
}
}
CustomizedResponseEntityExceptionHandler 클래스 생성
@RestControllerAdvice 어노테이션을 이용하여 @RestController 에서 발생하는 예외를 전역적으로 처리하기 위한 클래스를 생성한다. 차후 스프링의 기본 예외 처리 로직을 확장할 경우가 생길 수도 있으므로 ResponseEntityExceptionHandler를 우선 상속받았다.
이 코드에서는 @ExceptionHandler(Exception.class)를 사용하여 모든 예외를 처리하는 메서드 'handleAllException' 와 @ExceptionHandler(CustomException.class)를 사용하여 커스텀 예외를 처리할 'handleInvalidResourceException' 메서드가 있다. @ExceptionHandler(CustomException.class)를 보면 알 수 있듯이 커스텀 예외처리를 하기 위해서는 이를 위해 생성한 클래스를 어노테이션 value에 명시하면 된다.
이를 이용하여 필요할 때마다 커스텀 예외처리를 위해 클래스를 추가하고 다양한 예외 상황에 대한 적절한 응답을 쉽게 구성할 수 있다.
@Slf4j
@RestControllerAdvice
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllException(Exception ex, WebRequest webRequest) {
log.error("message : {}", NestedExceptionUtils.getMostSpecificCause(ex));
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), webRequest.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(InvalidRequestException.class)
public final ResponseEntity<Object> handleInvalidResourceException(Exception ex, WebRequest webRequest) {
log.error("message : {}", NestedExceptionUtils.getMostSpecificCause(ex));
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), webRequest.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.CONFLICT);
}
}
테스트를 위한 엔드포인트 생성
@RestControllerAdvice 가 잘 적용되어 컨트롤러에서 발생한 예외처리가 잘 되는지 확인하기 위한 엔드포인트를 생성하였다.
@GetMapping("/invalidRequestExceptionTest")
public ResponseEntity<?> conflictTest() {
throw new InvalidRequestException("잘못된 입력값 오류 발생");
}
@GetMapping("/exceptionTest")
public ResponseEntity<?> exceptionTest() throws Exception {
throw new Exception("공통 오류 발생");
}
테스트
Swagger UI를 이용해 위에서 생성된 엔드포인트를 호출하여 예외처리가 잘 동작되는지 확인하였다.
- "/invalidRequestExceptionTest" 호출 (커스텀 예외처리 호출)
"잘못된 입력값 오류 발생" - "/exceptionTest" 호출 (공통 예외처리 호출)
"공통 오류 발생"
스프링에서 예외가 발생하였을 때의 예외처리 흐름
- 컨트롤러에서 예외가 발생
- 컨트롤러에서 예외가 발생
- 동일한 컨트롤러에 @ExceptionHandler 메서드가 존재하는지 확인
- 예외가 발생한 컨트롤러 클래스에 해당 예외를 처리하는 @ExceptionHandler 메서드가 있는지 확인
- YES: 해당 메서드에서 예외를 처리하고, 클라이언트에게 응답을 반환
- NO: 다음 단계로 진행
- @ControllerAdvice 클래스에 @ExceptionHandler 메서드가 존재하는지 확인
- @ControllerAdvice 클래스를 확인하여, 해당 예외를 처리하는 @ExceptionHandler 메서드가 있는지 확인
- YES: 해당 메서드에서 예외를 처리하고, 클라이언트에게 응답을 반환
- NO: 다음 단계로 진행
- 예외가 @ResponseStatus로 주석이 달렸는지 확인
- 예외 클래스에 @ResponseStatus 어노테이션이 있는지 확인
- YES: 스프링의 ResponseStatusExceptionResolver 클래스가 예외를 처리
- NO: 스프링의 DefaultHandlerExceptionResolver 클래스가 예외를 처리
- 클라이언트에게 응답 반환
- 각 처리 방법에 따라 예외가 처리된 후, 클라이언트에게 응답이 반환