스프링 인터셉터
서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결하는 기술
스프링 MVC가 제공하는 기술
스프링 인터셉터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출
스프링 인터셉터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 // 로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단 시 컨트롤러 호출 X )
스프링 인터셉터 체인
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터1 -> 스프링 인터셉터2 -> 컨트롤러
스프링 인터셉터는 체인으로 구성
중간에 인터셉터를 자유롭게 추가 가능(순서 지정 가능)
HandlerInterceptor 인터페이스
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {}
default void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}
default void afterCompletion(HttpServletRequest request, HttpServletResponse
response,Object handler, @Nullable Exception ex) throws Exception {}
}
서블릿 필터의 경우 doFilter()하나만 제공하나 인터셉터는 컨트롤러 호출 전, 호출 후, 요청 완료 이후와 같이 단계적으로 제공
서블릿 필터의 경우 단순히 request, response만 제공했지만, 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보 제공
또한 modelAndView가 반환되는지 응답 정보도 받을 수 있음
preHandle : 컨트롤러 호출 전에 호출 (정확히는 핸들러 어댑터 호출 전에 호출)
응답값이 true : 다음으로 진행
응답값이 false : 나머지 인터셉터, 핸들러 어댑터도 호출 X
항상 호출
postHandle : 컨트롤러 호출 후에 호출 (정확히는 핸들러 어댑터 호출 후에 호출)
컨트롤러에서 예외 발생 시 해당 메소드는 호출되지 않음
aterCompletion : 뷰가 렌더링 된 이후에 호출
항상 호출
컨트롤러에서 예외 발생 시 해당 메소드에서 예외 정보를 포함하여 호출
정상 흐름일 경우 예외는 null
예외와 무관하게 공통 처리를 하는 경우 사용
인터셉터를 사용하여 요청 로그 남기기
1. 인터셉터 구현
@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);
// @RequestMapping : HandlerMethod
// 정적 리소스(/resource/static 과 같은 것 ) : ResourceHttpRequestHandler
if(handler instanceof HandlerMethod){
HandlerMethod hm = (HandlerMethod) handler;// 호출할 컨트롤러 메서드의 모든 정보가 포함
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true; // handler 호출
}
@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();
Object logId = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
if(ex != null){
log.error("afterCompletion error!!", ex);
}
}
}
서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리
따라서 preHandle에서 지정한 값을 다른 메서드로 넘기려면 request.setAttribute()를 사용하여 담아둔 후 사용
preHandle에서 return true 면 정상 호출. 다음 인터셉터나 컨트롤러가 호출
@Controller, @RequestMapping을 활용한 경우 핸들러 정보로 HandlerMethod가 반환
/resources/static 과 같은 정적 리소스가 호출되는 경 ResourceHttpRequestHandler가 반환
2. 인터셉터 적용
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()) // 인터셉터 등록
.order(1) // 순서 지정
.addPathPatterns("/**") // 적용 URL 패턴
.excludePathPatterns("/css/**","/*.ico","/error"); // 제외 URL 패턴
}
}
3. 실행
인터셉터를 사용하여 인증 체크
1. 인터셉터 구현
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("인증 체크 인터셉터 실행 {}", requestURI);
HttpSession session = request.getSession();
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
log.info("미인증 사용자 요청");
response.sendRedirect("login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
인증은 컨트롤러 호출 전에만 호출되면 되기에 preHandle만 구현
2. 인터셉터 적용
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()) // 인터셉터 등록
.order(1) // 순서 지정
.addPathPatterns("/**") // 적용 URL 패턴
.excludePathPatterns("/css/**","/*.ico","/error"); // 제외 URL 패턴
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/","/members/add","/login","/logout","/css/**","/*.ico","/error");
}
}
3. 실행
미인증 사용자
인증 사용자
PathPattern
? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처