서블릿에서는 2가지 방식으로 예외를 처리한다고 한다.
웹 애플리케이션은 사용자 요청 별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
만약 try-catch로 예외를 잡는다면 문제 없지만, 예외를 잡지 못하면 아래와 같이 예외가 전달된다.
WAS(tomcat) <-- 필터 <-- 서블릿(디스패처 서블릿) <-- 인터셉터 <-- 컨트롤러(예외 발생)
일반적으로 웹 애플리케이션은 예외가 발생하면 Http 상태코드 500을 반환한다고 한다.
오류가 발생했을때, HttpServletResponse가 제공하는 sendError라는 메서드를 사용해도 된다.
sendError를 호출한다면 예외는 아니지만, 서블릿 컨테이너에게 오류가 발생했다고 알릴 수 있다.
sendError 메서드도 Exception이 발생했을때 와의 흐름이 동일하다.
WAS(Tomcat에서 sendError 호출 기록 확인) <-- 필터 <-- 서블릿(디스패처 서블릿) <-- 인터셉터 <-- 컨트롤러(sendError 호출)
sendError 메서드를 호출하면 response 에 sendError()가 호출되었는지 그 상태를 저장해두고 서블릿 컨테이너에서는 sendError 호출 기록을 확인한다고 한다.
서블릿은 Exception(예외) 또는 sendError() 메서드가 호출되었을떄 각각의 상황에 맞춘 오류 처리를 가능하도록 제공한다. 스프링부트를 적용하기 전에는 web.xml에 error 페이지를 등록했다고 한다.
<web-app>
<error-page>
<error-code>404</error-code>
<location>/error-page/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error-page/500.html</location>
</error-page>
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/error-page/500.html</location>
</error-page>
</web-app>
스프링부트를 사용하는 경우에는 아랭 ㅘ같이 서블릿 오류 페이지를 등록할 수 있다.
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = 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(errorPage404, errorPage500, errorPageEx);
}
}
WebServerFactoryCustomizer를 구현하는 클래스를 만들어 customize 함수를 오버라이드한다.(스프링에서 정해놓은 규격이므로 암기밖에..)
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
위 코드를 예시로 간단히 분석해보자면 404에러(HttpStatus.NOT_FOUND)가 발생하면 /error-page/404.html을 호출해주도록 지정해놓는다.
이렇게 생성된 페이지들을 factory에 등록해주면 된다.
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
위 코드에서 RuntimeException.class 또는 그 자식타입의 예외까지 모두 포함한다.
Exception이 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을때는 설정된 오류 페이지를 찾는다.
예외 발생 흐름
WAS <-- 필터 <-- 서블릿 <-- 인터셉터 <-- 컨트롤러(예외 발생)
sendError 흐름
WAS(sendError 호출 기록 확인) <-- 필터 <-- 서블릿 <-- 인터셉터 <-- 컨트롤러(response.sendError)
WAS에서는 등록된 오류 페이지 정보를 확인해서 다시 오류 페이지를 요청하게 된다.
에외 발생과 오류 페이지 요청 흐름
1.WAS <-- 필터 <-- 서블릿 <-- 인터셉터 <-- 컨트롤러(예외발생)
2.WAS /error-page/400 다시 요청 --> 필터 --> 서블릿 --> 인터셉터 --> 컨트롤러
앞서 오류가 발생하면 오류 페이지를 출력하기 위해선 WAS 내부에서 다시 한번 오류 페이지로의 호출이 발생한다. 문제는 이 과정에서 필터, 서블릿, 인터셉터가 다시 호출된다는 점이다.
따라서, 필터와 인터셉터가 중복 호출되는것을 막기 위해서는 클라이언트로부터 발생한 정상 요청이닞, 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야한다.
서블릿은 이 구분을 위해서 DispatcherType이라는 추가 정보를 제공한다.
package javax.servlet;
/**
* @since Servlet 3.0
*/
public enum DispatcherType {
FORWARD,
INCLUDE,
REQUEST,
ASYNC,
ERROR
}
실제 코드에서 DispatcherType을 보면 위 코드처럼 몇가지가 존재한다.
@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;
}
필터를 등록할떄 위 코드처럼 setDispatcherTypes를 통해서 어떤 요청을 처리할것인지 지정할 수 있다.
위 코드는 클라이언트의 요청과 오류 페이지의 요청에서도 필터가 호출되도록 지정되었다.
setDispatcherTypes를 지정하지 않으면 기본적으로 REQEUST만 지정된다.
인터셉터는 서블릿이 아닌 스프링의 기술이기 떄문에 DispatcherTypes는 존재하지 않는다.
다만, 강력한 exludePathPatterns에서 지정이 가능하다.
@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/**");//오류 페이지 경로
}
}
앞선 코드들에서는 예외 처리 페이지를 만들기 위해서 여러 가지 코드를 추가해야했다.
스프링 부트는 위 과정을 기본적으로 제공한다.
따라서 개발자는 오류 페이지만 등록해주면 된다.
BasicErrorController의 처리 순서는 아래와 같다.
해당 포스팅은 아래의 강의를 공부하여 정리한 내용입니다.
김영한님의 SpringMVC2-예외처리