스프링이 아닌 순수 서블릿 컨테이너는 예외를 다음 2가지 방식으로 처리한다.
자바를 직접 실행하는 경우 main 메서드를 실행하여 처음 main이라는 이름의 쓰레드가 실행되고 코드 흐름이 전개된다. 이때 실행 도중 호출된 공간에서 잡지 못한 예외들은 main 메서드까지 던져질 수 있고 main메서드가 최후의 보루이다. main에서도 던져져야 한다면 예외 정보를 남기고 main 쓰레드는 종료된다.
웹 애플리케이션의 경우 사용자 요청별로 별도의 쓰레드가 할당되며 서블릿 컨테이너 안에서 실행된다. 만약 에플리케이션에서 예외를 잡지 못하고 서블릿 밖으로 까지 예외가 전달되면 WAS까지 예외가 전달될 것이다.
우리는 이때동안 다음과 같은 스프링부트가 기본적으로 제공하는 whitelable 에러 페이지를 경험했다.

이를 application.properties에서 server.error.whitelabel.enabled=false와 같이 주어 적용을 해제할 수 있다. 이렇게 whitelabel을 끄면 tomcat이 기본으로 제공하는 오류화면을 볼 수 있는데 예외(Exception)이 터질 경우 500 에러를 화면에 띄우고 맞지 않은 URL로 접근시 404에러 페이지가 뜬다.
오류 발생시 HttpServletResponse 객체에 sendError()메서드를 사용할 수 있다. 이는 응답으로 전해질 response객체에 Error정보를 싣는다.(에러코드, 에러메세지) 그 후 서블릿 컨테이너에서 서버 애플리케이션에서 전달해준 response를 받고 Error가 포함되어있는 지 확인한다.
컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS(sendError 확인)
위의 흐름을 보면 결국 WAS에서 최종확인을 진행하므로 어느 시점에든 response.sendError()로 하여금 에러정보를 담아주면 응답시점에 WAS에서는 sendError()를 통해 에러정보가 들어있는 것을 확인하여 에러 정보가 존재한다면 에러페이지를 랜더링해준다.
현재 우리는 스프링 부트를 통해 서블릿 컨테이너를 띄우기 때문에 스프링 부트가 제공하는 기능을 활용하여 서블릿 오류 페이지를 등록할 수 있다.
에러 페이지 랜더링을 위해 해야할 작업은 다음과 같다.
@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);
}
}
위의 코드는 톰켓(WAS)의 기능을 커스터마이징한다고 보면 된다. ErrorPage라는 객체를 이용하여 ConfigurableWebServerFactory에 ErrorPage의 흐름을 등록한다. 흐름을 등록한다는 것은 만약 ErrorPage의 첫번 째 인자의 형태가 WAS에서 포착된다면 두번 째 인자인 URI로 요청로직을 내부적으로 다시 처리하는 것이다.
해당 URI에 대한 에러 컨트롤러를 설계하고 그 컨트롤러에서 에러 템플릿을 띄우도록 처리하면 된다.
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 404");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 500");
return "error-page/500";
}

Browser -> WAS -> 컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS(Error 확인)
요청의 흐름에서 컨트롤러, 인터셉터, 서블릿 또는 필터에서 예외가 발생하거나 HttpServletResponse에 4xx/5xx 상태 코드가 설정되고 애플리케이션에서 처리되지 않은 경우,
WAS는 Spring Boot에서 등록된 ErrorPage 설정을 참고하여 상태 코드 또는 예외 타입에 매핑된 에러 경로로 요청을 포워딩한다.
이때 Spring은 해당 경로에 매핑된 에러 처리 컨트롤러 또는 뷰를 호출하여 응답을 구성한다.
WAS까지 온 에러 상태를 담은 응답은 다시 WAS로 하여금 매핑된 에러 컨트롤러로 DispatcherServlet을 통해 요청-응답 프로세스가 다시 동작한다.
WAS -> Error컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS -> Browser
의 과정이 한번 더 수행된다.
처음 요청 흐름은 브라우저가 보낸 요청이지만 WAS로부터의 두번째(에러 흐름으로 인한 랜더링 요청)는 서버 내부에서 다시 에러 관련 컨트롤러로 내부요청 로직이 실행되는 것이다. 당연히 에러페이지를 받은 브라우저는 서버가 이렇게 두번 왕복하는 것을 알지는 못할 것이다.
또한 WAS는 Error로직(두번째 왕복)시 몇 가지 에러 정보를 더 담아서 컨트롤러에 전달해준다.
@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";
...
private void printErrorInfo(HttpServletRequest request) {
log.info("ERROR_EXCEPTION: ex=", request.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_EXCEPTION_TYPE: {}",
request.getAttribute(ERROR_EXCEPTION_TYPE));
log.info("ERROR_MESSAGE: {}", request.getAttribute(ERROR_MESSAGE)); //ex의 경우 NestedServletException 스프링이 한번 감싸서 반환
log.info("ERROR_REQUEST_URI: {}",
request.getAttribute(ERROR_REQUEST_URI));
log.info("ERROR_SERVLET_NAME: {}",
request.getAttribute(ERROR_SERVLET_NAME));
log.info("ERROR_STATUS_CODE: {}",
request.getAttribute(ERROR_STATUS_CODE));
log.info("dispatchType={}", request.getDispatcherType());
}
}
위와 같이 에러에 관련한 정보들을 컨트롤러에서 활용가능하도록 WAS의 기능을 알고가자.
아래의 두 프로세스는 자세한 전체 로직을 표현해보았다.
Browser -> WAS -> 필터 -> 서블릿 -> 인터셉터 시작 콜백 -> 비즈니스 로직 컨트롤러 -> 인터셉터 종료 콜백 -> 서블릿 -> 필터 -> WAS(Error 확인)WAS(Error 확인) -> 필터 -> 서블릿 -> 인터셉터 시작 콜백 -> Error컨트롤러 -> 인터셉터 종료 콜백 -> 서블릿 -> 필터 -> WAS -> Browser두 번째 왕복 흐름에 대해서 필터와 인터셉터를 거칠 필요가 있을까? 단순히 에러 정보를 통해 에러 페이지 랜더링을 위함이니
2. WAS(Error 확인) -> 필터 -> 서블릿 -> 인터셉터 -> Error컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS -> Browser 2번 흐름을 다음과 같이 바꿀 수 있다면 좋을 것이다.
수정 후: WAS(Error 확인) -> 서블릿 -> Error컨트롤러 -> 서블릿 -> WAS -> Browser
필터를 위의 1번 흐름에만 적용하기 위해서는 필터를 설정하는 WebConfig에서 FilterRegistrationBean으로 다룰 수 있다.
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1);
filterFilterRegistrationBean.addUrlPatterns("/*");
//추가
filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return filterFilterRegistrationBean;
}
DispatcherType은 위의 WAS가 에러 로직때 뿌려주던 에러 정보중 하나였다.
이를 출력해보면 Error라고 뜨는데, 이는 요청 흐름의 구분을 하기위해 만들어졌다.
여러가지가 존재하지만 DispatcherType.REQUEST, DispatcherType.ERROR로 요청 흐름과 에러 요청 흐름을 구분가능하다. 즉 필터가 적용될 디스패처 타입을 전달해주면 된다.
잔뜩 말했지만 사실 기존 처럼 아무것도 적지 않아도 default는 DispatcherType.REQUEST만 적용되기에 필터는 REQUEST흐름에만 적용된다.
인터셉터는 간단하다 .excludePathPatterns("/css/**", "/*.ico", "/error", "/error-page/**")처럼 제외할 패턴에 error-page를 모두 전달해주면 된다.
위에서는 에러 페이지가 어떻게 랜더링되는 지 과정을 알기위해 모두 구현해보았지만 결론은 스프링 부트가 알아서 제공해주는 기능에 숟가락만 얹히면 된다.
스프링 부트는 기본적으로 에러 컨트롤러와 에러 설정을 자동으로 등록시켜준다.
개발자는 오류 템플릿만 룰에 따라 등록하면 된다.
뷰 템플릿이 정적 리소스보다 우선순위가 높고, 404, 500처럼 구체적인 것이 5xx처럼 덜 구체적인 것 보다 우선순위가 높다.5xx, 4xx 라고 하면 500대, 400대 오류를 처리해준다.
정확히 말하자면 resources/templates/error/ 이 경로를 세팅하고 500Error.html 또는 500Exception.html이렇게 만들지 않고, 정확히 500.html로 만들어 위 경로에 이 에러 페이지 html 파일을 생성하면 이 에러 페이지를 스프링이 알아서 랜더링해준다는 것이다.
파일 이름을 500.html, 404.html로 하지 않는다면 스프링은 디폴트로 제공 및 적용된 화이트 라벨 에러 페이지를 랜더링 할 것이다.
템플릿을 활용하지 않고 오로지 json으로만 응답을 준다고 가정해보자.
에러 페이지또한 우리는 에러에 관련한 json 데이터만 주어야하고 페이지 랜더링은 절!때! 스프링이 관여해서는 안된다.
# application.yml
server:
error:
whitelabel:
enabled: false
요청-응답 흐름에서 발생한 예외때문에 응답에 에러코드가 셋팅되었을때 WAS에서 다시 스프링단으로 에러 요청 흐름이 진행되는 것을 막아야한다.
위 설정 코드로 하여금 whitelabel 에러 페이지 랜더링을 막는다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception ex, HttpServletRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("path", request.getRequestURI());
body.put("message", ex.getMessage());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
화이트 라벨 에러 페이지는 모든 예외에 대해 대응되었다. 그러한 역할을 REST 방식에서도 만들어주어야 하기 때문에 위와 같이 코딩하여 우선 Exception 이하 예외들을 모두 받아 json으로 형성될 수 있는 에러응답을 규정해주어야 한다.
다른 에러들을 핸들러에서 잡으며 범위를 좁혀나가는 식으로 개발해야할 것이다.
예외에 대해서는 위와 같지만,
엔드포인트 자체를 잘못 요청(이것은 Exception으로 캐치할 수 없다.)하거나 필터에서 걸리는 것들에 대해서는 여전히 WAS가 예외를 다루게 된다.
결론적으로는 우리는 뷰 선택 우선순위에 따라 랜더링될 에러 페이지만 만들어주면 된다. 하지만 이러한 에러 페이지가 어떤 과정에 따라 랜더링되는 지 잘 이해할 필요가 있고 어떠한 아이디어가 적용되었는지 추가적으로 활용할 수 있는 부분을 인지(ex. 필터나 인터셉터를 적용해제 하거나 에러용 필터 인터셉터를 적용하는 등의 방법)하는 것도 매우 중요하다.