Spring MVC 2편 예외처리와 오류페이지
순수 서블릿 컨테이너의 경우 2가지 방식으로 예외처리를 한다.
Exception , response.sendError(HTTP상태코드 , 오류메시지)
위 2가지 경우가 발생했을때 에러임을 인지하고 에러 처리 로직이 동작하는것이다.
Exception
1. 자바에서 직접실행
자바의 메인 메서드를 직접 실행하는 경우 main 이라는 이름의 쓰레드가 실행된다.
실행 도중에 예외를 잡지 못하고 처음 실행한 main() 메서드를 넘어서 예외가 던져지면 , 예외 정보(stack trace)를 남기고 해당 쓰레드는 종료된다.
2. 웹앱에서 실행
웹 앱은 사용차 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
앱에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리하면 아무 문제가 없지만 만약에 앱에서 잡지 못하 고, 서블릿 밖으로 까지 예외가 전달되면 아래처럼 동작한다.
was(여기 까지 전파,tomcat) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
거꾸로 돌아가고 톰캣 같은 WAS까지 예외가 전달된다. 그럼 WAS는 어찌 처리할꼬?
Exception 의 경우 서버 내부에서 처리할 수없는 오류가 발생한 것으로 생각해서 HTTP 상태코드 500을 반환해준다.
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(404, "404 오류!");
}
WAS(sendError 호출 기록 화인) <- 필터 <- 서블릿 <- 입터셉터 <- 컨트롤러 (response.sendError)
실제 에러가 난것은 아니다.
서블릿 컨테이너는 client에게 응답을 전달 하기전에 sendError 가 호출된것을 확인하고 상태코드에 맞게 기본 오류 페이지를 보여준다.
상태코드를 지정할 수 있는 장점이 있다. 메시지는 뒤에서 꺼내는 방법을 설명해준다. default로 메세지는 숨기게 되어 있다.
서블릿 예외처리 - 오류화면 제공
서블릿 컨테이너가 제공하는 기본 예외처리 화면은 고객 친화적이지 않다. 서블릿이 제공하는 오류화면 기능을 사용해보자
예전에는 web.xml 에 에러코드및 에러에 매핑되는 페이지를 등록했었다.
지금은 스프링 부트를 통해서 서블릿 컨테이너를 실행하기 때문에, 스프링 부트가 제공하는 기능을 사용해서 서블릿 오류 페이지를 등록하면 된다.
@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);
}
}
위에 만든 에러페이지들은 스프링부트 기동시에 서블릿컨테이너에게 등록해달라고 요청한다.
RuntimeException.class 뿐만아니라 이를 상속하는 Exception들도 /error-page/500(@Controller) 으로 다 보내준다.
컨트롤러 단에서 에러가 발생해서 WAS가 까지 전파되었을 때 WAS가 위의 코드를 확인하고 해당 에러 매칭이 있는지 확인하고 존재하면 거기로 다시 넘긴다. 이때 Controller단에서 해당 /error-page에 대한 라우팅을 만들어줘야 한다.
서버내부에서 마치 HTTP 요청이 다시 일어난것 처럼 발생한다.
이때 오류 정보를 request attribute에 추가해서 넣어서 전달한다.
WAS (/error-page/500 요청) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> view
중요한 점은 웹 브라우저(클라이언트)는 서버 내부에서 이런일이 일어나는지 전혀 모른다는점이다. 오직 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출을 한다.
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: {}", 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("dispatchType={}", request.getDispatcherType());
}
이런식으로 reqeust attribute에 오류 정보를 확인해 볼 수 있다.
이 정보는 was에서 다시 error page요청을 할떄 request에 추가해서 보내는것이다. 추후 error page rendering할 때 쓸 수 있도록 하기위함이다.
서블릿 예외처리 - 필터
예외가 발생했을 때 was에서 에러페이지를 요청하면서 필터나 인터셉터가 한번 더 호출되는것은 비효율적이다.
결국 클라이언트로 부터 발생한 정상 요청인지, 아니면 오류 페이즈를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다. 서블릿은 이런 문제를 해결하기 위해 DispatcherType이라는 추가 정보를 제공한다.
FORWARD = RequestDispatcher.forward(request,response) 로 호출될시에 구분된다.
고객이 처음 요청하면 dispatcherType=REQUEST 이것으로 구분이 가능하다.
예외때문에 WAS가 요청하면 dispatcherType=ERROR 으로 나온다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
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;
}
만든 LogFilter가 어떤 타입에 적용될지 setDispatcherTypes에 명시해주면 된다.
위의 경우에는 client(request요청), was(error요청) 둘다 다 적용이 될 것이다.
서블릿 예외처리 - 인터셉터
@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/**");//오류 페이지 경로
}
인터셉터의 경우에는 따로 DispatcherTypes를 세팅 할 수 없고 excludePathPatterns 로 처리해주어야 한다.
error-page를 제외했기에 request시에만 해당 LogInterceptor를 거치게된다.
DispatcherType은 request.getDispatcherType() 로 꺼내 볼 수 있다.
여기에서 /error-page/** 를 제거하면 내부호출의 경우에도 인터셉터가 호출된다.
이런 등록과정을 스프링에서 편히 제공해준다.
여태까지 한 것들은 서블릿에서 제공해주는것들.
스프링 부트 - 오류페이지
스프링 부트는 아래의 과정을 기본으로 제공한다.
ErrorPage를 자동으로 등록한다. 이때 /error 라는 경로로 기본 오류 페이지를 설정한다.
new ErrorPage("/error") 를 기본으로 등록한다. 상태코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용된다.
서블릿 밖으로 예외가 발생하거나, response.sendError() 가 호출되면 모든 오류는 /error 를 호출하게 된다.
BasicErrorController 라는 스프링 컨트롤러를 자동으로 등록한다.
ErrorPage 에서 등록한 /error 를 매핑해서 처리하는 컨트롤러다.
개발자는 오류 페이지만 등록 하면 된다.
BasicErrorController에 기본적인 로직이 모두 개발되어 있다. 개발자는 오류 페이지 화면만 BasicErrorController 가 제공하는 룰과 우선순위에 따라서 등록하면 된다. 정적 html이면 정적 리소스, 뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면 된다.
우선순위는 뷰템플릿 -> 정적 리소스 -> 기본(/error) 순위이다.
error/4xx -> error/404 구체적일 수록 우선순위 가 높다
BasicErrorController 는 무엇을 제공해주는가?
<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>
이것을 model에 담아 넘겨줌으로 view에서 이것들을 활용할 수 있다.
하지만 실제 view에서 렌더링 되는것은 많이 없다. 대부분 null 이 나오게 된다. 고객은 에러를 알 필요도 없고 보안 문제가 생기기 떄문이다.
보게하려면 설정을 해줘야한다.
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param
이렇게 해야 null로 안오고 model에 포함할지 말지 선택할 수 있다.
on_param 같은 경우 url에 특정 parameter가 존재할 경우에만 노출해준다.