김영한 개발자님의 강의를 수강한 후 정리한 내용이다.
자바는 메인 메소드 실행시 main의 쓰레드가 실행된다. 예외를 잡지 못하고 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료된다.
response.sendError
(Http 상태코드, 오류 메시지)@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx(){
throw new RuntimeException("Exception occured");
}
@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);
}
}
하지만 서블릿 컨테이너가 제공하는 기본 예외 처리 화면은 사용자가 보기에 불편하다.
먼저 스프링 부트가 제공하는 기능을 사용해 서블릿 오류 페이지를 등록한다.
@Component // 스프링에 등록해주는 어노테이션
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
// 서블릿 컨테이너가 이렇게 사용하도록 지정함
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
// 낫파운드 에러가 뜨면 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);
// 등록을 함
}
}
해당 오류를 처리할 컨트롤러가 필요하다.
@Slf4j
@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";
}
}
- WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
- WAS
/error-page/500
다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500
) -> View
예를 들어서 RuntimeException
예외가 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인한다.
확인해보니 RuntimeException
의 오류 페이지로 /error-page/500
이 지정되어 있다. WAS는 오류 페이지를 출력하기 위해 /error-page/500
를 다시 요청한다.
WAS는 오류 페이지를 단순히 다시 요청만 하는 것이 아니라, 오류 정보를 request
의 attribute
에 추가해서 넘겨준다.
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));
//클라이언트 요청 URI - ex의 경우 NestedServletException 스프링이 한번 감싸서 반환
log.info("ERROR_REQUEST_URI: {}", request.getAttribute(ERROR_REQUEST_URI));
//오류가 발생한 서블릿 이름
log.info("ERROR_SERVLET_NAME: {}", request.getAttribute(ERROR_SERVLET_NAME));
// HTTP 상태 코드
log.info("ERROR_STATUS_CODE: {}", request.getAttribute(ERROR_STATUS_CODE));
//디스패처 타입 - 클라이언트로 부터 발생한 정상 요청인지, 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분
log.info("dispatchType={}", request.getDispatcherType());
}
두번씩 호출되는게 비효율적이기 때문에 클라이언트로부터 발생된 정상 요청인지 오류 페이지 출력을 위한 내부 요청인지 구분해야하고, 이를 위해 DispatcherType
을 사용한다.
dispatcherType=REQUEST
dispatchType=ERROR
dispatchType=FORWARD
dispatchType= INCLUDE
dispatchType=ASYNC
WebConfig
에 다음을 추가한다
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);{
return filterRegistrationBean;
}
이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출된다.
아무것도 넣지 않으면 기본 값이 DispatcherType.REQUEST
이다.
WebConfig
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error", "/error-page/**");
}
인터셉터는 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능이기 때문에 DispatcherType
과 무관하게 항상 호출된다.
대신에 인터셉터는 다음과 같이 요청 경로에 따라서 추가하거나 제외하기 쉽게 되어 있기 때문에, 이러한 설정을 사용해서 오류 페이지 경로를 excludePathPatterns
를 사용해서 빼주면 된다.
개발자는 오류 페이지 화면만 BasicErrorController 가 제공하는 룰과 우선순위에 따라서 등록하면 된다.
정적 HTML이면 정적 리소스, 뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면 된다.
BasicErrorController
의 처리 순서
1. 뷰 템플릿
resources/templates/error/500.html
resources/templates/error/5xx.html
(static, public)
resources/static/error/400.html
resources/static/error/404.html
resources/static/error/4xx.html
(error)
resources/templates/error.html
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>
/hello
)오류 관련 내부 정보들을 고객에게 노출하는 것은 좋지 않다. 고객이 해당 정보를 읽어도 혼란만 더해지고, 보안상 문제가 될 수도 있다.
//application.properties 각각의 포함여부
server.error.include-exception=true //예) exception 포함여부
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param
기본 값이 never
인 부분은 다음 3가지 옵션을 사용할 수 있다.
never
: 사용하지 않음always
:항상 사용on_param
: 파라미터가 있을 때 사용