서블릿 예외 처리 - 필터
바로 전 포스트에서 예외 발생 시 오류 페이지 요청 흐름에 대해 알아보았다.
2023.08.12 - [Spring/Spring mvc] - Sprinng Exception 1. 서블릿
불필요한 호출
오류가 발생시에 WAS 내부에서 다시 호출을 진행하는데 불필요하게 인터셉터, 서블릿, 필터가 2번씩 비효율적으로 호출된다.
하지만 로그인 기능 같은 경우, 이미 필터, 인터셉터에서 검증을 했는데, 한번 더 호출되는 것은 비효율적이다.
클라이언트에서 발생한 요청인지, 오류 페이지를 보여주기 위한 요청인지 구분해야 한다.
서블릿은 이럴 때 사용하기 위해 DispatcherType이라는 옵션을 제공한다.
DispatcherType
DispatcherType에는 5가지 타입이 존재하는데, 아래 두 가지를 활용하여 어떠한 요청인지 구분한다.
REQUEST: 클라이언트의 요청
ERROR: 오류 요청
로그를 보기 위해 LogFilter를 생성하고, Filter가 실행될 때 다름을 구분하기 위해 uuid를 선언하고, 어떠한 요청인지 타입을 확인하고 요청 url을 확인한다.
@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);
}
}
}
그 후 생성한 LogFilter를 WebConfig에 등록한다.
아래와 같이 DispatcherType을 REQUEST와 ERROR 둘 다 등록할 경우, 두 타입 실행마다 로그 필터가 실행된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
// 아래의 2가지 경우에 필터가 호출이 된다.
// 처음에는 REQUEST로, 그 후에 다시 오류가 났을 때는 DispatcherType이 ERROR로 나오게된다.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterRegistrationBean;
}
}
디스패쳐 타입으로 2번의 요청이 들어올 때 로그를 확인해 보면 아래와 같이 첫 요청 시에는 REQUEST로 요청이 들어오고 RESPONSE 또한 REQUEST로 나간다.
그 후 WAS에서 다시 요청이 들어올 때는 서버 내부에서 요청한 것으로, 요청 DispatcherType이 ERROR인 것을 볼 수 있으며 오류 view를 렌더링 하기 위해 errorPage 500으로 요청한다.
전 포스트에서 에러에 대해 자세히 보기 위해 printErrorInfo 메서드에서 출력한 로그와 함께 RESPONSE 또한 ERROR로 나오는 것을 볼 수 있다.
사실, 아래와 같이 DIispatcherType중 ERROR를 추가했기 때문에, 위와 같이 결과가 나온것이었다나온 것이었다.
DIspathcerType의 Default값은 REQUEST만 되어있기 때문에, 아래와 같이 ERROR을 추가하지 않으면, 기본적으로 필터를 거치지 않는다. 정리해서 요약하자면, ERROR를 제외하고 실행했을 때 ERROR부분이 실행되지 않는다. ERROR에 대한 로그를 보고 싶으면 아래와 같이 추가해주면 된다.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
서블릿 예외 처리 - 인터셉터
필터는 Java에서 지원하는 기능이지만 인터셉터는 Spring에서 지원하는 기능이다.
인터셉터의 ERROR 부분을 확인하기 위해 Interceptor를 생성하여 WebConfig에 등록하였다.
Interceptor의 구성은 preHandle, postHandle, afterCompletion 이렇게 구성되고 있다.
Interceptor의 동작원리를 알아야 이해가 되므로, 요약하자면 다음과 같다.
Interceptor 동작 원리
1. preHandle은 컨트롤러가 요청을 처리하기 전에 호출되며, false를 반환하면 요청 처리가 중단되고, true일 시 요청 처리가 계속 진행된다.
2. postHandle은 preHandle을 실행하고 false가 반환되면 실행되지 않고 true일 경우 실행되는데 뷰가 렌더링되기 전에 호출된다.
3. afterCompletion은 요청 처리가 끝난 후 위 2개와 관계 없이 무조건적으로 실행되며 요청 처리의 마지막 단계에서 실행된다.
*스프링 mvc패턴에선 view, RestApi 개발에서는 뷰가 아닌 응답 데이터에 대한 후처리나 로깅 작업을 말함
@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);
}
}
}
Filter와 비슷한 역할을 하기 위해 필요한 내용을 담았다.
Interceptor에는 필터의 DispatcherType과 같은 것이 없기 때문에, 예외가 발생했을 때 기본적으로 인터셉터를 거치게 된다.
그렇기 때문에 특정 요청을 제외하고 싶은 경우, 'excludePathPatterns' 메서드를 사용해서 안거치게 할 수 있다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error")
.excludePathPatterns("/error-page/**"); // 오류 페이지 경로
}
interceptor에서 excludePathPatterns를 사용하여 error-page를 제외한 경우 2번째 요청에 있어서 제외하게 된다.
왼쪽은 excludePathPatterns로 제외했을 때이고, 오른쪽은 제외하지 않았을 때이다. 오른쪽은 제외하지 않아서 REQUEST가 존재하고, 왼쪽에는 존재하지 않는 것을 볼 수 있다.
이번 공부를 하면서 예외가 발생했을 때 필터와 인터셉터를 안 거치고 통과하는 방법을 알게 되었다.
다음 포스팅에서는 API예외처리에 대해서 알아본다.
코드 링크
Filter
https://github.com/toychip/StudySpring/commit/265cb9536a331ab00b1775b8572a9e7ad23ae90e
Feat: Spring-mvc2-exception 서블릿 예외처리 - 필터 · toychip/StudySpring@265cb95
toychip committed Aug 14, 2023
github.com
Interceptor
https://github.com/toychip/StudySpring/commit/315e77932691a5030c26f20b77b2377acb295d17
Feat: Spring-mvc2-exception 서블릿 예외처리 - 인터셉터 · toychip/StudySpring@315e779
toychip committed Aug 14, 2023
github.com
김영한님 mvc2 - Exception 강의로 학습하였습니다.
공부한 내용을 상기시키기고 해당 내용을 몰랐을 때의 입장에서 쉽게 배울 수 있지 않을까 해서 작성한 글입니다. 잘못된 내용이 있을 경우 말씀해 주세요. 피드백 환영합니다.
'Spring > Spring Exception' 카테고리의 다른 글
Spring Exception 6. ExceptionResolver (HandlerExceptionResolver) (0) | 2023.08.17 |
---|---|
Spring Exception 5. 스프링 부트 Api 오류처리 (BasicErrorController) (0) | 2023.08.16 |
Spring Exception 4. Api 예외 처리 (Spring mvc VS RestfulApi) (0) | 2023.08.16 |
Spring Exception 3. 스프링 부트 오류페이지 View(BasicErrorController) (0) | 2023.08.15 |
Spring Exception 1. 서블릿1 (Spring에서 예외처리를 어떻게 할까?) (0) | 2023.08.12 |