서블릿의 예외 처리 방식
- Exception
- response.sendError(HTTP 상태 코드, 오류 페이지)
서블릿은 Exception이 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을 때 설정된 오류 페이지를 찾는다.
예외 발생 흐름
WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
sendError 흐름
WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(response.sendError())
WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다.
'new ErrorPage(RuntimeException.class, "error-page/500") -> RuntimeException이 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인한다. RuntimeException이 일어날 경우에 /error-page/500이 지정되어 있다. 그래서 WAS는 오류페이지를 출력하기 위 /error-page/500을 다시 요청한다. 이전에 알게된 요청 흐름과 같이 컨트롤러에게 '/error-page/500'을 요청하여 최종적으로 View를 호출하게 된다.
그런데 이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.
추가적으로 WAS는 오류에 대한 정보를 request의 attribute에 추가하여 넘겨준다.
오류가 나서 다시 오류페이지를 호출할 때 필터, 서블릿, 인터셉터가 다시 호출되게 된다. 그러면 로그인 인증체크 같은 것을 두번 시행하게 되는 것이다. 이는 매우 비효율적이다.
그래서 클라이언트로부터 정상 요청인지, 오류 페이지를 호출하기 위한 요청인지 구분할 수 있어야 한다. 이를 위해 서블릿은 'DispatcherType'이라는 추가 정보를 제공한다.
DispatcherType
필터는 이런 경우를 위해 'DispatcherTypes'라는 옵션을 제공한다. 위에서 WAS가 오류에 대한 정보를 request의 attribute에 담는다고 했는데, 여기서
log.info("dispatchType = {}", request.getDispatcherType())
이 코드로 DispatcherType을 확인할 수 있었다. 출력으로는 ERROR가 나왔다.
고객이 처음 요청하면 REQUEST로 나오는데 이를 통해 실제 고객의 요청인지 오류 페이지 요청인지 알 수 있다.
DispatcherType
- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
- FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때
- RequestDispatcher.forward(request, response);
- INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
- RequestDispatcher.include(request, response);
- ASYNC : 서블릿 비동기 호출
서블릿 필터에서 적용하기
@Configuration
public class WebConfig implements WebMvcConfigurer {
@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;
}
}
LogFiler는 이전에 작성한 포스팅처럼 만들고,
'filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);'을 통해 어떤 경우에 필터를 작동하게 할지 지정할 수 있다.
에러의 경우에만 작동하게 하려면 ' filterRegistrationBean.setDispatcherTypes( DispatcherType.ERROR);'로 하면 된다.
스프링 인터셉터에서 적용하기
필터에서 적용하려면 'WebServerCostomizer'를 만들고, 예외 종류에 따라서 ErrorPage를 추가하고, 예외 처리용 'ErrorPageController'를 만들었다.
스프링 부트는 이런 과정을 모두 기본으로 제공한다.
- ErrorPage를 자동으로 등록한다. 이때 /error라는 경로로 기본 오류 페이지를 설정한다.
- new ErrorPage("/error"), 상태 코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용된다.
- 서블릿 밖으로 예외가 발생하거나, response.sendError(...)가 호출되면 모든 오류는 /error를 호출하게 된다.
- BasicErrorController라는 스프링 컨트롤러를 자동으로 등록한다.
- ErrorPage에서 등록한 /error를 매핑해서 처리하는 컨트롤러다.
따라서 오류가 발생하면 기본 오류 페이지 ./error를 요청한다.
개발자는 오류 페이지만 등록하면 된다. BasicErrorController가 제공하는 룰과 우선 순위에 따라 등록하면 된다.
//오류 페이지 등록
@Slf4j
@Controller
public class ServletExceptionController {
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("예외 발생!");
}
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(404, "404 오류!");
}
@GetMapping("/error-400")
public void error400(HttpServletResponse response) throws IOException {
response.sendError(400, "400 오류!");
}
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
response.sendError(500);
}
}
이렇게 오류 페이지를 등록해주고,
뷰 템플릿인 경우에는 'resources/templates/error/'에 상태코드에 맞게 html파일 생성
정적 리소스인 경우에는 'resources/static/error/'에 상태코드에 맞게 html파일 생성
적용 대상이 없을 때는 error.html이 불려와진다.
html파일이 불려와지는 것은 자세하게 적힌 상태코드가 4xx,5xx보다 우선순위가 높다.
다 구현되어 있기 때문에 맞는 위치에 파일만 만들어도 동작한다.
BasicErrorController는 에러에 대한 정보를 model에 담아 뷰에 전달한다. 뷰 템플릿은 이 값을 활용하여 출력할 수 있다.
하지만 오류 정보를 고객에게 요청하는 것은 좋지 않다. 그래서 오류 정보를 포함시킬지 말지 여부를 정할 수 있다.
사용자에게는 간단한 오류 메시지만 보여주는 것이 좋다.
<li>오류 정보</li>
<ul>
<li th:text="|timestamp: ${timestamp}|"></li>
<li th:text="|path: ${path}|"></li>
<li th:text="|status: ${status}|"></li>
<li th:text="|message: ${message}|"></li>
<li th:text="|error: ${error}|"></li>
<li th:text="|exception: ${exception}|"></li>
<li th:text="|errors: ${errors}|"></li>
<li th:text="|trace: ${trace}|"></li>
</ul>
</li>