예외가 발생했을 때 /error 로 재요청이 이루어진다. 스프링 부트의 BasicErrorController 는 /error 에 매핑되어 상태 코드에 맞는 오류페이지 뷰를 찾아 렌더링해준다.
서버가 뷰를 반환하는 경우가 아닌 Body 데이터를 담아 응답하는 API 통신에서는 오류페이지 뷰를 렌더링 하는게 아닌 별도 처리가 필요하다. API 통신의 예외처리에 대해 알아보자
BasicErrorController 는 요청의 accept 헤더가 text/html 이면 오류페이지 뷰를 렌더링하도록 동작하고, 아닌 경우 HTTP Body 에 JSON 데이터를 담아 반환한다. 이때 BasicErrorController 가 뷰에 전달하는 데이터인 timestamp, status, error 등을 JSON 화 하여 반환한다.
즉 BasicErrorController 로도 API 예외 처리가 가능하지만, 세밀한 처리가 불가능해 에러 페이지 반환에만 주로 사용한다.
ExceptionResolver 란 스프링에서 컨트롤러의 예외를 잡아 처리하고 정상흐름으로 변경시킨다. 원래 컨트롤러에서 예외가 던져지면 WAS 까지 거슬러 올라가고 /error 로 재요청이 이루어진다. 하지만 ExceptionResolver 를 사용하면 컨트롤러의 예외를 ExceptionResolver 가 처리하고 ModelAndView를 반환해서 정상흐름으로 변경시킨다. 즉 재요청이 이루어지지 않는다. ExceptionResolver 를 사용해도 인터셉터의 postHandle 은 호출되지 않는다.
HandlerExceptionResovler 인터페이스를 구현하고, resolveExcpetion 메서드를 오버라이딩 한다. 그러면 예외가 발생할 경우 해당 메서드가 실행된다.
public class MyHandlerExceptionResolve implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServlerRequest request,
HttpServlerResponse response, Object handler, Exception ex) {
try {
if (ex instance of RuntimeException) {
// response.sendError로 에러코드 변경
// 빈 ModelAndView 반환시 뷰 렌더링 X 정상흐름으로 리턴됨
// return new ModelAndView 에 데이터를 채워서 뷰 렌더링 수행
// response.getWriter() 로 JSON 응답 수행
}
}
}
}
이때 response.sendError 로 에러코드를 변경시키거나, 정상흐름으로 뷰를 렌더링하거나, JSON 응답을 수행할 수도 있다. 즉 예외 발생시 반드시 실행되므로 원하는 처리를 해줄 수 있는 것이다.
생성한 ExceptionResolver는 인터셉터처럼 WebMvnConfigurer 를 구현한 클래스의 extendHandlerExceptionResolver 메서드에서 등록해줘야한다.
@Overried
public void excetndHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
ExceptionResolver 를 사용하면 WAS 까지 예외가 올라가서 재요청 할 필요 없이 중간에 예외를 처리하므로 정상 흐름으로 수행된다. 그런데 구현이 번거롭다. 편의를 위해 스프링 부트가 기본 제공하는 ExceptionResolver 를 우선순위 순서로 알아보자.
ResponseStatusExceptionResolver 는 response.sendError 를 호출하여 상태코드를 변경하여 재요청을 수행한다. 두 가지의 예외를 처리하는데 @ResponseStatus 가 달려있는 예외와 ResponseStatusException 예외를 처리한다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason="잘못된 요청")
public class BadRequestException extends RuntimeException {
}
public String responseStatusEx() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new RuntimeException());
}
위와 같이 @ResponseStatus 가 추가된 예외나 ResponseStatusException이 발생하면, ResponseStatusExceptionResolver 가 이를 낚아챈다. 그 후 sendError 로 상태코드를 변경하여 /error 로 재요청을 수행한다.
DefaultHandelrExceptionResolver 는 스프링 내부에서 발생한 예외를 처리한다. 예를 들어 ModelAttribute 객체에 바인딩 시킬때 타입이 맞지 않으면 TypeMismatchException 이 발생하는데, DefaultHandlerExceptionResolver 가 해당 예외를 처리하여 response.sendError 로 400 으로 상태코드를 변경하며 재요청을 수행한다.
ResponseStatusExceptionResolver 는 ResponseStatus 예외를 잡아 처리하며, DefaultHandelrExceptionResolver 는 스프링 내부 예외를 잡아 처리한다. 그러면 다른 예외들은 누가 처리할까?
스프링은 @ExceptionHandler 어노테이션을 제공한다. 해당 어노테이션이 추가된 메서드는 파라미터의 예외가 발생하면 ExceptionResolver 로 동작한다.
@ExceptionHandler
public ReponseEntity<ErrorData> userExHandle(UserException e) {
ErrorData errorData = new ErrorData("User-ex", e.getMessage());
return new ResponseEntity<>(errorData, HttpStatus.BAD_REQUEST);
}
컨트롤러 내부에 @ExceptionHandler 를 추가하고 UserException 을 파라미터로 하는 메서드를 생성한다. 그러면 UserException 이 발생하면 ExceptionHandlerExceptionResolver 가 해당 메서드를 ExceptionResolver로 활용한다. 위에서는 ResponseEntity 를 반환하므로 메시지 컨버터가 변환하여 오류 결과를 JSON 으로 응답할 것이다.
@ExceptionHandler 는 존재하는 컨트롤러 클래스 내부에서 발생한 예외만 잡아서 처리한다.
@ExceptionHandler 는 정의된 클래스 내부에서 발생한 예외만 처리한다. 이렇게 되면 정상처리 핸들러와 예외처리 핸들러가 뒤섞이게 된다. 따라서 @ControllerAdvice 를 클래스에 추가하여 ExceptionHandler 들을 별도로 갖도록 한다. @ControllerAdvice 는 예외처리를 적용할 컨트롤러 범위를 설정할 수 있고, 설정하지 않으면 모든 컨트롤러에 적용된다. @RestControllerAdvice 도 존재한다.
오류 페이지 반환은 BasicErrorController 를 사용해 뷰만 개발자가 만들어 주면 된다. 다만 세부적인 반환이 필요한 API 오류 반환은 ExceptionResolver 를 사용한다. ExceptionResolver 는 컨트롤러에서 예외 발생시 낚아채서 정상 흐름으로 만들어준다. API 예외 처리는 @RestControllerAdvice 클래스 내부에 @ExceptionHandler 로 예외마다 처리과정을 선언하여 사용한다.