예제 파일 링크 바로가기
https://github.com/gahyeonkwon/dev_study/tree/main/exceptiondemo/src/main
Exception과 logging 은 개발할 때마다 가장 고심하는 부분이다. spring 프레임워크에서 제공하는 Exception 처리는 사용하기 편리하지만 실제로 웹 애플리케이션이 어떻게 Exception을 전달하는지에 대해 정리 필요성을 느껴 글을 작성하게 되었다.
Exception 이 전달 되는 순서
Exception 이 발생하면
Controller -> Interceptor -> Servlet -> Filter -> Was 순으로 전달이 된다.
일반적으로 Exception 은 Try - Catch 구문을 통해 Servlet 내에서 처리되지만, 처리되지 않았을 경우 Was까지 전달 되게 된다.
예제 코드를 보자.
@GetMapping("/exception-404")
public void test2(HttpServletResponse response) throws IOException {
response.sendError(404,"404error");
}
reponse.sendError( 상태코드, 오류메시지 ) 를 발생시켜 was에게 404 에러가 발생함을 알리는 Controller를 하나 생성했다.
클라이언트가 서버에 어떤 서비스를 요청한 상황인데, 404 에러가 발생했다고 가정해보자. 여기서 /excpetion-404 는 클라이언트의 요청에 해당한다.
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("error page - 404");
return "error-page/404";
}
/error-page/404는 오류 페이지에 대한 Controller 다. was에 에러가 전달되었을 때 내부적으로 호출하는 에러페이지 요청에 해당한다.
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage400 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage400, errorPage500, errorPageEx);
}
}
was에서 전달받은 HttpStatus에 따라 에러 페이지를 매핑하는 컴포넌트다. 위 예제 코드에서 404 에러를 responseError를 통해 was로 전달했기 때문에, 서버에서는 /error-page/404를 호출한다.
Filter에서 제공하는 DispatcherType
위 코드에서 확인한 것처럼 Was는 컨트롤러에서 에러를 받는 순간 어떤 에러가 왔는지 확인하고, 오류 페이지를 출력하기 위해 '오류 페이지'를 호출하는 링크를 다시 한번 더 호출하게 된다. 즉 Controller -> Interceptor -> Servlet -> Filter -> Was 가 클라이언트의 요청에 의해서 한번, was에 의해서 한번 더 발생하는 셈이다. 이렇게 되면 Interceptor와 Servlet, Filter에서 작성해 둔 서비스 로직이 의도치 않게 두 번 발생한다.
그렇기 때문에 Was 가 호출하는 요청이 클라이언트의 요청으로부터 발생한 에러가 Filter/Interceptor을 호출하는 것인지 내부에서 오류페이지를 출력하기 위한 Filter/Interceptor의 호출인지 구분하는 것이 필요한데 , 이런 것을 위해 필터에서 제공하는 것이 DispatcherType 이다.
- DistpacherType 은 두 가지로 나뉜다.
- DispatcherTpye = Request ( default )
- DispatcherType = Error
Request는 default 값으로, 일반적인 클라이언트 요청을 의미하며 error는 was에서 발생하는 에러페이지 요청에 해당한다.
Interceptor에서는 DispatcherType을 제공하지는 않지만 excludePathPatterns를 통해 Interceptor을 실행하지 않게 특정 경로를 설정할 수 있다.
코드 예시는 다음과 같다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");//오류 페이지 경로
}
// @Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterRegistrationBean;
}
}
BasicErrorController
서블릿에서 제공하는 에러 처리의 기본적인 흐름은 위에서 서술한 것과 동일하다. Spring에서는 이렇게 DispatcherType을 지정하는 것들을 어느 정도 자동화 해서 제공하는데 이것이 BasicErrorController 다.
정해진 규칙에 따라 프로젝트를 구성하면, 편하게 에러 페이지 매핑을 사용할 수 있다. 에러 페이지 매핑을 커스터마이징 하려면 스프링에서 기본적으로 제공하는 에러 페이지 사용을 false로 변경해줘야 한다.
변경 방법은 다음과 같다.
- application.properties
server.error.whitelabel.enabled=false
- 규칙
- view : templates 하위에 error 폴더를 만들고 4xx.html / 400.html 등으로 생성한다. 4xx.html, 400.html 이 모두 다 있다면 400.html 이 호출된다. ( 호출 순서는 상세할수록 우선순위를 가진다. )