[Spring] Spring Boot는 어떤 방법으로 예외 처리를 할까? @ExceptionHandler, @ControllerAdvice
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
본 글은 인프런의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 참고하여 작성되었습니다~
저번 글에서는 Servlet이 예외 처리를 하는 흐름에 대해서 알아보았습니다. 그럼 오늘은 Spring Boot는 Servlet이 하는 예외 처리를 어떻게 깔끔하게 처리하는지 알아보겠습니다.
우선 Spring Boot는 오류 발생 시 /error 를 오류 페이지로 요청을 합니다. 이 경로는 application.properties에서 server.error.path 로 수정할 수 있습니다.
server.error.path=/error
우리가 API를 만들 때 오류 처리가 필요하면 매우 복잡하게 생각이 필요합니다. 어떤 기능을 하는 컨트롤러인지 어떤 응답인지에 따라서 응답을 다르게 출력해줘야 하기 때문입니다.
그럼 이제 Spring Boot에서 예외 처리를 어떻게 해야하는지 알아봅시다~
ExceptionResolver
Spring 은 ExceptionResolver를 이용해서 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경할 수 있습니다.
ExceptionResolver의 반환 값에 따른 DispatcherServlet의 동작 방식은 아래 세가지가 있습니다.
- return new ModelAndView(): 다음과 같이 아무것도 없는 ModelAndView를 반환하게 되면 정상 흐름으로 Servlet이 리턴됩니다.
- return new ModelAndView("error/400"): 다음과 같이 뷰의 정보를 넣어주게 되면 뷰를 렌더링 합니다.
- null: null을 반환하게 되면 다음 ExceptionResolver를 찾아서 반환합니다. 처리할 수 있는게 없다면 예외 처리가 되지 않고, 서블릿 밖으로 보내게 됩니다.
Spring Boot는 ExceptionResolver를 기본적으로 세 가지를 제공합니다.
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
위 세 가지 중에서 ExceptionHandlerExceptionResolver가 우선 순위가 가장 높고, DefaultHandlerExceptionResolver가 우선 순위가 가장 낮습니다. (Spring Boot는 항상 자세한 것이 우선 순위가 높습니다 ! )
DefaultHandlerExceptionResolver
일단 가장 우선 순위가 낮은 DeafultHandlerExceptionResolver 부터 살펴봅시다.
DefaultHandlerExceptionResolver의 예로는 TypeMismatchException이 있습니다.
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
위와 같이 작성하고 http://localhost:8080/api/default-handler-ex?data=hello 를 요청해보면, 400(Bad Request) 에러가 발생할 것 입니다.
Integer인데 hello를 보내줘서 예외가 발생하였기 때문에 500 오류가 발생해야 하지만 HTTP에서는 400 이 경우 400에러를 사용하도록 되어 있기 때문에 DefaultHandlerExceptionResolver는 400에러로 바꿔서 보여줍니다.
ResponseStatusExceptionResolver
다음으로 ResponseStatusExceptionResolver 입니다. ResponseStatusExceptionResolver는 @ResponseStatus가 붙어있거나 ResponseStatusException 예외를 처리합니다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
}
(error.bad는 messages.properties를 만들어서 작성해줬습니다.)
위와 같이 작성하고 컨트롤러에서 호출해줍니다.
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
그리고 http://localhost:8080/api/response-status-ex1?message= 를 호출하면 400(Bad Request)가 발생할 것 입니다.
BadRequestException 예외가 컨트롤러 밖으로 나가면 ResponseStatusExceptioNResolver가 @ResponseStatus가 있는 것을 확인하고 오류 코드를 400으로 변경한 것입니다.
@ResponseStatus는 코드를 직접 작성하는 것이기 때문에 라이브러리 예외에는 처리하기가 힘듭니다. 이 경우에는ResponseStatusException을 사용할 수 있습니다.
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new
IllegalArgumentException());
}
@ExceptionHandler, @ControllerAdvice
이제 Spring Boot가 제공하는 @ExceptionHandler와 @ControllerAdvice를 사용해보겠습니다. 결론적으로 우리는 개발을 할 때 이것들만 잘 사용하면 됩니다.
우선 예외 발생 시 API 응답으로 사용할 객체를 만들어줍니다.
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
그리고 예외 처리를 할 컨트롤러에서 @ExceptionHandler를 사용해줍니다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
첫번째의 경우 IllegalArgumentException또는 그 하위 자식 클래스를 모두 처리 가능하고, 두 번째 경우는 메서드 파라미터의 예외가 지정됩니다. 그리고 앞에서 말했듯이 Spring Boot는 자세한 것이 우선 순위가 높습니다.
그런데 @ExceptionHandler를 컨트롤러 내에서 처리하기에는 일반적인 코드와 섞여있을 수 있어 보기 안좋습니다. 그래서 @ControllerAdvice로 분리할 수 있습니다.
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] ex", e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
}
위와 같이 처리하면 모든 컨트롤러에 대해서 에외 처리를 하게 됩니다. @RestControllerAdvice(annotation = RestController.class)라고 하게 되면 @RestController가 달린 컨트롤러에 대해서만 처리하게 되고, @RestControllerAdvice(annotation = "org.example.controllers") 라고 하게 된다면 해당 패키지 내에 존재하는 컨트롤러에 대해서만 처리하게 되고, @RestControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) 라고 해주면 해당 클래스에 대해서만 처리해주게 됩니다.
오늘은 Spring Boot에서 예외 처리를 보기 좋게 해주기 위하여 @ExceptionHandler와 @ControllerAdvice에 대해서 알아보았습니다.
'Spring Boot' 카테고리의 다른 글
[Spring] 챗봇을 활용한 레시피 추천 앱 (0) | 2024.05.02 |
---|---|
[Spring] Servlet에서 예외 처리는 어떻게 진행 될까? (3) | 2024.04.28 |
[Spring Boot] Spring Boot와 MongoDB 연동하기 (2) | 2023.07.19 |
[Spring] Grafana와 Prometheus로 서버 모니터링 하기 (0) | 2023.07.07 |
[Spring] Spring MVC의 구조 (0) | 2023.06.03 |
댓글