사용자 요청별로 별도의 스레드가 할당, 서블릿 컨테이너 안에서 실행
try ~ catch 로 예외 잡아서 처리하면 문제 없다. 하지만, 예외를 미처 잡지 못하고 서블릿 밖으로 나가면 WAS까지 전달 가능
💨 WAS(여기까지 전달) ← filter ← servlet ← interceptor ← controller (예외 발생)@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);
}
}
@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/**"); // 오류 페이지 경로
}
// @Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
**filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);**
// 이 필터는 dispatcherType이 request,error일 때 호출 됩니다.
return filterRegistrationBean;
}
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response); // 필수
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
<aside>
💨 1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
2. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
3. WAS 오류 페이지 확인
4. WAS(/error-page/500, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) ->
컨트롤러(/error-page/500) -> View
</aside>
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 정상적인 ModelAndView로 변환
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView(); // 에외 먹어버림
}
} catch (IOException e) {
e.printStackTrace();
log.error("resolver ex", e);
}
return null;
}
}
💨 1. 컨트롤러에서 예외 발생
2. 서블릿에 예외 전달
3. ExceptionResolver에서 예외 처리 하고 ModelAndView 리턴하여 정상 흐름처럼 보이도록함
4. WAS 정상 응답
5. WAS에서 오류 페이지 정보를 다시 찾아 /error 호출
<@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver>
resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
public class UserException extends RuntimeException
@ResController
public class ApiExceptionController{
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id){
if(id.equals("user-ex"){
throw new UserException("사용자 오류");
}
return new MemberDto(id,"hello"+id);
@Data
@AllArgsConstructor
static class MemberDto{
private String memberId;
private String name;
}
}
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof UserException) {
log.info("UserException resolver to 400");
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)) { // accept이 json이면
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else{ // 아니면 == html 이면
return new ModelAndView("error/500");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
throws IOException {
if (!StringUtils.hasLength(reason)) {
response.sendError(statusCode);
}
else {
String resolvedReason = (this.messageSource != null ?
this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
reason);
**response.sendError(statusCode, resolvedReason);**
}
return new ModelAndView();
}
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
**@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")**
public class BadRequestException extends RuntimeException {
}
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2(){
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad",
new IllegalArgumentException());
}