[스프링 웹 MVC 2편] 012. 예외 처리
서블릿 예외 처리
서블릿 예외 처리 방식
Exception
Response.sendError(HttpStatus.StatusCode, "ErrorMessage")
1. Exception
컨트롤러에서 발생한 예외는 인터셉터 -> 서블릿 -> 필터를 거쳐 WAS까지 반환됨
WAS로 예외가 올라오는 경우 Tomcat이 기본으로 제공하는 오류 화면을 볼 수 있다.
p.s) 이를 보기 위해 whitelabel 에러 페이지 생성 기능을 꺼두었다.
2. response.sendError(HttpStatus.상태코드, "errmsg")
HttpServletResponse
가 제공하는 .sendError()
메서드를 사용하는 방법도 있다.
서블릿 컨테이너에게 오류가 발생한 것을 전달할 수 있다
이 메서드는 HttpStatus.상태코드
와 errmsg
즉 오류메세지도 전달할 수 있다.
사용 용례는 다음과 같다
package hello.exception.servlet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx(){
throw new RuntimeException("Exception Occurred");
}
// WAS까지 예외가면 WAS에서 다시 여기로 호출함.
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
// 발생하는 것은 아니고 메서드자체가 checked 예외로 IOException이 있어서 던져줘야함.
response.sendError(404, "404 오류!");
}
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
// Exception 터진건 무조건 500으로 나간다.
// 아니면 직접 예외 상태코드를 아래처럼 만들어서 처리할 수 있다!
response.sendError(500);
}
}
sendError
실행 과정
WAS (.sendError
호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (response.sendError
)
WAS는 .sendError
의 호출을 확인하면 설정된 오류코드에 맞추어 기본 오류페이지를 보여준다.
서블릿 예외처리 - 오류화면 제공
스프링 부트를 통해 서블릿 컨테이너를 실행하기 때문에 WebServerFactoryCustomizer
의 구현체를 생성하고 이를 @Component
로 스프링 빈 등록함으로써 다음과 같이 서블릿 오류페이지를 등록할 수 있다.
// 서블릿 방식
//@Component
public class WebServerCustomizer // TOMCAT Server Error page Customization
implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
// errorPage404 호출
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
/// errorPage500 호출
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
// errorPageEx 호출
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
오류 페이지는 해당 예외와 자식예외까지 한꺼번에 처리한다.
내부 로직에 뭔가 isAssignableFrom
같은 supports
로직이 있을거같은데 그건 아니고 자바 Generic
이 이용됐다.
하긴 이건 Resolver
와는 느낌이 다르니,,,
(물론 뒤에 Exception
관련 Resolver
들이 등장한다. 이건 애노테이션 기반인지, HTTP text/json인지 구분)
public ErrorPage(Class<? extends Throwable> exception, String path) {
this.status = null;
this.exception = exception;
this.path = path;
}
위에서 특정 컨트롤러로 다시 호출하도록 코드를 구성했기 때문에 해당 urlPattern
에 대응하는 컨트롤러를 구현해야한다.
@Slf4j
@Controller
public class ErrorPageController {
@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";
}
}
그리고 리턴한 뷰에 대한 타임리프 템플릿을 이용한 html
페이지도 구성하였는데 별게 없어서 여기서는 다루지 않는다. (그냥 오류화면입니다 띄우는게 전부였기에)
서블릿 예외 처리 - 오류페이지 작동 원리
서블릿은 Exception
이 발생해서 서블릿 밖으로 전달되거나Response.sendError()
가 호출되었을 때 설정된 오류 페이지를 찾는다.
WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다
오류 정보 추가
WAS는 오류 페이지를 요청할 때 오류 정보를 Request
의 attribute
에 추가해서 넘겨준다
@Slf4j
@Controller
public class ErrorPageController {
// RequestDispatcher 상수화 되어있음
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)); // 클라이언트 요청 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());
// HTTP 상태 코드
}
}
request.getDispatcherType()
랑 request.getAttribute(ERROR_EXCEPTION_CODE)
이랑 다른게 뭔지 고민하고 있었는데 사실상 같은거였다.
위에 정의된 상수를 .getDispatcherType()
에서 가져온거라...
서블릿 예외 처리 - 필터
일단 WAS가 예외를 감지하고 다른 페이지를 호출했다고 치자.
그런데, 에러로 인해 요청된 다음 페이지에서 필터 / 인터셉터 호출이 모두 다시 호출되는데
이 필터 인터셉터는 이미 이전에 컨트롤러에서 예외를 던지기 전 과정에서 모두 진행된 로직이다.
결국 불필요한 로직이 두 번 실행되는 것이고 이건 한번만 검증해줘도 될거같은데 방법이 없을까?
서블릿은 이런 문제를 해결하기 위해 이것이 고객의 요청인지, 아니면 WAS에서 오류 페이지를 호출한 것인지DispatcherType
이라는 추가 정보를 통해 구별한다.
DispatcherTypes
/*
* Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
* package javax.servlet;
/**
* Enumeration of dispatcher types. Used both to define filter mappings and by Servlets to determine why they were * called. * * @since Servlet 3.0
*/public enum DispatcherType {
/**
* {@link RequestDispatcher#forward(ServletRequest, ServletResponse)}
*/ FORWARD,
/**
* {@link RequestDispatcher#include(ServletRequest, ServletResponse)}
*/ INCLUDE,
/**
* Normal (non-dispatched) requests. */
REQUEST,
/**
* {@link AsyncContext#dispatch()}, {@link AsyncContext#dispatch(String)} and
* {@link AsyncContext#addListener(AsyncListener, ServletRequest, ServletResponse)}
*/ ASYNC,
/**
* When the container has passed processing to the error handler mechanism such as a defined error page. */
ERROR
}
다음과 같이 요청의 종류에 따라 DispatcherType
을 구분한다.
그리고 WebMvcConfigurer
의 구현체인 WebConfig
에서 다음과 같이 DispatcherType
에 따른 필터 실행 조건을 추가할 수 있다
setDispatcherTypes(...)
@Configuration
public class WebConfig implements WebMvcConfigurer {
//@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
// 이 필터는 Request, Error인 경우 호출된다.
return filterRegistrationBean;
}
}
서블릿 예외 처리 - 인터셉터
@Slf4j
@Component
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);
}
}
}
필터와는 다르게 인터셉터의 경우 스프링 MVC에 의해 제공되는 기능으로 서블릿이 제공하는 DispatcherType
를 직접 사용할 수 없는데 때문에 WebConfig
에서 excludPathPatterns
를 사용하여 오류 페이지 경로에서는 인터셉터를 사용하지 않도록 설정해주어야 한다.
코드는 다음과 같다
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
// 오류페이지 경로를 넣어버리면 된다 / DispatcherType을 가지고는 어떻게 할 수 없다
// provided by Spring MVC Not Servlet(DispatcherType - Servlet Functionality)
}
정리
정상 요청 처리 과정WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> 뷰
오류 요청 처리 과정
- WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
- 컨트롤러(예외) -> 인터셉터 -> 서블릿 -> 필터 -> WAS
- WAS에서는
.sendError()
호출 여부와 에러 페이지를 확인 - WAS(dispatcherType=error) -> 필터(x) -> 인터셉터(x) -> 컨트롤러 -> View 호출
오류 페이지 생성의 경우 내용이 너무 간단해서, 따로 정리하지는 않겠다.
만약 스프링 프로젝트를 하다가 오류페이지 관련해서 기억이 안나는게 있으면 교안을 다시 한번 보자(나를 위한 메모)