안녕하세요. 현재 데브코스 4기로 활동 중인 모아밤팀 서버 개발자 홍혁준입니다.
이번 포스팅에서 예외 처리에 대해 이야기를 풀어내 보려고 합니다. 감사합니다.
배경
모아밤에서는 @RestControllerAdvice와 @ExceptionHandler를 통해 예외 처리를 진행하고 있습니다. 하지만 왜? 해당 방식을 사용할까요? 단순 예외 처리가 아닌 일반적인 요청 흐름과 예외 발생 시, 흐름을 파악하여 왜 해당 방식을 사용하게 되었는 지 살펴봅시다!
사전 지식
BasicErrorController
Spring 1.X 버전부터는 예외 처리를 위한 BasicErrorController를 구현했습니다. 그리고 Spring Boot는 예외 발생 시, 기본적으로 /error로 예외 요청을 다시 전달하도록 WAS 설정이 되어 있습니다. 즉, 별도 설정이 없다면 예외 발생 시, BasicErrorController로 예외 처리 요청이 전달됩니다.
'BasicErrorController'는 'accept' Header에 따라 예외 페이지를 반환하거나 예외 메시지를 반환합니다. 예외 경로는 위에서 말했듯, 기본적으로 '/error'로 정의되어 있습니다.
스프링의 예외 처리 흐름
ExceptionHandlerExceptionResolver 동작
- 예외 발생 시 컨트롤러 안에 적합한 @ExceptionHandler가 있는 지 검사합니다.
- 있다면 처리하고 없다면, @ControllerAdvice/@RestControllerAdvice로 넘어갑니다.
- @ControllerAdvice/@RestControllerAdvice 안에 적합한 @ExceptionHandler가 있으면 처리하고 없다면, 다음 처리기로 넘어갑니다.
ResponseStatusExceptionResolver 동작
- @ResponseStatus/ResponseStatusException가 있는 지 검사합니다.
- 맞다면, ServletResponse의 sendError()로 예외를 Servlet까지 전달합니다.
- Servlet은 BasicErrorController로 요청 전달합니다.
DefaultHandlerExceptionResolver 동작
- Spring 내부 예외인 지 검사 후 맞다면 예외를 처리합니다.
- 없다면 적합한 ExceptionResolver가 없어서 예외가 Servlet까지 전달되고 Servlet은 Spring Boot가 진행한 자동 설정에 맞게 BasicErrorController로 요청을 다시 전달합니다.
요청 흐름
기본 요청 흐름
WAS(Tomcat) -> Filter -> Servlet(Dispatcher-Servlet) -> Interceptor -> Controller
컨트롤러에서 예외 발생 시, 기본 요청 흐름
WAS에 도착하면 Application에서 처리를 못하는 예외가 올라왔다고 판단하고 예외 처리를 진행합니다.
Controller(Error!) -> Interceptor -> Servlet(Dispatcher Servlet) -> Filter -> WAS(Tomcat)
-> Filter Servlet(Dispatcher Servlet) -> Interceptor -> Controller(BasicErrorController)
이처럼 기본적인 예외 처리 방식은 아래와 같이 결국 Exception Controller를 한 번 더 호출하게 됩니다. 즉, Filter or Inteceptor도 다시 호출되는 것이죠. 정리하면, 이는 요청이 2번 생기는 것이 아닌, 1번의 요청이 2번 전달되는 것이게 되므로 이를 제어할 수 있도록 별도의 설정이 필요해 보입니다.
해결하기
try-catch
보통 자바에서는 try-catch를 통해 예외 처리를 합니다. 하지만 모든 코드에 try-catch 문을 붙이는 것은 매우 비효율적입니다.
@ResponseStatus와 ResponseStatusException
@ResponseStatus는 Error HTTP Status를 변경하도록 도와주는 어노테이션이지만, BasicErrorController에 의한 응답입니다. 즉, @ResponseStatus을 처리하는 ResponseStatusExceptionResolver는 WAS까지 예외를 전달시키기 때문에 1번의 요청이 2번 전달됩니다.
그럼 @ResponseStatus의 대안인 Spring 5 버전부터 제공하는 ResponseStatusException은 어떨까요?
ResponseStatusException는 HttpStatus와 함께 선택적으로 reason/cause를 추가할 수 있을 뿐만 아니라, unCheck Exception도 상속받고 있어서 명시적으로 예외 처리를 하지 않아도 됩니다. 즉, Http Status를 직접 설정해 예외 클래스와 결합도를 낮추고 기본적인 예외 처리를 빠르게 적용하여 손쉽게 프로토타이핑할 수 있습니다. 하지만 여전히 @ResponseStatus와 동일하게 ResponseStatusExceptionResolver가 예외를 처리합니다.
@[Rest]ControllerAdvice와 @ExceptionHandler
@ExceptionHandler 어노테이션은 컨트롤러 클래스 혹은 @[Rest]ControllerAdvice가 있는 클래스의 메소드에 해당 어노테이션을 추가하고 Exception Class를 속성으로 받아 손쉽게 예외 처리를 할 수 있도록 도와줍니다. 즉, @[Rest]ControllerAdvice는 @ExceptionHandler를 전역적으로 제공할 수 있도록 스프링에서 제공하는 어노테이션입니다.
또한 위에서 말했듯 @[Rest]ControllerAdvice와 @ExceptionHandler는 Spring에서 예외 처리 공통 관심사(Cross-Cutting Concerns)를 메인 로직으로부터 분리해 다양한 예외 처리 방식을 고안한 방식인 HandlerExceptionResolver의 구현체 중 ExceptionHandlerExceptionResolver로 처리하게 됩니다.
ExceptionHandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태나 응답 메시지를 설정하기 때문에, WAS 입장에선 해당 요청이 정상적인 응답인 것으로 인식되어 위와 다르게 WAS의 에러 전달이 진행되지 않습니다.
이러한 이유로 모아밤에서는 @[Rest]ControllerAdvice와 @ExceptionHandler 예외 처리 방식을 선택하게 되었습니다. 감사합니다.
Reference
'Activity > 데브코스 - 모아밤 테크' 카테고리의 다른 글
Moabam Tech - Caching으로 성능 개선하기 (2) | 2024.01.12 |
---|---|
Moabam Tech - 서브모듈 도입기 (0) | 2023.11.09 |
Moabam Tech - 실시간 선착순 쿠폰 이벤트 도입기 (0) | 2023.11.08 |
Moabam Tech - Embedded Redis 테스트 환경 적용기 (feat. M1-ARM) (0) | 2023.11.04 |
Moabam Tech - 모아밤의 FCM Push Notification 도입기 (0) | 2023.10.29 |