스프링에서는 예외 처리를 상당히 깔끔하게 할 수 있는 HandlerExceptionResolver를 제공한다.
해당 인터페이스를 implements 하여 예외처리를 직접 구현하여 서블릿 컨테이너까지 전달되는 예외를 정상 처리로 변경할 수 있거나, 사용자 정의 Exception일 때 특정 에러 페이지를 렌더링 하여 클라이언트에게 전달해 줄 수 있다.
@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// header text/html, application/json
try {
if (ex instanceof UserException) {
log.info("userException resolver to 400");
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
String result = objectMapper.writeValueAsString(errorResult);
response.getWriter().write(result);
return new ModelAndView();
} else {
return new ModelAndView("error/500");
}
}
}catch(IOException e) {
}
return null;
}
}
API에 대한 예외 처리를 할 때 기업 간의 혹은 오픈 API를 정의할 때 예외 스펙을 해당 익셉션 리졸버에서 만들어 리턴하게 할 수 있다. 이 응답은 DispatcherServlet으로 전달되기 때문에 반환 값은 ModelAndView객체를 반환한다. 만약 모델 앤 뷰 객체가 비어있을 때 빈 뷰를 렌더링 하는 대신 서블릿 컨테이너에게 바로 전달한다.
만약 null을 리턴하게 된다면 다음 HandlerExceptionResolver를 찾게 되며 전부 null을 리턴할 시 예외를 잡지 못하였기 때문에 서블릿 컨테이너가 서버 오류로 처리하게 되어 서블릿 컨테이너의 기본정책을 따르게 된다.
또한 Response객체의 sendError()메서드를 통해 처리할 수도 있다. 하지만 ExceptionResolver를 사용하는 큰 이유는 복잡한 서버 내부 호출 과정을 줄이는 의도가 크다고 볼 수 있다.
기존 에러를 처리하기 위해 WAS -> 필터 -> FrontController -> 인터셉터 -> 핸들러까지 진행한 후에(혹은 더 깊이)
다시 역순으로 WAS까지 예외가 던저진 후 WAS에서 예외를 처리하기 위한 핸들러를 재호출 하면서 여러 리소스를 사용하게 되는데, 한번 던져진 예외를 HandlerExceptionResovler에서 예외를 잡아서 처리하기 때문에 WAS는 정상응 답으로 판단하여 번거로운 작업이 사라지게 된다.
스프링 부트 기본제공 ExceptionResolver
사용자가 정의한 에러가 추가될 때마다 HandlerExceptionResolver를 구현하고 Spring에 등록하는 과정은 상당히 번잡하다. 그렇기에 스프링 부트는 여러 HandlerExceptionResolver를 기본 제공한다.
- ExceptionHandlerExceptionResolver : @ExceptionHandler를 처리하며, API 예외 처리는 대부분 이 기능을 해결이 가능하다.
- ResponseStatusExceptionResolver : HTTP 상태 코드를 지정해준다.
@ResponseStatus( value = HttpStatus.NOT_FOUND)와 같은 애노테이션이 달려있는 예외와 ResponseStatusException 예외를 처리한다. - DefaultHandlerExceptionResolver : 스프링 내부 기본 예외를 처리한다.
이 순서는 Resolver의 우선순위별로 나열되어있다.
ResponseStatusExpcetion
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "클라이언트 오류 잘못된 요청")
public class BadRequestException extends RuntimeException{
}
이 방식은 response.sendError(status, reasone)과 동일하게 동작한다. 서버가 전달하는 HTTP Status의 코드와 메시지를 애노테이션으로 등록 시 ResponseStatusExpcetionResolver에서 response.sendError()를 사용한다.
또한 Spring의 Mesaage를 reason에 사용할 수 있다.
그런데 @ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 적용할 수없다. 어노테이션을 직접 넣어야 하는데 라이브러리의 같은 예외 코드에는 적용할 수 없다. 이러한 경우에는 ResponseStatusException 예외를 사용하면 된다.
private String responseStatusException() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "error.bad", new IllegalArgumentException());
}
DefaultHandlerExceptionResolver
스프링 내부에서 발생하는 스프링 예외를 해결한다. 대표적으로 파라미터 바인딩 시점에서 타입이 맞지 않으면 내부에서 TypeMismatchEception이 발생하는데 이 경우 예외를 일반적으로 던지면 500 오류가 발생한다. 하지만 바인딩이 되지 않는 오류는 대부분 클라이언트 오류로써 클라이언트 오류는 HTTP 상태 코드 400인 클라이언트 오류로 처리하도록 되어있다. 이때 DefaultHandlerExceptionResolver는 500 오류는 400 오류로 변경한다.
protected ModelAndView handleTypeMismatch(TypeMismatchException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return new ModelAndView();
}
실제 스프링에서 TypeMismatchException을 처리하는 코드이다. 이 코드도 보면 response.sendError()를 통해 상태 코드를 변경하고 빈 ModelAndView를 반환한다. 하지만 sendError()의 내역 때문에 다시 해당 오류 처리 페이지를 서블릿 컨테이너가 재 호출한다.
즉 스프링이 내부에서 터진 예외를 HTTP 상태 코드에 맞게 예외를 중간에 ExceptionHandlerResolver를 통해 변환해주는 작업들을 대신해준다.
하지만 요즘은 서버 to 서버 통신을 할 때는 HTTP API를 사용하여 API통신을 하는데 대부분 JSON, 혹은 XMl 등등 예외에 대해서 메시지 바디에 데이터를 직접 규약에 맞게 넣어준다.
지금까지 한 내용을 바탕으로 해당 처리를 하려면 ExceptionHandlerResovler를 사용하여 재정의해야 하며, return값이 ModelAndView라는 것은 큰 제약이 되어 예전 서블릿 코드를 작성하듯이 response.getWriter(). write()로 지정해주어야 하는 불편함을 가지는데 이러한 불편함을 처리해주기 위해 @ExceptionHandler라는 예외 처리 기능을 제공한다.
@ExceptionHandler
HTML 화면 오류와 API오류는 다른데, 웹 브라우저의 HTML오류 화면을 제공할 때는 BascitErrorController를 사용하면 매우 편하다. 단지 에러 페이지만 만들어서 주면 되고 나머지는 스프링이 모두 구현했기 때문이다.
API는 정말 응답이 모두 다르고, 스펙도 모두 달라서 여러 예외 응답을 내려줄 필요가 있다.
BasicErrorController를 상속받아 protected를 오버라이드 하여 구현하거나, HandlerExceptionResolver를 직접 구현하는 방식으로는 API예외를 다루기는 쉽지 않다.
스프링은 API 예외 처리 문제를 해결하기 위 @ExceptionHandler라는 애노테이션을 제공한다.
이 애노테이션은 ExceptionHandlerExceptionResolver를 사용하며, 이는 스프링 부트의 첫 번째 우선순위를 가지고 등록된다.
@RestController
------
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e){
log.info("[exceptionHandler] ", e);
return new ErrorResult("BAD", e.getMessage());
}
ExceptionResolver에서 예외 시도를 하는데 ExceptionHandlerExceptionResolver가 첫 번째 우선순위를 가지고 있기 때문에 예외 처리 여부를 묻게 된다. 그럼 ExceptionHandlerExceptionResolver는 Controller에 @ExceptionHandler가 존재하는지 확인하고 매칭 시 해당 메서드를 실행시켜준다.
그래서 HTTP 응답으로 오류를 정의한 객체를 내려줄 수 있게 된다. 이는 정상 흐름으로 변경되어 HTTP 상태 코드가 200으로 전달된다. 그런데 사실 예외 상태 코드를 변경해야 한다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e){
log.info("[exceptionHandler] ", e);
return new ErrorResult("BAD", e.getMessage());
}
다음과 같이 @ResponseStatus 어노테이션을 추가하면 지정된 Http상태 코드로 내려온다.
@ExceptionHandler
public String userExHandler(Exception e) {
return "error/500";
}
또한 View페이지 제공도 가능하며 컨트롤러에 정의되지 않은 Exception이 발생한 경우 부모 타입으로 확장되어서 해당 자식 클래스까지 Exception을 처리하게 된다.
해당 어노테이션은 생략하면 핸들러 Argument에 지정한 Type으로 처리되며, 여러 Exception을 한 번에 잡고 싶다면 배열을 사용할 수 있다.
만약 컨트롤러에 예외가 같이 존재하는게 싫다면 다음을 고려해보자
'Spring|Spring-boot' 카테고리의 다른 글
스프링 DB (1) (0) | 2022.08.15 |
---|---|
파일 업로드 및 다운로드 (0) | 2022.01.26 |
Spring-boot Error Page (0) | 2022.01.13 |
Spring Formatter (0) | 2022.01.10 |
Spring Converter (0) | 2022.01.10 |
댓글