[Spring Framework] 오류 페이지 처리 1탄
해당 글은 인프런 김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 을 보고 정리한 내용입니다.
자바 실행
자바의 main 메서드를 실행하는 경우 main 이라는 이름의 쓰레드가 실행된다.
에러가 발생하고 잡지 못하면 main 쓰레드가 예외 정보를 남기고 종료된다.
웹 애플리케이션
사용자 요청 별로 별도의 쓰레드가 할당 되고, 서블릿 컨테이너 안에서 실행된다.
try ~ catch로 예외를 잡으면 아무런 문제가 없지만 잡지 못하고 서블릿 밖으로 전달되면
서블릿의 오류 페이지 요청 흐름
- WAS → 필터 → 서블릿 → 인터셉터 → 컨트롤러 ⇒ 정상요청
- WAS ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생) ⇒ 예외발생
- WAS “/error-page/500" → 필터 → 서블릿 → 인터셉터 → 컨트롤러(/error-page/500) → View ⇒ 에러페이지 호출
위와 같이 클라이언트로 부터 정상요청을 받았는데 내부에서 어떠한 400 또는 500 등의 에러가 발생하고 예외에 잡히게 된다. 그러면 정상 요청에 대한 오류 응답이 WAS까지 전달 되고 WAS 는 다시 에러 페이지 호출을 위한 컨트롤러에 요청을 보내게 된다.
즉, 예외에서 컨트롤로 한 번(예외처리) 그리고 WAS에서 컨트롤(에러 페이지 호출)로 두 번의 호출이 일어나게 된다.
오류 페이지를 보여주기 과정에서 클라이언트의 요청인지 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분이 필요한데 이를 위해 서블릿은 DispatcherType 이라는 추가 정보를 제공한다.
DispatcherType 종류
REQUEST: 클라이언트 요청
ERROR: 오류 요청
FORWARD: 서블릿에서 다른 서블릿이나 JSP를 호출할 때
INCLUDE: 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
ASYNC: 서블릿 비동기 호출
정상 및 오류 요청 흐름 구별
※ 해당 방법은 서블릿이 제공하는 필터에서만 적용이 가능합니다.
오류 페이지 처리 방법
- 필터는 DispatcherType 으로 중복호출 제거
- 인터셉터는 경로 정보로 중복 호출 제거(excludePathPatterns("/error/**"))
소제목과 요청 흐름에서 보면 서블릿을 기준으로 왼쪽의 필터와 오른쪽의 인터셉터에서 처리 할 수 있다.
(필터는 서블릿이 제공하고 인터셉터는 스프링이 제공하는 기능이다.)
필터(Filter)
Filter 인터페이스
- package 의 경로를 보면 javax.servlet 으로 서블릿(servlet)이 제공하는 것을 알 수 있다.
- 해당 인터페이스를 구현하려면 init, doFilter 그리고 destory 메서드를 구현해야한다.
package javax.servlet;
import java.io.IOException;
public interface Filter {
void init(FilterConfig var1) throws ServletException;
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
void destroy();
}
Filter 를 구현한 LogFilter 클래스
- 해당 클래스는 개발자가 직접 구현을 하면 된다.
즉, 클래스 이름은 알아서 정하면 된다.
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import java.io.IOException;
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest requset, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException
{
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
try {
log.info("Request [{}][{}]", request.getDispatcherType(), requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
}
}
@Override
public void destroy() {
}
}
webConfig 클래스
- setDispatcherTypes의 default 는 REQUEST 이다.
- 따라서, 에러에 대해서 해당 필터는 두 번 호출 되지 않는다.
- 만약, 호출을 원한다면 DispatcherType.ERROR를 추가하면 된다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
@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);
return filterRegistrationBean;
}
}
인터셉터(Intercepter)
인터셉터 클래스
- preHandle
- postHandle
- 에러가 발생하면 호출이 안된다.
- afterCompletion
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("Request [{}][{}][{}]", request.getDispatcherType(), requestURI, handler);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
webConfig 클래스
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.icon", "/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);
return filterRegistrationBean;
}
}