서블릿 예외 처리 - 시작
스프링의 도움을 받지 않고 순수한 서블릿 컨테이너는 어떻게 예외를 처리할까?
다음 2다기 방식으로 처리한다
웹 앱은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행이 되는데 예외 발생시 try-catch로 잡지 못한 경우 서블릿 밖까지 예외가 튀어나가면 어떻게 될까?
이렇게 톰캣과 같은 WAS까지 예외가 전달된다.
일단 스프링 부트가 제공하는 에러페이지 (흔히 whitelabel) 옵션을 꺼두고 시작하자
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx() {
thrownewRuntimeException("예외 발생!");
}
}
HTTP Status 500 – Internal Server Error
위와같은 톰캣이 기본적으로 제공하는 에러페이지가 나올것이고
위 매핑 말고 다른 url로 요청할 경우 또한 기본 제공하는 404 에러페이지가 나올것이다.
서블릿 컨테이너에게 오류가 발생했다는 점을 전달하고, 오류 메시지를 추가하기 위해 response.sendError 를 사용해보자
//ServletExController
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(404, "404 오류!"); }
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
response.sendError(500);
}
WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러
위 과정에서 WAS에서 컨트롤러의 sendError를 통해 보낸 에러 메시지를 확인하고 오류 코드에 맞추어 "기본 에러 페이지" 를 보여준다
서비스가 망한것같은 기본 오류페이지 말고 좀더 산뜻한 오류페이지를 만들고 매핑해보자
서블릿 예외처리 - 오류 화면 제공
과거에는 web.xml 파일을 만들어 오류 화면을 등록했지만
이제는 스프링 부트를 통해 서블릿 컨테이너를 실행하기 때문에 좀 더 수월하게 등록할 수 있다
@Component
public class WebServerCustomizer implements
WebServerFactoryCustomizer<ConfigurableWebServerFactory>
위 클래스에 에러페이지를 만들어 객체를 생성하고 factory에 등록해준다.
@Slf4j
@Controller
public class ErrorPageController
적절한 컨트롤러를 만들어주고, templates/error-page/OO.html 파일을 만들어 매핑해주자
서블릿 예외처리 - 오류 페이지 작동 원리
http://localhost:8080/error-ex
해당 url을 요청했다고 가정하자
WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러
컨트롤러에서 ("/error-ex") 매핑시 throw new RuntimeException("예외 발생!") 예외를 던지고,
WebServerCustomizer 에서 new ErrorPage(RuntimeException.class, "/error-page/500"); 을 통해 에러페이지를 등록을 해놨기 때문에 error-page/500.html 을 바로 띄우는 것이 아니라 "컨트롤러에 다시 요청" 한다
정리하자면,
컨트롤러에서 발생한 예외가 WAS 까지 전파가 된 후
WAS는 오류 페이지 경로를 찾아 오류 페이지를 호출하는데, 이 과정에서 필터, 서블릿, 인터셉터, 컨트롤러를 모두 재호출한다.
@Slf4j
@Controller
public class ErrorPageController {
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";
...
}
RequestDispatcher 상수로 정의하고 호출함으로써 에러 정보도 확인이 가능하다
위 내용을 정리하면서 느낀건데 클라이언트의 요청부터 MVC의 과정에 있어
WAS와 서블릿, 인터셉터 등 자세한 전달 과정을 복기시켜볼 필요가 있을 것 같다
서블릿 예외처리 - 필터
필터는 재호출 되는 경우를 위해서 dispatcherTypes 라는 옵션을 제공한다.
LogFilter 와 WebConfig를 설정하고 http://localhost:8080/error-ex
위 url을 다시 호출해보자
...
try {
log.info("REQUEST [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
}
...
클라이언트의 요청에서는 dispatcherType REQUEST 인 것을 확인할 수 있다.
이후 chain.doFilter를 통해 필터 or 서블릿을 지나
ServletExController의 @GetMappling("/error-ex") 호출
예외가 발생해서 다시 LogFilter의 catch 문에서 예외 처리
다시 WAS로 갔다가 로그를 찍고
이번엔 dispatcherType ERROR 인 것을 확인 할 수 있다.
//WebConfig
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR)
WebConfig에 dispatcherType을 설정하지 않으면 기본값은 REQUEST 이다.
필터는 서블릿의 기술이지만 인터셉터는 스프링의 일부이기 때문에 다른 방법이 필요하다
다음 챕터에 이어서 알아보자
서블릿 예외처리 - 인터셉터
인터셉터는 서블릿이 제공하는 것이 아닌 스프링이 제공하는 기능이기 때문에,
DispatcherType과 무관하게 항상 호출 된다.
즉, 필터는 DispatcherType으로 중복 호출을 제거하고 (dispatcherType = REQUEST)
인터셉터는 경로정보로 중복 호출을 제거한다 (excludePathPatterns(" /error-page/** ")
이정도만 간단히 이해하고 넘어가자
스프링 부트 - 오류 페이지1
지금까지 예외 처리 방식을 서블릿으로 근간을 파악했다
등 매우 귀찮은 작업들을 진행했는데 스프링부트는 이런 기능을 모두 기본으로 제공한다
BasicErrorController 에는 모든 기능이 구현되어 있기 때문에 개발자는 제공되는 룰과 우선순위에 따라 등록만 하면 된다
뷰 템플릿과 정적 리소스 모두 자세한것 -> 포괄적인것 순서로 페이지를 띄운다 (404 가 4xx 보다 우선)
스프링 부트 - 오류 페이지2
BasicErrorController의 기능에 대해 더 알아보자
* timestamp: Fri Feb 05 00:00:00 KST 2021
* status: 400
* error: Bad Request
* exception: org.springframework.validation.BindException
* trace: 예외 trace
* message: Validation failed for object='data'. Error count: 1
* errors: Errors(BindingResult)
* path: 클라이언트 요청 경로 (`/hello`)
위 정보를 model에 담아 뷰에 전달하게 된다
<ul>
<li>오류 정보</li>
<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>
</li>
</ul>
위 뷰 템플릿으로 해당 값들을 출력할 수 있다
오류 관련 정보를 고객에게 노출하는 것은 좋지 않으므로 다음 오류정보들을 model에 포함할지 여부를 선택할 수 있다
//application.properties
server.error.include-exception=true
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param
밑에 3개의 오류정보는 기본값이 never이기 때문에 never, always, on_param 등 3개의 속성값을 설정할 수 있다.
Reference
김영한 님 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술