예외가 발생하면 다음과 같은 동작으로 처리된다.(WAS = 서블릿 컨테이너)
요청 -> WAS -> 필터 -> 인터셉터 -> 컨트롤러(오류 발생!) -> 인터셉터 -> 필터
-> WAS에서 오류 감지 -> WAS에서 에러 처리 재요청
-> 필터 -> 인터셉터 -> 컨트롤러 -> 인터셉터 -> 필터 -> WAS -> 응답
중요한 것은 클라이언트는 이 과정을 모르고 그냥 에러 페이지를 응답 받는 것이다.
HttpServletResponse
에서 제공하는 sendError()
를 활용해서 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.
이것을 호출한다고 당장 예외가 발생하는 것은 아니다.
정상적으로 동작하지만 WAS에 도달해서 WAS가 오류를 감지하고 오류를 처리한다.
sendError()
와 필터, 인터셉터를 이용해서 에러 발생시 어떤 흐름으로 동작되는지 확인해보자.
참고로 throw new Exception
으로 예외를 발생시키면 WAS에 500을 전달한다.
간단한 오류를 발생시키는 컨트롤러를 작성해보자.
@Controller
public class ServletExController {
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "찾지 못함");
}
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.addPathPatterns("/**")
}
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LogFilter());
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
로그를 찍는 필터와 인터셉터를 미리 추가해두었다.
간단한 로그를 찍는 필터와 인터셉터이기 때문에 코드는 첨부하지 않겠다.
/error-404
로 요청을 보내고 로그를 확인해보자.
그런데 이상한 부분이 있다. /error-404
요청을 보내고 WAS에서 /error
라는 URI로 재요청을 보내는데 필터를 거치지 않았다.
서버 내부에서 오류 페이지를 호출한다고 해서 해당 필터나 인터셉트가 한 번 더 호출되는 것은 매우 비효율적이다. 서블릿은 이러한 문제를 해결하기 위해서 DispatcherType
을 제공한다.
다시 필터를 등록하는 코드로 돌아가보자.
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LogFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1);
return registrationBean;
}
해당 코드처럼 아무런 설정도 없으면 기본값은 DispatcherType.REQUEST
이다.
만약 필터를 오류페이지를 요청할 때도 나오게 하고 싶다면 다음과 같이 바꿀 수 있다.
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LogFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1);
registrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return registrationBean;
}
하지만 따로 에러를 처리하기 위한 필터가 아니라면 기본값으로 설정하는 것이 제일 좋다.
이제 다시 로그를 확인해보자.
/error
요청에 대한 필터도 정상적으로 나오는 것을 확인할 수 있다.
인터셉터의 경우 간단하다. excludePathPatterns
에 해당 URI를 추가해 주면 된다. 그러면 해당 URI를 요청하면 인터셉터를 거치지 않는다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/error") // 추가
}
로그를 보면 예외 발생 후에 /error
페이지를 다시 요청하는 것을 확인할 수 있다.
/error
페이지는 스프링 부트에서 제공해주는 BasicErrorController
에서 처리한다.
BasicErrorController
는 /error
폴더 아래 위치한 상태코드를 출력해준다.
예를들어 다음과 같은 것들이 출력되는 것이다.
/error/500 - 500 에러일 때 출력
/error/400 - 400 에러일 때 출력
/error/5xx - 500번대 에러일 때 출력
/error/4xx - 400번대 에러일 때 출력
...
현재 프로젝트에서 Thymeleaf를 사용하고 있기 때문에 templates 밑에 error 폴더를 만들고 html을 작성했다.
이후 404 에러를 발생시켜보면 다음과 같이 4xx.html
을 브라우저가 렌더링한 것을 확인할 수 있다.
스프링 부트에서 error가 발생하면 로직을 타는 ErrorHandler
를 만들어보자.
public class ErrorHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
log.info("ErrorHandler 동작");
return null;
}
}
HandlerExceptionResolver
를 구현하면 되는데 사실 스프링 부트에서는 사실 기본으로 제공하는 HandlerExceptionResolver
들이 있다. 하지만 로그를 찍어서 흐름을 보기위해 만들었다.
그리고 해당 extendHandlerExceptionResolvers
메서드를 통해 핸들러를 등록했다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new ErrorHandler());
}
}
다음 로그는 throw new 사용자정의예외();
를 하는 컨트롤러에 요청을 보냈을 때의 로그이다. 로그 중간에 Exception에 대한 Stack Trace가 있어서 생략했다.
예외가 발생했기 때문에 인터셉터의 postHandle
은 없는 것을 확인할 수 있다.
ErrorHandler
는 인터셉터의 preHandler
와 afterCompletion
에서 동작했다.
해당 ErrorHandler
에서 Exception을 적절히 처리하면 WAS에서 다시 재요청하는 것을 막을 수 있다.
하지만 스프링 부트에서는 기본으로 다음 HandlerExceptionResolver
를 제공하고 있다.
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
ExceptionHandlerExceptionResolver
ResponseStatusException
, DefaultHandlerExceptionResolver
는 중간에서 결국 적절한 sendError
를 보낸다. 그래서 WAS에서 재요청해서 BasicErrorController
에서 처리한다.
만약 재요청하지 않고 바로 보내고 싶다면 ExceptionHandlerExceptionResolver
에 대해 찾아보자.