exception 프로젝트를 생성하자
server.error.whitelabel.enabled=false
application.properties
에서 해당 옵션을 false로 끈뒤 was에 exception이 던져졌을 때 어떻게 처리되어질지 확인해보자.
@Slf4j
@Controller
public class ServletExceptionController {
@GetMapping("/error-ex")
public void errorEx(){
throw new RuntimeException("에러");
}
}
해당 url로 요청하여 무조건 Exception을 던지도록 설계했고
url로 요청시 다음과 같은 tomcat이 제공하는 에러 페이지가 그대로 노출된다.
그리고 없는 url로 요청 했을 경우에도
기존의 해당 에러 페이지가 아닌 톰캣 404 에러 페이지가 노출된다.
@Slf4j
@Controller
public class ServletExceptionController {
...
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(404, "404 error!");
}
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
response.sendError(500, "500 error!");
}
}
spring에서 exception 에러 외에 response.sendError() 메서드를 통해서 에러를 서블릿 컨테이너
에 전달할 수 있다.
위 url로 테스트하면 톰캣 에러 페이지를 그대로 확인할 수 있다.
exception와 sendError의 차이점은 exception의 경우 모두 500 상태 코드를 가지지만 sendError의 경우 상태 코드를 변경해서 반환할 수 있다.
<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>
예전에는 xml로 다음과 같이 상태코드나 exception을 지정하여 에러 페이지를 지정할 수 있었다.
요즘은 java 코드로도 지정이 가능한데 아래에서 진행해보자.
@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"); //Exception으로 기준을 잡을 경우 해당 exception을 상속한 자식 class들도 모두 포함된다.
//페이지 등록
factory.addErrorPages(errorPage404);
factory.addErrorPages(errorPage500);
factory.addErrorPages(errorPageEx);
}
}
다음과 같이 코드를 작성해주면 끝이다.
이제 정말 잘 작동하는지 확인하기 위해
@Controller
@Slf4j
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest req, HttpServletResponse res){
log.info("error 404");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest req, HttpServletResponse res){
log.info("error 500");
return "error-page/500";
}
}
각 에러를 처리하는 페이지 컨트롤러를 만들고
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>404 오류 화면</h2>
</div>
<div>
<p>오류 화면 입니다.</p>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>500 오류 화면</h2> </div>
<div>
<p>오류 화면 입니다.</p>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
404와 500을 각각 만들어서 에러 페이지를 노출시켜보자.
다음과 같이 우리가 작성한 에러 페이지로 변경된 것을 확인할 수 있다.
서블릿은 Exception이 발생하거나 response.sendError()가 호출되었을 때 에러 페이지를 찾는다.
exception 발생 -> 인터셉터 -> 서블릿 -> 필터 -> Was(exception 전파)
sendError() -> 인터셉터 -> 서블릿 -> 필터 -> Was(sendError 호출 기록 확인)
다음과 같이 에러가 발생하면 was는 올 페이지 정보를 확인한다. 등록되어 있는 에러가 발생했을 경우
was '500 error' 페이지 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> view
http 요청이 온것과 동일하게 서버 내부에서 다시 해당 페이지로 요청이 된다.
//RequestDispatcher 상수로 정의되어 있음
public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
public static final String ERROR_MESSAGE = "javax.servlet.error.message";
public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";
public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";
public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
servlet에 다음과 같은 상수로 정보들을 기록하고 있는데
@Controller
@Slf4j
public class ErrorPageController {
//RequestDispatcher 상수로 정의되어 있음
public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
public static final String ERROR_MESSAGE = "javax.servlet.error.message";
public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";
public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";
public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest req, HttpServletResponse res){
log.info("error 404");
printErrorInfo(req);
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest req, HttpServletResponse res){
log.info("error 500");
printErrorInfo(req);
return "error-page/500";
}
private void printErrorInfo(HttpServletRequest req){
log.info("ERROR_EXCEPTION = {}", req.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_EXCEPTION_TYPE = {}", req.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_MESSAGE = {}", req.getAttribute(ERROR_MESSAGE));
log.info("ERROR_REQUEST_URI = {}", req.getAttribute(ERROR_REQUEST_URI));
log.info("ERROR_SERVLET_NAME = {}", req.getAttribute(ERROR_SERVLET_NAME));
log.info("ERROR_STATUS_CODE = {}", req.getAttribute(ERROR_STATUS_CODE));
log.info("dispatcherType = {}", req.getDispatcherType());
}
}
다음 코드로 요청해서 서블릿에 저장된 정보를 꺼내볼 수 있다.
다음과 같이 에러에 대한 서블릿에서 저장되는 정보들을 확인해 볼수 있다.
예외 처리시 처리 흐름을 보면 최초 고객이 요청할 경우 필터와 인터셉터가 실행되고 예외가 발생되었을 때 또 필터와 인터셉터가 호출이 된다. 물론 중복 호출이 되어도 상관없을 수 있지만 중복 호출이 되면 안되는 필터와 인터셉터가 존재할 수도 있고 인터셉터의 처리 크기가 커진다면 불필요한 동작이 될 수 있다.
그래서 filter는 DispatcherType 옵션을 제공한다. 위에서 dispatcherType = {}
로그에서 error type이 ERROR로 찍혀있는 것을 확인할 수 있다. 해당 타입을 통해 서블릿은 고객이 요청한것인지 서버 내부에서 에러로 해당 페이지를 요청한 것인지 확인할 수 있다.
public enum DispatcherType {
FORWARD,
INCLUDE,
REQUEST,
ASYNC,
ERROR
}
dispatcher type은 다음과 같이 enum으로 정의되어 있으며
REQUEST
클라이언트 요청
ERROR
서버 내부 에러 요청
FORWARD
MVC에서 다른 서블릿이나 JSP를 호출할 때 사용
INCLUDE
다른 서블릿이나 JSP의 결과를 포함할 때
ASYNC
서블릿 비동기 호출
실제 filter를 작성해서 테스트해보자
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString(); try {
log.info("REQUEST [{}][{}][{}]", uuid,
request.getDispatcherType(), requestURI); //dispatcher type 출력
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}][{}]", uuid,
request.getDispatcherType(), requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
filter를 작성하고
@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); //request error type 모두 사용
return filterRegistrationBean;
}
}
filter를 등록해주었다. 이제 실제 로그를 확인해보자
최초 요청 시 에러가 발생하기 전엔 고객에 요청에 의해 REQUEST type으로 요청되며
에러가 발생하여 다시 filter로 요청된 로그들은 ERROR type으로 진행되는 것을 확인할 수 있다.
filter와 마찬가지로 interceptor에서도 동일하게 처리해줄 수 있다.
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
log.info("REQUEST [{}][{}][{}][{}]", uuid, request.getDispatcherType(), requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String)request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, request.getDispatcherType(), requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
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); //request error type 모두 사용
return filterRegistrationBean;
}
}
다음과 같이 interceptor를 등록하고 filter를 제거해 놓았다. interceptor는 excludePathPatterns
메서드를 통해 오류 페이지 경로를 지정해 놓고 interceptor가 해당 url의 요청을 아예 막아두는 것이다.
최초 요청 REQUEST dispatcher type을 출력하지만 이후 error type에 대한 로그는 찍히지 않는 것을 확인할 수 있다. error 로그를 찍어내고 싶다면 excludePathPatterns
에서 "/error-page/**"
부분을 제거하면 찍어낼 수 있다.
지금까지 spring의 servlet을 사용하여 에러 페이지를 처리해봤다. 하지만 spring boot에서 에러 페이지를 관리할 수 있는 기능이 있는데 다음 포스트에서 알아보도록 하자!