안녕하세요. 저는 백엔드 공부를 하고 있는 학생입니다. 프레임워크는 주로 스프링을 이용해 개발하고 있습니다.
프로젝트를 진행하면 주로 Custom Exception을 이용해 예외처리하고 있습니다. 하지만 실제 Spring 내부에서 발생하는 일이나 효율적인 예외처리에 대해 고민을 해본적은 없었던 것 같습니다. 그래서 저는 이 글을 작성하게 되었습니다.
여기에서부터는 문장의 간결함을 위해서 높임말은 생략하겠습니다.
디스패처 서블릿(Dispatcher Servlet)
→ 이전에는 컨트롤러마다 공통 로직을 복붙 형식으로 다시 작성하여 사용했다.
필터(Filter)
인터셉터(Interceptor)
스프링부트 예외가 발생하면 기본적으로 /error
로 에러 요청을 다시 전달하도록 WAS 설정을 해두었다.
그래서 별도의 설정이 없다면 예외 발생시 BasicErrorController
로 에러 처리 요청이 전달된다.
일반적인 요청 흐름
WAS(톰캣)
→ 필터
→ 디스패처 서블릿
→ 인터셉터
→ 컨트롤러
컨트롤러 하위에서 예외가 발생하고 별도의 예외처리를 하지 않았을 경우 WAS까지 에러가 전달된다.
WAS는 애플리케이션이 처리할 수 없는 예외가 올라왔다고 판단하고 대응 작업을 진행한다.
컨트롤러(예외발생)
→ 인터셉터
→ 디스패처 서블릿
→ 필터
→ WAS(톰캣)
WAS는 스프링 부트가 등록한 에러 설정(/error
)에 맞게 요청을 전달한다.
WAS(톰캣)
→ 필터
→ 디스패처 서블릿
→ 인터셉터
→ 컨트롤러
→ 컨트롤러(예외발생)
→ 인터셉터
→ 디스패처 서블릿
→ 필터
→ WAS(톰캣)
→ WAS(톰캣)
→ 필터
→ 디스패처 서블릿
→ 인터셉터
→ 컨트롤러(BasicErrorController)
기본적인 예외처리는 에러 컨트롤러를 한번 더 호출한다.
기본 설정으로 받는 에러 응답은 나름 잘 갖추어져 있지만 클라이언트 입장에서 유용하지 못하다는 단점이 있다.(설정으로 변경할 수 있지만 유의미한 에러 응답을 전달하지 못한다.)
에러가 처리되지 않고 WAS에서 에러를 전달받아서 status가 500이 뜬다.
← 필터에서 Exception이 던져졌다면 에러가 처리되지 않고 서블릿까지 전달된다. 서블릿은 예외가 핸들링 되기를 기대했지만, 예외가 그대로 올라와서 예상치 못한 Exception을 만나 내부에 문제가 있다고 판단하여 status가 500이 뜬다.
Spring은 예외 처리 전략을 추상화한 HandlerExceptionResolver
인터페이스를 만들었다. (전략 패턴이 사용된 것이다.)
→ 에러 처리라는 공통 관심사를 메인 로직으로부터 분리하였다.
HandlerExceptionResolver
는 발생한 Exception을 catch하고 HTTP 상태, 응답 메시지 등을 설정한다.
WAS는 해당 요청이 정상적인 응답인 것으로 인식이 되어 위에서 설명한 WAS의 에러 전달이 진행되지 않는다.
public interface HandlerExceptionResolver {
//Object 타입의 handler는 예외가 발생한 컨트롤러 객체이다.
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}
예외가 던져지면 디스패처 서블릿까지 전달되고 적합한 예외처리를 위해 HandlerExceptionResolver
구현체들을 빈으로 등록해서 관리한다. 그리고 적용 가능한 구현체를 찾아 예외처리를 한다.
DefaultErrorAttributes
: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.ExceptionHandlerExceptionResolver
: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함ResponseStatusExceptionResolver
: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함DefaultHandlerExceptionResolver
: 스프링 내부의 기본 예외들을 처리한다.직접 예외를 처리하지 않고 속성만 관리하는 DefaultErrorAttributes
를 제외하고 직접 에러를 처리하는 ExceptionResolver
들을 HandlerExceptionResolverComposite
로 모아서 관리한다.(컴포지트 패턴이 적용됐다.)
ResponseStatus
에러 HTTP 상태를 변경하도록 도와주는 어노테이션이다.
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
...
}
@ResponseStatus
를 사용하는 경우
@ExceptionHandler
와 함께@RestControllerAdvice
와 함께@ResponseStatus의 한계
→ DefaultErrorAttributes
를 수정하면 가능하긴 하다.
@ResponseStatus
를 붙일 수 없다.프로퍼티 설정이나 에러 응답 커스터마이징를 이용해 해결할 수 있지만 개발자가 원하는대로 에러를 처리하는 것은 어렵다.
ResponseStatusException
@ResponseStatus
의 프로그래밍적 대안으로써 손쉽게 에러를 반환할 수 있다.
HttpStatus와 함께 선택적으로 reason과 cause를 추가할 수 있다.
언체크 예외를 상속받고 있어서 명시적으로 예외를 처리하지 않아도 된다.
@GetMapping("/{id}")
public ResponseEntity<Post> getPost(@PathVariable String id) {
try {
return ResponseEntity.ok(postService.getPost(id));
} catch (NoSuchElementFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Post Not Found");
}
}
ResponseStatusException의 장점
→ 예외 클래스를 사용하지 않는다.
ResponseStatusException ****의 한계
ExceptionHandler
유연하게 에러처리 할 수 있는 방법이다.
@RestController
@RequiredArgsConstructor
public class ProductController {
private final PostService postService;
@GetMapping("/{id}")
public Response getPost(@PathVariable String id){
return postServcie.getPost(id);
}
@ExceptionHandler(NoSuchElementFoundException.class)
public ResponseEntity<String> handleNoSuchElementFoundException(NoSuchElementFoundException exception) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
}
}
Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다.
ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않으면 파라미터에 설정된 에러 클래스를 처리한다.
@ResponseStatus
와 다르게 에러 응답을 자유롭게 할 수 있다.
@ExceptionHandler
사용시 주의할 점 & 단점
@ExceptionHandler
에 등록된 예외 클래스와 파라미터로 받는 예외 클래스가 동일해야 한다.→ 값이 다르면 런타입 에러가 발생한다.
→ 에러 처리 코드가 중복될 수 있다.
@ControllerAdvice
와 @RestControllerAdvice
전역적으로 @ExceptionHandler
를 적용할 수 있다.
둘의 차이점은 @ResponseBody 유무로 응답을 Json으로 내려준다는 점(RestControllerAdvice
인 경우)이 다르다.(ex. Controller vs RestController)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoSuchElementFoundException.class)
protected ResponseEntity<?> handleIllegalArgumentException(NoSuchElementFoundException e) {
final ErrorResponse errorResponse = ErrorResponse.builder()
.code("Item Not Found")
.message(e.getMessage()).build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
ControllerAdvice
의 장점
ControllerAdvice
사용시 주의할 점
ControllerAdvice
만 관리하는 것이 좋다.→ 여러 개 있을 경우 @Order
를 이용해서 순서를 지정해야한다.(하지 않으면 임의의 순서로 에러를 처리한다.)
예외가 발생하면
ExceptionHandlerExceptionResolver
→ 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler
가 있는지 검사한다.
→ 컨트롤러의 @ExceptionHandler
에서 처리가능하다면 처리하고, 그렇지 않으면 ControllerAdvice
로 넘어간다.
→ ControllerAdvice
안에 적합한 @ExceptionHandler
가 있는지 검사하고 없으면 다음 처리기로 넘어간다.
ResponseStatusExceptionResolver
→ @ResponseStatus
가 있는지 또는 ResponseStatusException
인지 검사한다.
→ 맞으면 ServletResponse
의 sendError()
로 예외를 디스패처 서블릿까지 전달되고, 디스패처 서블릿이 BasicErrorController
로 요청을 전달한다.
DefaultHandlerExceptionResolver
→ Spring의 내부 예외인지 검사하여 맞으면 에러를 처리하고 아니면 넘어간다.
ExceptionResolver
가 없으므로 예외가 서블릿까지 전달된다.→ 서블릿은 SpringBoot가 진행한 자동 설정에 맞게 BasicErrorController
로 요청을 다시 전달한다.
@ResponseStatus
, ResponseStatusException
등과 같이 직접 에러 응답을 반환하지 않는 경우에는 BasicErrorController
를 거쳐 에러가 처리된다.
→ 내부에서 2번 컨트롤러 요청이 전달된다.
→ ExceptionHandler나 ControllerAdvice처럼 직접 에러를 반환하는 경우에는 BasicErrorController
를 거치지 않는다.
[Spring] 디스패처 서블릿이란? (Dispatcher Servlet)
[Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)
[Spring] 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)