Exception Handling & Error pages

Servlet Exception Handling

Exception class

웹 어플리케이션에서는 사용자의 요청에 따라 별도의 쓰레드를 할당하고, 이를 서블릿 컨테이너 안에서 실행하게 된다. 이때, 예외가 발생하게 되면 try-catch를 이용해서 어플리케이션 레벨에서 처리할 수 있지만, 처리하지 못하고 서블릿 밖으로 예외가 전달되면 아래와 같이 WAS 서버까지 예외가 전달된다.

WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)

Error Controller

@Slf4j
@Controller
public class ServletExController {
    @GetMapping("/error-ex")
    public void errorEx() {
    throw new RuntimeException("예외 발생!");
    }   
}

위와 같이 Exception 에러를 발생시키고, WAS 서버까지 예외를 처리하지 않으면 WAS 서버는 기본 오류 화면을 출력하게 된다.

HTTP Status 500 – Internal Server Error

response.sendError()

HttpServletResponse를 이용해서 서블릿 컨테이너에 예외가 발생했음을 전달하게 된다.

Error Controller

@GetMapping("/error-400")
public void error400(HttpServletResponse response) throws IOException {
    response.sendError(400);
}

@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,"404 Error!");
}

서블릿 컨테이너는 클라이언트에 response를 보내기 전에 sendError()가 호출되었는지 여부를 확인한 다음, 호출되었으면 해당 오류 코드에 맞는 오류 페이지를 띄우게 된다.

HTTP Status 404 – Bad Request
HTTP Status 500 – Internal Server Error

오류 페이지 등록

위와 같이 WAS 서버와 서블릿 컨테이너가 제공해주는 기본 오류 페이지는 고객에게 의미 있는 정보를 전달하지 않는다. 그래서, 개발자는 각각의 오류 코드에 대해 오류화면을 설정해줘야한다.

WebServerCustomizer

@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);
    }
}

위의 과정을 통해 오류 코드에 대한 오류 페이지를 등록해주는 작업을 진행한다. 엄밀히 말하면, 위의 오류 코드에 대해서, 등록된 경로로 서버 내부적으로 요청을 수행하게 된다.

ErrorPage Controller

@Controller
public class ErrorPageController {
    @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";
    }
}

위의 WebServerCustomizer에 의해서 오류 코드는 새로운 경로로 요청을 진행하게 되고, 이에 대한 Controller을 설정할 필요가 있다. 해당 컨트롤러에 대해서는 예외에 대해서 적합한 View를 호출하고 있다.

error-page/404.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>404 오류 화면</h2>
    </div>
    <div>
        <p>오류 화면 입니다.</p>
    </div>
    <hr class="my-4">
</div> <!-- /container -->
</body>
</html>

error-page/500.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>

이렇게 오류 페이지를 등록하게 되면, 오류가 발생하게 되었을 때, 직접 등록한 오류 페이지가 고객에게 보여지게 된다.

오류 페이지 요청 흐름

1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/errorpage/
500) -> View

컨트롤러에서 발생한 예외에 대해서, WAS 서버까지 전파가 되며, WAS 서버에서는 오류 코드에 맞는 오류 페이지를 호출하게 되고, 고객에게 사용자 정의 오류 페이지가 출력된다.

오류 정보

@Slf4j
@Controller
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 request, HttpServletResponse response) {
        log.info("errorPage 404");
        printErrorInfo(request);
        return "error-page/404";
    }

    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
        log.info("errorPage 500");
        printErrorInfo(request);
        return "error-page/500";
    }

    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)); //
        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("dispatcher type:{}", request.getDispatcherType());
    }
}

WAS에 오류가 전달되면, WAS는 새로운 request를 전달하는 것 뿐만 아니라, 위와 같이 오류 정보를 같이 전달하게 된다.

Attributes Descriptions
ERROR_EXCEPTION 예외
ERROR_EXCEPTION_TYPE 예외 타입
ERROR_MESSAGE 오류 메세지
ERROR_REQUEST_URI 클라이언트 요청 URL
ERROR_SERVLET_NAME 오류가 발생한 서블릿 이름
ERROR_STATUS_CODE HTTP 상태 코드

Duplicated Filters?

오류 페이지 동작 흐름

1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/errorpage/
500) -> View

동작 흐름을 보게 되면, 필터가 2번 중복되는 것을 확인할 수 있다. 이렇게 되면, 비효율적인 호출이 추가되는 것이므로, 필터가 중복 실행되는 것을 방지하기 위해, Servlet에서는 DispatcherType를 관리하게 된다.

public enum DispatcherType {
    FORWARD, //서블릿에서 다른 서블릿,JSP를 호출하는 경우
    INCLUDE,//서블릿에서 다른 서블릿,JSP의 결과를 포함하는 경우
    REQUEST,//실제 고객의 요청
    ASYNC,//서블릿 비동기 호출
    ERROR//서버 내부적으로 오류 페이지 처리
}

서블릿에 어떤 방식으로 요청되었는지에 대한 정보를 가지고 있다. 그래서, 실제 고객이 요청한 것인지, 서버가 내부적으로 오류페이지를 요청하는 것인지를 판단할 수 있다.

그렇기 때문에, 필터에서 DispatcherType이 ERROR인 것에 대해서는 필터가 수행되지 않도록 지정하면 된다.

WebConfig

@Configuration
public class WebConfig{
    @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;
    }
}

filter을 등록할 때, setDispatcherTypes를 이용해서 해당 필터를 적용할 Dispatcher Type을 지정할 수 있다.

기본적으로, REQUEST만 적용되어 있는데, 만약, 오류 페이지 처리 과정에 대해서, Filter을 처리하고자 하는 경우 위와 같이 DispathcerType.ERROR을 등록해줘야한다.

Duplicated Interceptors?

Filter는 위와 같이 setDispatcherType를 이용해서, Filter을 적용할 Dispatcher Type을 지정할 수 있다. Interceptor의 경우, Dispatcher Type 대신에 URL 패턴을 활용한다.

WebConfig

@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/**" //오류 페이지 경로
        );
    }
}

위의 오류 페이지 처리 과정을 살펴보면, 모든 예외 처리는 /error-pages/ 경로를 포함하고 있기 때문에, 해당 url 패턴에 대해서는 인터셉터를 적용하지 않도록 지정한다.

Spring Boot Exception Handling

Spring Boot를 활용하면, 위와 같이 복잡한 오류 페이지 등록 작업, Error PageController을 생성할 필요없이, Spring 내부적으로 자동으로 이를 지원한다.

Spring은 BasicErrorController을 등록하며, 내부적으로 /error를 통해 ErrorPage를 등록해놓았다. 그래서 개발자는 정해진 경로에 오류페이지만 생성하기만 하면 된다.

오류 페이지를 찾는 과정

  1. 뷰 템플릿
    • resources/templates/error/500.html
    • resources/templates/error/5xx.html
  2. 정적 리소스( static , public )
    • resources/static/error/400.html
    • resources/static/error/404.html
    • resources/static/error/4xx.html
  3. 적용 대상이 없을 때 뷰 이름( error )
    • resources/templates/error.html

위와 같이 message 코드를 찾는 과정과 유사하게 범용적인 이름 부터 상세한 이름 까지 단계적으로 찾게 된다.

그래서 개발자는 위의 경로에 오류코드에 대한 오류 페이지를 생성해서 넣어놓기만 하면, Spring에서 내부적으로 자동으로 오류 페이지를 호출하게 된다.

Error Info

BasicErrorController는 model을 이용해서 오류와 관련된 정보를 전달하게 된다.

<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>

위의 오류 정보는 시스템 내부 구성요소를 파악할 수 있는 수단으로 활용될 수 있기 때문에 외부에 노출하지 않는 것이 좋다.

그래서, 해당 정보들에 대해 노출 여부를 application.properties에서 지정할 수 있다.

application.properties

server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param

always: 항상 허용 on-param: 파라미터가 있을 때 허용 never: 사용하지 않음

on-param으로 설정되었을때는, 아래와 같이 경로에 파라미터를 추가해서 요청 했을 때만, 해당 오류 정보를 노출하게 된다.

http://localhost:8080/error-ex?message=&errors&trace=

References

link: inflearn

link:springmvc

댓글남기기