스프링의 예외 처리

기본 예외 처리

스프링은 기본적으로 예외가 발생하면 /error에 매핑된 핸들러를 찾는다.
만약 /error에 매핑되지 않았다면 우리가 흔히 보게 되는 Whitelabel Error Page를 보여주게 된다.

스프링 MVC의 요청 흐름

요청이 오면 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 핸들러 순으로 진행된다.
이때 컨트롤러에서 예외가 처리되지 않고 발생하면 컨트롤러 -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣) 순으로 예외가 전파된다.

DispatcherServlet의 예외 처리

디스패처 서블릿의 application context는 HandlerExceptionResolver를 구현해서 처리되지 않은 예외를 인터셉트해서 처리한다.

1
2
3
4
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
}

Spring MVC에서는 기본적으로 세가지 예외 리졸버를 만든다.

  • ExceptionHandlerExceptionResolver : 핸들러나 컨트롤러 어드바이스에서 @ExceptionHandler 가 처리할 수 있는 예외를 처리한다.
  • ResponseStatusExceptionResolver : @ResponseStatus 가 붙은 예외를 처리한다.
  • DefaultHandlerExceptionResolver : 스프링의 기본 전략으로 예외를 처리한다.

이 세가지 리졸버는 순서대로 체인되어 실행된다.

서블릿에서 예외

서블릿에서는 크게 두가지 로 예외 처리를 지원한다.

  • Exception
  • response.sendError(HTTP 상태 코드, 오류 메시지)

예외

자바에서 직접 실행하는 경우, main 함수를 실행하는 main 스레드가 예외를 만나서 처리하지 못하고 main 메서드를 넘어가게 되면 예외 정보를 남기고 스레드가 종료된다.

한편 웹 어플리케이션은 요청 별로 스레드가 할당되고 서블릿 컨테이너 안에서 실행된다. 애플리케이션에서 처리하지 못한 예외는 어디로 가는걸까?

1
WAS(여가까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외 발생)

WAS는 예외를 받으면 상태코드 500으로 처리한다.

response.sendError(HTTP 상태 코드, 오류 메시지)

이 방식은 당장 예외가 발생한 것은 아니지만 서블릿 컨테이너에게 오류가 발생했음을 알린다.

이 메서드를 호출하면 response 객체 안에 호출 기록을 남겨두고 WAS가 이를 확인하고 예외로 인식한다.

서블릿 예외 처리 - 오류 화면 제공

웹서버 팩토리커스터마이저를 통해 상태코드에 따라 주소를 매핑하고, WAS는 상태 코드가 응답하면 매핑된 주소로 진짜 요청을 다시 날린다! WAS가 날린 요청을 처리하기 위해 컨트롤러를 통해 예외 처리된 화면을 보여 줄 수 있다.

여기서 중요한 점은 WAS가 다시 요청을 날릴 때 필터 → 서블릿 → 인터셉터 를 다 거친다는 사실이다.
WAS가 다시 요청을 보낼 때는 요청 객체에 예외 관련 정보를 담아서 요청을 보낸다.

필터

서블릿은 DispatcherType 을 통해 지금 보낸 요청이 고객이 요청한 것인지 WAS가 에러로 인한 내부 호출한 내용인지 파악하는데 도움을 준다. 그래서 필터가 인증 인가 같은 로직을 예외 처리를 위한 요청에서 적용하지 않고 넘길 수 있게 된다. (필터의 기본 디스패처타입이 REQUEST라서 적용되지 않는다. 만약 예외 처리 요청을 위한 필터를 만들려면 디스패처 타입을 ERROR로 설정해주면 된다.)

인터셉터

예외가 발생하는 요청인 경우, postHandle 메서드가 작동하지 않고 afterCompletion 만 작동한다. 하지만 WAS가 예외 처리를 위해 내부 요청을 보내는 경우는 해당 요청 자체는 예외가 발생하지 않으므로 postHandle도 잘 작동한다.

스프링 부트의 예외 처리

서블릿은 귀찮게 경로를 정해주고 그 경로에 맞는 컨트롤러를 만들어줘야 했다.

스프링 부트는 /error 를 기본으로 에러 페이지를 설정한다. 그리고 BasicErrorController 라는 스프링 컨트롤러를 자동으로 등록한다. 이 일을 ErrorMvcAutoConfiguration 이라는 클래스가 자동으로 등록해준다.

컨트롤러는 resources/tempalte/error 디렉토리에서 5xx.html, 400.html 와 같은 이름의 파일을 상태코드에 매핑해서 뷰를 보여준다. 그리고 모델로 에러 상황과 관련된 데이터를 전달한다.

만약 컨트롤러 로직을 확장하고 싶으면 기존의 BasicErrorController 를 상속해서 오버라이딩하면 된다.

API 예외 처리

서블릿의 API 예외 처리

WAS가 예외를 감지하고 내부에서 다시 보낸 요청을 처리하는 컨트롤러에서 HTTP header의 accept를 json으로 해주고 ResponseEntity 를 반환하도록 하면 JSON으로 변환 되서 반환된다.

스프링 부트의 API 예외 처리

스프링 부트는 기본적으로 accept를 어떻게 하냐에 따라 JSON을 알아서 반환해줄 수도, 뷰를 반환해줄 수도 있다.

HandlerExceptionResolver

WAS까지 예외가 전달되면 500으로 처리한다. 이를 바꾸고 싶으면 HandlerExceptionResolver를 사용하자.

HandlerExceptionResolver 가 디스패처 서블릿에 등록되면 디스패처 서블릿에 전달된 예외를 잡아서 response.sendError(status code) 를 호출하고 빈 ModelAndView 을 반환한다. 즉 WAS까지 안가고 요청도 다시 보내지 않는다.

여기서 빈 ModelAndView 를 반환하면 예외가 처리되고 정상 흐름으로 진행되고 ModelAndView 가 지정되어서 반환되면 해당 내용으로 뷰를 렌더링한다. null을 반환하면 다음 리졸버를 찾는다. 만약 맞는 리졸버가 없으면 WAS까지 예외가 전파된다.

스프링이 제공하는 ExceptionResolver

  1. ExceptionHandlerExceptionHandler
    1. 특정 컨트롤러에서 발생한 예외를 @ExceptionHandler 로 처리.
    2. 참고로 파라미터로 받는 예외를 통해 어떤 예외를 처리할지 선언하는 역할도 한다.
  2. ResponseStatusExceptionHandler
    1. @ResponseStatus 가 붙은 경우
    2. ResponseStatusException 이 발생시켜서 이미 존재하는 예외를 처리
  3. DefaultHandlerExceptionResolver
    1. 스프링 내부에서 발생하는 예외를 적절한 상태코드로 처리

1~3까지 해결이 안되면 다음으로 넘기는 방식.

@ControllerAdvice, @RestControllerAdvice

대상이 되는 여러 컨트롤러에 @InitBinder, @ExceptionHandler 기능을 부여하는 역할을 한다.

대상을 지정안하는 경우 모든 컨트롤러에 적용된다.

Share