4 분 소요

 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술편을 학습하고 정리한 내용 입니다.

서블릿 예외 처리 - 필터

예외 처리에 따른 필터와 인터셉터, 그리고 서블릿이 제공하는 DispatchType 이해하기

예외 발생과 오류 페이지 요청 흐름

1
2
3
1. WAS(여기 까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외 발생)

2. WAS '/error-page/500' 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> View

오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한번 호출이 발생한다.

이때 필터, 서블릿, 인터셉터도 모두 다시 호출된다.

중복 호출을 막기 위해 이 요청이 클라이언트로부터 발생한 요청인지, 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다.

서블릿은 이런 문제를 해결하기 위해 DispatchType이라는 추가 정보를 제공한다.

DispatchType

저번에 찍어본 로그에 dispatchType=ERROR라는 로그가 남아있다.

고객이 처음 요청하면 dispatchType=REQUEST이다.

1
2
3
4
5
6
7
8
9
10
public enum DispatcherType {  
    FORWARD,  
    INCLUDE,  
    REQUEST,  
    ASYNC,  
    ERROR;  
  
    private DispatcherType() {  
    }  
}
  • REQEUST : 클라이언트 요청
  • ERROR : 오류 요청
  • FORWARD : 서블릿에서 다른 서블릿이나 JSP 호출 시
    • RequestDispatcher.forward(request, response);
  • INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
    • RequestDispatcher.include(request, response);
  • ASYNC : 서블릿 비동기 호출

로그 필터

간단하게 로그 필터를 사용해서 예외 발생 시 필터의 호출을 확인해 보자.

패키지 이름 logFilter가 아니라 filter이다.

hello.exception.filter.LogFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Slf4j  
public class LogFilter implements Filter {  
  
  
    @Override  
    public void init(FilterConfig filterConfig) throws ServletException {  
        log.info("log filter init");  
    }  
  
    @Override  
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {  
  
        HttpServletRequest request = (HttpServletRequest) servletRequest;  
        String requestURI = request.getRequestURI();  
  
        String uuid = UUID.randomUUID().toString();  
  
        try {  
            log.info("REQUEST [{}][{}][{}]", uuid, servletRequest.getDispatcherType(),  requestURI);  
            filterChain.doFilter(servletRequest, servletResponse);  
        } catch (Exception e) {  
            throw e;  
        } finally {  
            log.info("RESPONSE [{}][{}][{}]", uuid, servletRequest.getDispatcherType(), requestURI);  
        }  
    }  
  
    @Override  
    public void destroy() {  
        log.info("log filter destroy");  
    }  
}

로그를 출력하는 부분에 request.getDispatcherType()을 추가해두었다.

이제 필터 등록 하자.

hello.exception.WebConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration  
public class WebConfig  implements WebMvcConfigurer {  
  
    @Bean  
    public FilterRegistrationBean<Filter> logFilter() {  
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();  
        filterFilterRegistrationBean.setFilter(new LogFilter());  
        filterFilterRegistrationBean.setOrder(1);  
        filterFilterRegistrationBean.addUrlPatterns("/*");  
        filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);  
        return filterFilterRegistrationBean;  
    }  
}

여기서 중요하게 볼 부분이

1
filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);  

이다.

이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출된다.

아무것도 넣지 않으면 기본 값이 REQUEST이다. 즉 클라이언트의 요청이 있는 경우에만 필터가 동작한다.

특별히 오류 페이지 경로도 필터로 적용하지 않을 거라면, 기본 값을 그대로 사용한다.

오류 페이지 전용 필터를 적용하고 싶으면 ERROR만 지정하면 된다.

결과를 보면 진짜 필터가 2번 호출이 된다..

서블릿 예외 처리 - 인터셉터

이번엔 인터셉터가 예외 상황 시 어떻게 동작하는지 알아보자.

로그 인터셉터 추가

hello.exception.interceptor.LogInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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 uuid = (String)request.getAttribute(LOG_ID);  
        String requestURI = request.getRequestURI();  
  
        log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);  
        if (ex != null) {  
            log.error("afterCompletion error", ex);  
        }  
    }  
}
1
log.info("REQUEST [{}][{}][{}][{}]", uuid, request.getDispatcherType(), requestURI, handler);

여기도 별건 없고 로그 찍는 곳에 request.getDispatcherType()를 추가했다.

이제 WebConfig에 등록하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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<Filter> logFilter() {  
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();  
        filterFilterRegistrationBean.setFilter(new LogFilter());  
        filterFilterRegistrationBean.setOrder(1);  
        filterFilterRegistrationBean.addUrlPatterns("/*");  
        filterFilterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);  
        return filterFilterRegistrationBean;  
    }  
}

자 먼저 로그 필터는 잠시 꺼두기 위해 @Bean은 주석 처리 했다.

그리고 필터와 인터셉터를 비교하면서 보자.

인터셉터는 따로 setDispatcherTypes()같은 기능이 없다.

즉 설정 안 하면 무조건 두 번 호출 될 것이다.

하지만 인터셉터는 excludePathPatterns()을 이용해서 상세하게 미 적용 대상을 지정할 수 있기 때문에

여기다가 에러에 관련된 컨트롤러 호출 uri을 넣어서 제외시킨다.

1
.excludePathPatterns("/css/**","*.ico","/error", "/error-page/**");

여기서 "/error-page/**" 이게 없다면 에러 호출 시 2번 인터셉터가 동작한다.

즉 지금은 사용자 요청 딱 1번만 인터셉터가 호출 된다.

인터셉터 호출 딱 한번만 이뤄지고 그 후 에러 컨트롤러 로직이 실행 됐다.

그럼 /error-page/**를 주석 처리 해보자.

두 번 호출 된다. 재밌는 건 WAS의 호출은 스프링 입장에서 error 컨트롤러 정상 작동 한 것이기 때문에

postHandle()이 호출 되었다.

전체 흐름 정리

정상 요청

/hello 정상 요청

1
WAS(/hello, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View

오류 요청

/error-ex 오류 요청

  • 필터는 DispatchType으로 중복 호출 제거(dispatchType=REQEUST)
  • 인터셉터는 경로 정보로 중복 제거(excludePathPatterns("/error-page/**"))
1
2
3
4
1. WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 
2. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생) 
3. WAS 오류 페이지 확인 
4. WAS(/error-page/500, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 컨트 롤러(/error-page/500) -> View

댓글남기기