서블릿은 2가지 방식으로 예외 처리를 지원한다
자바 직접 실행
자바의 메인 메서드를 직접 실행하는 경우, main이라는 이름의 쓰레드가 실행된다.
실행 도중에 예외를 잡지 못하고, 처음 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고, 해당 쓰레드는 종료된다.
웹 애플리케이션
웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
애플리케이션에서 예외가 발생했는데, 어디선가 try-catch 로 예외를 잡아서 처리한다면 아무런 문제가 없다. 그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖까지 예외가 전달되면??
WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러
컨트롤러에서 예외가 발생하고, 이것이 WAS 까지 왔다면.
톰캣과 같은 WAS 까지 예외가 전달된다. WAS 는 예외가 올라오면 어떻게 처리할까?
ServletExController
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("에외 발생!");
}
@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);
}
}
http://localhost:8080/error-ex 요청 시 500 - Internal Server Error가 발생한다.
http://localhost:8080/error-404 요청 시 404 에러 코드가 발생함.
http://localhost:8080/error-500 요청 시 500 에러 코드가 발생함.
서블릿 컨테이너가 제공하는 기본 예외 처리 화면은 고객 친화적이지 않다. 서블릿이 제공하는 오류 화면 기능을 사용해보자.
서블릿은 Exception (예외) 가 발생해서, 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출 되었을 때 각각의 상황에 맞춘 오류 처리 기능을 제공한다.
서블릿은 Exception 이 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError() 가 호출 되었을 때, 설정된 오류 페이지를 찾는다.
예외 발생 흐름
WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러
컨트롤러에서 예외가 발생 → WAS 까지 전달
sendError 흐름
WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러
이후 WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다.
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
HttpStatus.NOT_FOUND 예외가 WAS까지 전달되면, 그에 맞는 오류페이지로 /error-page/404 가 지정되어 있다.
WAS는 오류 페이지를 출력하기 위해 /error-page/404 를 다시 요청한다. 이후 컨트롤러에서 오류페이지를 띄운다(View 호출)
WAS "/error-page/404" 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 ("/error-page/404")
-> View
💡 웹 브라우저 (클라이언트) 는 서버 내부에서 이런일이 일어나는지 전혀 모른다.
WAS는 오류페이지를 단순히 다시 요청만 하는것이 아니라, 오류 정보를 request의 attribute에 추가해서 넘겨준다.
ErrorPageController - 오류 출력
@Slf4j
@Controller
public class ErrorPageController {
//RequestDispatcher 상수로 정의되어 있음
public static final String ERROR_EXCEPTION =
"jakarta.servlet.error.exception";
public static final String ERROR_EXCEPTION_TYPE =
"jakarta.servlet.error.exception_type";
public static final String ERROR_MESSAGE = "jakarta.servlet.error.message";
public static final String ERROR_REQUEST_URI =
"jakarta.servlet.error.request_uri";
public static final String ERROR_SERVLET_NAME =
"jakarta.servlet.error.servlet_name";
public static final String ERROR_STATUS_CODE =
"jakarta.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("dispatchType={}", request.getDispatcherType());
}
}
LogFilter
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
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);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1);
filterFilterRegistrationBean.addUrlPatterns("/*");
filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterFilterRegistrationBean;
}
}
http://localhost:8080/error-ex → 요청
실행 과정
LogFilter에서 로그 출력
REQUEST [5a44ed08-cc80-4a68-b126-f8192d76714b][REQUEST][/error-ex] → LogFilter
서블릿 호출
컨트롤러 errorEx() 컨트롤러에서 RuntimeException(”예외 발생!”) 호출
서블릿으로 예외 전달
필터로 예외 전달
필터에서 예외 catch 이후 로그 출력 :
EXCEPTION Request processing failed: java.lang.RuntimeException: 에외 발생!
finally 로그 출력
RESPONSE [5a44ed08-cc80-4a68-b126-f8192d76714b][REQUEST][/error-ex]
WAS 로 예외 전달
WebServerCustomizer 에서
ErrorPage errorPageEx = new ErrorPage*(*RuntimeException.class, "/error-page/500"*)*;
/error-page/500 으로 요청
LogFilter에서 로그 다시 출력됨.
REQUEST [609391c2-4820-4cd0-b64d-004c3279ab8e][ERROR][/error-page/500]
서블릿 호출
컨트롤러 errorPage500 호출 이후
로그 출력됨.
errorPage 500
ERROR EXCEPTION: java.lang.RuntimeException: 에외 발생!
…
dispatchType=ERROR
뷰 렌더링 “error-page/500”
서블릿 호출
필터 호출 - 예외 없음.
finally 로그 출력
RESPONSE [f9fd2bed-5dc4-4b6a-a1d0-ba2d93fc0fb3][ERROR][/error-page/500]
→ 즉, Filter는 2번 호출됨.
Filter → Servlet 기술이지만, Interceptor → Spring 기술이다,
Interceptor에서 예외 처리는 어떻게 이루어질까?
LogInterceptor
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
log.info("REQUEST [{}][{}][{}][{}]", uuid, request.getDispatcherType(),
requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String)request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, request.getDispatcherType(),
requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
preHandle → 실행 됨
controller → 예외 발생 함
postHandle → controller 에서 예외 발생하여 실행 안됨.
afterCompletion → 항상 호출 됨.
Interceptor를 내부 호출 시 호출 하지 않기 위해서는
.excludePathPatterns*(*"/css/**", "*.ico", "/error", "/error-page/**"*)*;
에서 “/error-page/**” 를 넣어줘야 한다.
WebConfig - addInterceptors
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
//필터 처럼 Dispatcher Type 에 따라 적용할 지 선택할 수 없다.
//대신 오류 경로를 추가하면 된다.
}
이제는 서블릿이 제공하는 기능을 넘어서, 스프링 부트가 오류페이지 같은 것들을 어떻게 제공하는지 알아보자.
지금까지 예외 처리를 위해
스프링 부트는 이런 과정을 모두 기본으로 제공한다.
/error 라는 경로로 기본 오류 페이지를 설정한다.new ErrorPage(”/error”), 상태 코드와 예외를 설정하지 않으면, 기본 오류 페이지로 사용된다response.sendError() 가 호출되면, 모든 오류는 /error 를 호출하게 된다./error 를 매핑해서 처리하는 컨트롤러다.즉, 에러가 발생하면, “/error” 를 기본 요청하고, 이를 처리하는 컨트롤러는 BasicErrorController 이다.
개발자는 오류페이지만 등록하면 된다. → 어디에?
View 선택 우선 순위
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`)
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
이를 통해 오류 정보를 model 에 포함할지 여부를 선택할 수 있다.
🚨 하지만, 실무에서는 노출하면 안된다. - 보안에 취약점, 고객이 이해할 수 있는 간단한 오류 메시지 만 포함해야 한다.