HTML은 HTTP 상태 코드에 따른 오류 페이지로 대부분 해결이 되지만 API의 경우에는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 반환해야 하기 때문에 더 복잡하다.

 

BasicErrorController

스프링부트가 제공하는 BasicErrorController에는 errorHtml(), error() 두개의 메서드가 있는데 클라이언트 요청이 Accept 헤더 값이 text/html인 경우에는 errorHtml()이 호출이 되고 그 외에는 error()가 호출이 되어 ResponseEntity로 HTTP Body에 JSON 데이터를 반환한다. 하지만 API 오류 처리는 각가의 경우에 따라 응답 결과를 다양하게 출력해야 되기 때문에 BasicErrorController보다는 @ExceptionHandler를 사용하는 것이 좋다.

 

HandlerExceptionResolver -> ExceptionResolver -> @ExceptionHandler

 

스프링 MVC는 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 된다.

DispatcherServlet에서 예외를 받아서 처리를 하고 정답 응답으로 바꿔 놓을 수 있다. (인터셉터의 postHandle()은 호출되지 않음)

 

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
    public ModelAndView resolveException(HttpServletRequest request,
        HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();
             }
         } catch (IOException e) {
             ...
         }
         return null;
     }
}

 

ModelAndView를 반환하는 이유는 예외를 처리해서 정상 흐름처럼 변경하는 것이 목적이기 때문이다.

DispatcherServlet은 HandlerExceptionResolver의 반환 값(ModelAndView)에 따라 다음과 같이 동작한다.

 

  • 빈 ModelAndView : 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴한다.
  • ModelAndView 지정 : ModelAndView에 View, Model 등의 정보를 담아서 반환하고 뷰를 렌더링한다.
  • null : 다음 ExceptionResolver를 찾아서 실행하고, 없을 경우 기존에 발생한 예외를 서블릿 밖으로 던진다

 

WAS까지 예외가 전달이 되고, WAS에서 오류 페이지 정보를 찾아서 다시 오류 페이지를 호출하는 과정은 너무 복잡한데 HandlerExceptionResolver를 통해 DispatcherServlet에서 예외를 처리하면 서블릿 컨테이너까지 예외가 전달되지 않아 WAS 입장에서는 정상 처리가 된 것처럼 만들 수 있다. 서블릿 컨테이너까지 예외가 올라가면 추가 프로세스가 복잡하게 실행되기 때문에 성능상 이점도 있는 것이다.

이제 RuntimeException을 상속 받은 예외 클래스를 생성하고 해당 예외를 처리하는 HandlerExceptionResolver를 만든 다음 WebMvcConfigurer에서 extendHandlerExceptionResolvers를 Override해서 resolver.add로 등록할 수 있다.

 

// WebMvcConfigurer 를 통해 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new MyHandlerExceptionResolver());
}

 


 

스프링 ExceptionResolver

 

위에서처럼 HandlerExceptionResolver를 직접 만들어 response에 데이터를 넣고, ModelAndView를 반환하는 것은 너무 불편하다.

스프링부트가 제공하는 ExceptionResolver는 HandlerExceptionResolverComposite에 다음과 같이 우선 순위 순으로 ExceptionResolver를 등록한다.

 

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver

 

스프링이 제공하는 ExceptionResolver를 통해 @ControllerAdvice, @RestControllerAdvice로 예외 처리를 하나의 클래스로 묶고 @ExceptionHandler와 @ResponseStatus를 통해 예외 처리를 메서드로 편리하게 관리할 수 있다.

 

 

1. ExceptionHandlerExceptionResolver

스프링은 @ExceptionHandler를 이용한 편리한 예외 처리 기능을 제공하는데 웹 화면에 HTML 오류 화면을 제공하는 경우는 BasicErrorController를 사용하는 것이 편하지만 API 예외의 경우는 복잡하기 때문에 세밀하게 설정할 필요가 있다.

 

@ExceptionHandler 애노테이션을 선언하고 처리하고 싶은 예외를 지정하면 해당 컨트롤러에서 예외 발생 시 ExceptionHanlderExceptionResolver는 해당 예외를 처리할 수 있는 @ExceptionHandler가 있는지 확인하고 실행하여 예외를 처리한다. ErrorResult를 반환하며 @ResponseStatus와 같이 사용할 수 있다. @ExceptionHanlder에 예외를 지정하지 않으면 해당 메서드의 파라미터 예외를 사용하며 @ResponseStatus 대신 ErrorResult를 ResponseEntity로 감싸서 HTTP 응답 코드를 동적으로 변경하여 줄 수도 있다.

컨트롤러에 기존 메서드와 예외 처리 @ExceptionHanlder 메서드가 섞여있는데 @ControllerAdvice, @RestControllerAdvice를 사용하면 이를 분리할 수 있다. @ExceptionHandler를 적용할 컨트롤러 또는 패키지를 지정하거나 아무 것도 지정하지 않으면 모든 컨트롤러에 글로벌ㄹ 적용할 수 있다.

 

2. ResponseStatusExceptionResolver

@ResponseStatus 애노테이션으로 상태 코드와 reason을 적용하면 해당 예외가 발생하여 컨트롤러 밖으로 넘어갈 때, ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인하고 오류 코드, 메시지를 적용한다. 결국 내부적으로 response.sendError() 를 호출하기 때문에 WAS에서 다시 오류 페이지(/error)를 내부 요청한다.

@GetMapping("/")
public String responseStatusEx() {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}

 reason은 MessageSource에서 찾는 기능도 제공하기 때문에 다음과 같이 messages.properties를 통해 사용할 수 있다.

error.bad=잘못된 요청 오류입니다. 메시지 사용

 

@ResponseStatus 애노테이션은 코드를 수정할 수 없는 라이브러리 예외 코드나 조건에 따라 동적으로 변경하고 싶은 경우 사용하기 힘들기 때문에 이런 경우는 ResponseStatusException을 직접 throw 하는 것이 좋다.

 

 

3. DefaultHandlerExceptionResolver

 

스프링 내부에서 발생하는 스프링 예외를 해결하는데 파라미터 바인딩 시점에 타입이 맞지 않는 경우 내부적으로 발생하는 TypeMismatchException의 경우 서블릿 컨테이너로 전달되어 500 오류가 발생하지만 파라미터 바인딩은 대부분 클라이언트에서 잘못 호출해서 발생하기 때문에 400오류가 더 적절하다. 이런 경우에 DefaultHandlerExceptionResolver는 이것을 400 오류로 변경한다. DefaultHandlerExceptionResolver도 내부적으로 sendError를 사용하며 스프링 내부 오류를 어떻게 처리할지 수 많은 내용이 정의되어 있다.

 

 

 

+ Recent posts