쟈미로그
[Spring] Spring의 예외 처리 구조 본문
서론
평소에 전역으로 예외를 처리해서 반환해줄 일이 있을 때면 @ControllerAdvice와 @HandlerException을 사용하곤했다.
많은 레퍼런스에서 이 방식을 사용하길래 쓰게 됐고, 자세한 동작원리는 모른 채 사용했다. 그렇게 예외처리 == @ControllerAdvice + @HandlerException을 사용하는 것 이라는 생각을 갖고 개발하다가 실수를 하게됐다. 이미 응답을 준 상황에서도 예외를 던지면 ControllerAdvice의 HandlerException가 잡아서 처리해줄 거라는 착각을 한 것..!
두 어노테이션의 동작 방식을 몰랐고, "예외 처리"를 왜 하는가에 대한 개념도 모호해서 이런 일이 발생했던 것 같다. (예외가 발생했을 때 왜 잡을까? 잡아서 클라이언트에게 보여주는 이유 등)
그래서 이참에 스프링의 예외 처리 구조를 알아보고, 예외처리에 대해 바뀐 내 생각도 정리해보고자한다!
스프링의 예외처리 방법
스프링은 예외를 효과적으로 다룰 수 있도록 다양한 방법을 제공한다고 한다.
예외 발생 시 예외를 잡고 커스텀하게 반환해주기 위해서, 스프링엔 예외처리 전략을 추상화한 HandlerExceptionResolver 인터페이스가 만들어져있다.
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
(주의할 점은 파라미터의 handler에 들어오는 값이 예외가 발생한 컨트롤러 객체라는 점이다. 그래서 HandlerExceptionResolver는 컨트롤러에서 발생하는 예외만을 처리할 수 있다.)
컨트롤러에서 예외가 발생하면 그 예외는 디스패쳐 서블릿까지 던져지는데, HandlerExceptionResolver의 구현체들 중에서 적용 가능한 구현체에 맞춰서 예외가 처리돼서 응답으로 내려진다.
HandlerExceptionResolver 구현체의 종류는 총 4개다.
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
- DefaultErrorAttributes
여기서 DefaultErrorAttributes는 직접 예외를 처리하진 않고 속성만 관리하는 것이라서 다른 구현체들과 성격이 다르다고 한다. 그래서 나머지 1, 2, 3 구현체들 위주로 알아보자. (예외 발생 시 처리되는 구현체의 우선순위는 위 목록 1, 2, 3번 순서대로 실행된다.)
1. ExceptionHandlerExceptionResolver
@ExceptionHandler 어노테이션을 사용해서 예외를 처리하면 ExceptionHandlerExceptionResolver가 동작한다.
ExceptionHandler는
- @Controller/@RestController 클래스의 메소드
- @ControllerAdvice/@RestControllerAdvice 클래스의 메소드
에 붙여서 사용할 수 있다.
@RestController
public class ProductController {
...
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(NotFoundException exception) {
...
}
}
(컨트롤러에 사용하는 예시)
특정 컨트롤러에서 ExceptionHandler 메소드를 정의하면 해당 컨트롤러에서만 그 ExceptionHandler가 동작한다. 그래서 전역으로 예외를 처리하고싶다면 컨트롤러 어드바이스를 사용하는 것이 더 편하다.
2. ResponseStatusExceptionResolver
ResponseStatusExceptionResolver은 @ResponseStatus, ResponseStatusException를 사용하면 동작한다.
2-1. @ResponseStatus
@ResponseStatus는 예외의 HTTP 상태코드를 변경해주는 어노테이션이다.
ResponseStatus는
- Exception 클래스 자체
- @ExceptionHandler와 함께 메소드
- @ControllerAdvice/@RestControllerAdvice와 함께 클래스
에 붙여 사용할 수 있다.
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class CustomNotFoundException extends RuntimeException {
...
}
(Exception 클래스 자체에 사용하는 예시)
이렇게 사용할 경우 아래와 같은 응답을 받을 수 있는데, 이는 문제가 있다.
{
"timestamp": "2023-04-04T14:26:45.146+00:00",
"status": 404,
"error": "Not Found",
"path": "/test"
}
이 응답은 BasicErrorController에 의한 것으로 (ex. WhiteLabel 에러 페이지..)
결국 디스패쳐 서블릿에서 ResponseStatusExceptionResolver가 동작해도 WAS의 에러처리 동작을 하는 것이다. (자세한건 BasicErrorController를 찾아보자)
이처럼 @ResponseStatus는
- WAS의 복잡한 에러처리 요청을 거치는 것 (BasicErrorController)
- 응답 내용을 변경하기 까다롭다는 것
- 상태코드를 커스텀하게 쓰기 불편함
등에서 사용하기 불편한 점들이 많다.
2-2. ResponseStatusException
@StatusException는 알아봤듯이 응답을 커스텀하게 쓰기엔 불편하다.
그래서 Spring5에선 상태코드 외에도 reason, cause도 추가할 수 있는 ResponseStatusException이 추가됐다.
이렇게 사용할 수 있다.
...
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Custom Not Found!!!");
...
하지만 결국 WAS 에러처리 요청을 거친다는 점과, 에러응답을 완전히 제어할 수는 없다는 점에서 @ExceptionHandler보다 한계가 있다.
3. DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver는 스프링에서 발생하는 주요 예외를 처리해서 반환해주는 기본 리졸버다. 가장 마지막 우선순위로 쓰이는 리졸버다.
예외 발생 시 이 리졸버가 동작했다는 것은 해당 예외가 따로 처리되지 않았다는 것으로, @HandlerException으로 커스텀하게 예외를 반환하고 있다면 이 리졸버는 최대한 동작하지 않도록 하는 것이 좋은 것 같다. (예외 응답이 일관적이지 않을테니까)
그렇게 하기 위해선 많은 예외들을 @ControllerAdvice안의 @HandlerException으로 잘 처리해야하는데 번거로움을 느낄 수 있다.
다행히 마침 스프링은 기본적인 예외를 미리 처리해둔 ResponseEntityExceptionHandler라는 추상클래스를 제공하고 있다. 그래서 @ControllerAdvice 클래스가 해당 추상클래스를 상속받도록 한다면, 많은 예외들을 일관적인 응답으로 내릴 수 있게된다!
참고
https://mangkyu.tistory.com/204
https://jaehun2841.github.io/2018/08/30/2018-08-25-spring-mvc-handle-exception/#Controller-%EB%A0%88%EB%B2%A8%EC%97%90%EC%84%9C%EC%9D%98-%EC%B2%98%EB%A6%AC
'Spring Boot' 카테고리의 다른 글
[Spring] 스프링의 AOP (0) | 2023.06.19 |
---|