스프링에서 제공하는 인터셉터는 서블릿에서 제공하는 필터와 거의 비슷한 기능을 합니다.
모든 컨트롤러에서 공통으로 처리해야 할 로직을 각 컨트롤러마다 작성하는 것은 매우 비효율 적일 뿐만 아니라, 이와 관련된 로직이 변경될 경우 모든 컨트롤러에 손을 대야하는 불편함이 있습니다.
이러한 공통 관심사를 한거번에 처리하기 위해, 컨트롤러 로직이 실행되기 전 실행되는 것이 바로 인터셉터입니다.
인터셉터는 다음과 같은 흐름을 가지고있습니다.
인터셉터는 아래와 같이 HandlerInterceptor 인터페이스를 구현하여 만듭니다.
public class TempInterceptro implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
doFilter() 하나만 제공하는 필터에 비해서 preHandler(호출전), postHandler(호출후)와 같이 단계적으로 잘 세분화 되어있습니다.
또한 request, response외에도 handler(어떤 컨트롤러가 호출될 지)정보 그리고 ModelAndView정보도 받아볼 수 있습니다.
preHandle : 컨트롤러(좀 더 정확히 핸들러 어댑터)호출 전에 호출된다.
preHandle의 응답값이 true이면 다음으로 진행.
postHandle : 컨트롤러(좀 더 정확히는 핸들러 어댑터) 호출 후에 호출된다.
afterCompletion : 뷰가 렌더링 된 이후에 호출된다.
컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않고 WAS로 예외가 전달됩니다.
afterCompletion은 예외가 발생하더라도 그 정보를 담아 호출됩니다.
모든 요청에 로그를 찍는 인터셉터.
package hello.login.web.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@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();
// LogInterceptor는 싱글톤으로 관리되기 때문에 uuid를 afterCompletion으로 넘길 때 멤버변수로 넘기면 안된다.
request.setAttribute(LOG_ID, uuid);
// @RequsetMapping: HanlderMethod
// 정적 리소스: ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; // 호출할 핸들러의 모든 메서드 정보가 담겨있다.
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandler [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String uuid = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
인터셉터는 필터와 달리 preHandle, postHandle, afterCompletion 세가지 메서드를 제공하기 때문에 로그를 찍을 때 어떤 요청에서 비롯된 메서드인지 Id를 지정해 줄 필요가 있습니다.
여기서는 UUID임의값을 만들어 넣어주었습니다.
인터셉터는 WAS에서 싱글톤으로 관리되기 때문에 멤버변수를 사용할 수 없습니다.
따라서 UUID정보를 넘길 때 request에 담아 두었습니다,
종료 LOG는 afterCompletion에 담아두었다. 그 이유는 예외가 터지면 postHandle이 호출되지 않기 때문입니다.
핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라집니다.
HandlerMethod : @Controller, @RequestMapping
ResourceHttpRequestHandler : 정적 리소스가 호출 되는 경우.
import hello.login.web.argumentresolver.LoginMemberArgumentResolver;
import hello.login.web.interceptor.LogInterceptor;
import hello.login.web.interceptor.LoginCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfigInterceptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/*", "/*.ico", "/error"); //얘들은 뺌
}
}
WebMvcConfigure가 제공하는 addInterceptors를 사용해 인터셉터를 등록할 수 있습니다.
registry.addInterceptor(new LogInterceptor()) : 인터셉터 등록
order(1) : 인터셉터 체인의 순서를 지정. (낮을수록 빨리 호출)
addPathPatterns("/**" ) : 인터셉터를 적용할 URL 패턴 지정,
excludePathPatterns("/css/", "/.ico", "/error") : 인터셉터에서 제외할 URL패턴 지정.
서블릿 필터가 제공하는 URL 경로와 다른 패턴이다.
PathPattern 공식 문서.
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html
package hello.login.web.interceptor;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@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("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
로그인 유무 체크는 컨트롤러 호출 전에만 확인하면 되기 때문에 preHandle만 구현해 주었다.
package hello.login;
import hello.login.web.argumentresolver.LoginMemberArgumentResolver;
import hello.login.web.interceptor.LogInterceptor;
import hello.login.web.interceptor.LoginCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfigInterceptor implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/*", "/*.ico", "/error"); //얘들은 뺌
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/members/add", "/login", "/logout",
"/css", "/*.ico", "/error");
}
}
비로그인 회원도 홈, 로그인화면등에 접근할 수 있도록 excludePathPatterns를 지정해 주었습니다.