이전 포스팅에서 이어집니다.
스프링이 제공하는 다양한 예외처리 방법
스프링에서는 다양한 예외 처리 전략을 수행하기 위해 HandlerExceptionResolver 인터페이스를 제공한다.
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex
);
}
직접 인터페이스에 접근해서 정의되어 있는 메서드를 살펴보면 request, response 객체, 요청을 수행하던 Controller 객체를 나타내는 handler, 발생한 예외가 담겨있는 Exception으로 이뤄져있다. 그래서 HandlerExceptionResolver의 구현체들은 해당 메서드를 구현함으로써 발생한 Exception을 catch하고 HttpStatus와 응답 메세지를 알맞게 조작할 수 있다.
스프링에서는 기본적으로 예외를 처리하기 위한 ExceptionHandlerResolvers 구현체들을 빈으로 등록해 관리하고 있는데 그 종류는 다음과 같다. 또한 빈으로 관리되는 ExceptionResolver들이 동작하는 순서는 위에서 먼저 소개하는 순과 동일하게 진행된다.
- DefaultErrorAttributes : 에러와 관련된 속성을 저장만 하고 직접 예외처리를 진행하지는 않는다.
- ExceptionHandlerExceptionResolver : 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리한다.
- ResponseStatusExceptionResolver : Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException을 처리한다.
- DefaultHandlerExceptionResolver: 스프링 내부의 기본 예외들을 처리한다.
그러면 우리는 어떻게 이 구현체들을 사용해서 예외를 처리할 수 있을까? 바로 아래에 소개되는 키워드를 사용하면 ExceptionResolver를 동작시켜 에러를 처리할 수 있다고 한다.
- @ResponseStatus
- ResponseStatusException
- @ExceptionHandler
- @ControllerAdvice, @RestControllerAdvice
[@ResponseStatus, ResponseStatusException]
앞에 소개된 ResponseStatus와 ResponseStatusException은 발생한 예외에 대해 반환할 HttpStatus타입을 직접 결정해줄 수 있다는 특징이 있다. 또한 ResponseStatusException을 사용하면 발생한 예외에 대한 메세지를 작성할 수 있으므로 별도의 예외 클래스를 만들지 않아도 된다는 장점이 있다.
하지만 ResponseStatus와 ResponseStatusException을 사용하는 경우 그때그때 예외 처리 코드를 직접 작성해줘야 한다는 번거로움이 발생한다. 또한 우리가 처음 원했던대로 WAS에서 바로 에러 응답 메세지가 반환되는 것이 아니라 BasicErrorController를 거쳐서 반환되게 된다는 문제점이 있다. 즉 ResponseStatusExceptionResolver를 통해 처리되는 예외는 직접 응답 메세지로 만들어주는 처리를 진행하는 것이 아니기 때문에 WAS의 에러 처리 로직을 수행하게 된다.
[@ExceptionHandler]
ExceptionHandler는 매우 유연하게 에러를 처리할 수 있도록 도와주는 기능이다. 다음과 같은 부분에 @ExceptionHandler를 사용해서 에러를 처리하도록 할 수 있다.
- Controller 메서드
- @ControllerAdvice, @RestControllerAdvice가 선언된 클래스의 메서드
@ExceptionHandler 어노테이션이 사용된 메서드에서 반환되는 예외 응답 메세지는 ExceptionHandlerExceptionResolver가 처리하게 된다. 이렇게 반환되는 에러 응답 메세지는 WAS 입장에서 에러에 대한 처리가 이뤄졌다고 판단하기 때문에 사용자가 직접 지정해준 응답 메세지 형식 그대로 클라이언트에게 넘길 수 있다는 장점이 생기게 된다.
@ExceptionHandler는 해당 어노테이션이 적용된 메서드가 위치한 컨트롤러에서 발생하는 예외만 처리하게 된다. 이 때문에 다른 컨트롤러에서 동일한 예외를 처리하는 로직이 중복으로 작성될 가능성이 높다. 이를 해결하기 위해 제공되는 것이 @ControllerAdvice와 @RestControllerAdvice다.
[@ControllerAdvice, @RestControllerAdvice]
ControllerAdvice는 @ExceptionHandler를 전역적으로 적용시켜준다. 이로 인해 하나의 클래스로 모든 컨트롤러에 대한 예외를 집약적으로 처리해줄 수 있다는 장점을 취할 수 있게 된다. 두 Advice 어노테이션의 차이는 @Controller와 @RestController 어노테이션의 차이처럼 @ResponseBody가 붙어있다는 것이다.
기본적으로 위에서 언급한대로 ExceptionHandlerExceptionResolver를 통해 예외가 처리되기 때문에 사용자가 설정한 응답 메세지가 별도의 WAS 에러 처리 로직을 거치지 않고 클라이언트에게 일관적으로 전달된다.
문제 해결
이전 글에서 스프링 예외 처리 방법에 대해 여러가지를 알아봤다. 특별히 단일 controller에 대해서만 예외처리를 따로 진행해줘야 하는 경우가 아니라면 ControllerAdvice 혹은 RestControllerAdvice를 사용해서 예외를 처리해주는 것이 바람직해 보인다.
이를 기반으로 문제제기에서 겪었던 문제를 해결하면 다음과 같이 RestControllerAdvice를 구현해서 해결해볼 수 있다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({MissingAuthorizationHeaderException.class, IncorrectAuthorizationMethodException.class})
public ResponseEntity<ErrorResponse> handleAuthorizationException(Exception exception) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(exception.getMessage()));
}
}
이렇게 RestControllerAdvice를 정의하고 다시 동일한 요청을 서버에 보내보자.
이제 우리는 앞에서 언급했던 문제들을 해결하고 원하는 양식의 에러 응답 메세지를 클라이언트에게 내려줄 수 있게 되었다.
Reference
'우아한테크코스 > 학습 정리' 카테고리의 다른 글
Domain 그리고 Entity, VO에 대한 개인적인 고찰 (0) | 2023.06.07 |
---|---|
스프링 @Transactional(readOnly=true)에 관한 간단한 고찰 (1) | 2023.06.04 |
스프링의 예외처리 과정 및 처리 방법에 대하여 (1) (0) | 2023.05.12 |
[Level 2] Layered Architecture에 대한 개인적인 고찰 (0) | 2023.04.27 |
[Level 2] Repository와 Dao를 분리하는 기준 (5) | 2023.04.21 |