필터
필터는 서블릿이 지원하는 수문장과 같다.
공통 관심사를 처리하기 위함이다.
로그인 페이지를 제외한 모든 페이지는 로그인한 사용자만 볼 수 있다.
요청마다 로그인 여부를 체크하는건 굉장히 번거롭고, 변경시 수정사항이 많다.
이러한 경우에 필터를 이용할 수 있다.
필터의 흐름
여기서 말하는 서블릿은 스프링의 디스패처 서블릿으로 생각하면 된다.
HTTP 요청 ->WAS-> 필터 -> 서블릿 -> 컨트롤러
필터 제한
필터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있다. -> 로그인 여부를 체크하기에 딱 좋다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자
필터 체인
필터는 체인으로 구성되는데, 중간에 필터를 자유롭게 추가할 수 있다.
로그를 남기는 필터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.
HTTP 요청 ->WAS-> 필터1-> 필터2-> 필터3-> 서블릿 -> 컨트롤러
필터 인터페이스는 아래와 같다.
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.
init(): 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
doFilter(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
필터 구현
@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, requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
HTTP 요청이 오면 doFilter 가 호출된다.
ServletRequest request 는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. -> 다운 케스팅이 필요하다.
chain.doFilter(request, response);
이부분이 가장 중요하다.
다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다.
만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.
WebConfig 설정
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
} }
스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록하면 된다. -> 다른 방법도 있다.
setFilter(new LogFilter()) -> 등록할 필터를 지정한다. -> 위에서 구현한 LogFilter를 등록했다.
setOrder(1) -> 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
addUrlPatterns("/*") -> 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.
서블릿 필터를 통한 인증 체크
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
log.info("인증 체크 필터 시작 {}", requestURI);
if (isLoginCheckPath(requestURI)) {
log.info("인증 체크 로직 실행 {}", requestURI); HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청 {}", requestURI);
//로그인으로 redirect httpResponse.sendRedirect("/login?redirectURL=" +
return; //여기가 중요, 미인증 사용자는 다음으로 진행하지 않고 끝! }
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e; //예외 로깅 가능 하지만, 톰캣까지 예외를 보내주어야 함 } finally {
log.info("인증 체크 필터 종료 {}", requestURI); }
}
/**
* 화이트 리스트의 경우 인증 체크X
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
}
whitelist = {"/", "/members/add", "/login", "/logout","/css/*"}; -> 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 접근할 수 있어야 한다.
httpResponse.sendRedirect("/login?redirectURL=" + requestURI); -> 로그인 후, 요청한 화면으로 다시 돌아간다.
WebConfig 에 filter를 추가해준다.
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new
FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
인터셉터
스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.
서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다.
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다.
특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다.
스프링 인터셉터 흐름
HTTP 요청 ->WAS-> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 된다.
스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장하게 된다.
매우 정밀하게 설정할 수 있다.
스프링 인터셉터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자
인터셉터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있다. -> 로그인 여부를 체크하기에 딱 좋다.
스프링 인터셉터 체인
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
스프링 인터셉터는 체인으로 구성되는데, 중간에 인터셉터를 자유롭게 추가할 수 있다.
스프링 인터셉터 인터페이스
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,Exception {}
Object handler, @Nullable Exception ex) throws Exception {}
}
인터셉터는 컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있다.
스프링 인터셉터 흐름
preHandle : 컨트롤러 호출 전에 호출된다.
preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다.
false 인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다.
postHandle : 컨트롤러 호출 후에 호출된다.
더 정확히는 핸들러 어댑터 호출 후에 호출된다.
afterCompletion : 뷰가 렌더링 된 이후에 호출된다.
스프링 인터셉터 예외 상황
preHandle : 컨트롤러 호출 전에 호출된다.
postHandle : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
afterCompletion : afterCompletion 은 항상 호출된다.
이 경우 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.
예외가 발생하면 postHandle() 는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 을 사용해야 한다.
인터셉터 구현 -> 요청로그 구현
인터셉터를 이용해서 요청시 로그를 남기는 로직을 아래와 같이 구현할 수 있다.
@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
//정적 리소스: ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
//호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
HandlerMethod hm = (HandlerMethod) handler;
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true; //false 진행X
}
@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, requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
request.setAttribute(LOG_ID, uuid)
서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있다.
preHandle 에서 지정한 값을 postHandle , afterCompletion 에서 함께 사용하려면 어딘가에 담아두어야 한다.
LogInterceptor 도 싱글톤 처럼 사용되기 때문에 맴버변수를 사용하면 위험하다. 따라서 request 에 담아두었다.
request에 저장된 값은 afterCompletion 에서 request.getAttribute(LOG_ID) 로 찾아서 사용한다.
HandlerMethod
핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다.
스프링을 사용하면 일반적으로 @Controller , @RequestMapping 을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod 가 넘어온다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
}
}
addInterceptors() 를 사용해서 인터셉터를 등록한다.
addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정한다.
"/**"이므로 모든 URI에 인터셉터를 적용하겠다. 라는 뜻이다.
excludePathPatterns("/css/*", "/ .ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.
필터와 비교해보면 인터셉터는 addPathPatterns , excludePathPatterns 로 매우 정밀하게 URL 패턴을 지정할 수 있다.
인터셉터 구현 -> 인증 체크
인증 체크를 인터셉터로 구현하면 아래와 같이 구현할 수 있다.
@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(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
서블릿 필터와 비교해서 코드가 매우 간결하다. 인증이라는 것은 컨트롤러 호출 전에만 호출되면 된다. 따라서 preHandle 만 구현하면 된다.
webConfig 설정
@Configuration
public class WebConfig 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"
);
}
}
인터셉터를 적용하지 않을 URL을 excludePathPatterns 를 통해서 다음과 같이 지정해 줬다.
PathPattern 공식문서
? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring" {spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처
/pages/t?st.html — matches /pages/test.html, /pages/tXst.html but not /pages/
toast.html
/resources/*.png — matches all .png files in the resources directory
/resources/** — matches all files underneath the /resources/ path, including /
resources/image.png and /resources/css/spring.css
/resources/{*path} — matches all files underneath the /resources/ path and
captures their relative path in a variable named "path"; /resources/image.png
will match with "path" → "/image.png", and /resources/css/spring.css will match
with "path" → "/css/spring.css"
/resources/{filename:\\w+}.dat will match /resources/spring.dat and assign the
value "spring" to the filename variable
REFERENCES