공통 관심사 (cross-cutting concern)
: 애플리케이션 여러 로직에서 공통으로 관심이 있는 것
웹과 관련된 공통 관심사를 처리할 때는 HTTP 헤더나 URL 정보가 필요한데,
서블릿 필터와 스프링 인터셉터는 HttpServletRequest를 제공한다
: 서블릿이 지원하는 수문장
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
특정 URL 패턴에 적용 가능
ex. 모든 요청에 적용시 /*
스프링을 사용하는 경우 서블릿은 디스패처 서블릿
필터에서 제한이 걸리면 바로 끝낼 수 있다
ex. 로그인 여부 체크
체인으로 구성되며 자유롭게 추가 가능
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()
ServletRequest request : HTTP 요청이 아닌 경우까지 고려, HTTP 사용시 다운캐스팅하여 사용FilterChain chain : 다음 단계로 진행destroy()
FilterRegistrationBean을 사용하여 등록setFilter(new LogFilter()) : 등록할 필터를 지정
setOrder(1) : 필터는 체인으로 동작하므로 순서 지정. 낮을 수록 먼저 동작
addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정. 한번에 여러 패턴을 지정 가능
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
@Slf4j
public class LoginCheckFilter implements Filter {
//whitelist를 제외한 모든 경로에 인증 체크 로직 적용
private static final String[] whitelist = {"/", "/members/add", "/login","/logout","/css/*"};
@Override
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=" + requestURI);
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);
}
}
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);: 스프링 MVC가 제공하는 기술
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출
URL 패턴 적용 가능
(서블릿 URL 패턴과는 다르고, 매우 정밀하게 설정 가능)
필터에서 제한이 걸리면 바로 끝낼 수 있다
ex. 로그인 여부 체크
체인으로 구성되며 자유롭게 추가 가능
스프링의 인터셉터를 사용하려면 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() 하나만 제공
인터셉터 : 컨트롤러 호출 전 (preHandle) , 호출 후 (postHandle) , 요청 완료 이후 (afterCompletion) 와 같이 단계적으로 세분화
서블릿 필터 : request , response 만 제공
인터셉터 : 호출되는 컨트롤러(handler) 정보 제공, 어떤 modelAndView 가 반환되는지 응답 정보 제공
필터 vs 인터셉터 ?
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공
스프링 MVC를 사용하며, 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리
HandlerMethod : @Controller, @RequestMapping 사용 시
ResourceHttpRequestHandler : /resources/static 와 같은 정적 리소스 사용 시


postHandle : 컨트롤러에서 예외가 발생하면 호출되지 않음
afterCompletion : 예외 발생시에도 항상 호출, 예외(ex)를 파라미터로 받아 로그 출력 가능
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()) //인터셉터 등록
.order(1) //호출 순서 지정
.addPathPatterns("/**") //적용 URL 패턴 지정
.excludePathPatterns("/css/**", "/*.ico", "/error"); //제외 패턴 지정
}
//...
}
addInterceptors()를 사용하여 인터셉터 등록🔎 PathPattern 공식 문서
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html
@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;
}
}
@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");
}
}
해당 기능을 사용하여 로그인 회원을 편리하게 찾을 수 있다
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
@Login 애노테이션을 생성하여
애노테이션이 있으면 ArgumentResolver가 동작해서 자동으로 세션에 있는 로그인
회원을 찾아주고, 없다면 null 을 반환하도록 개발
@Target(ElementType.PARAMETER) //파라미터에만 사용
@Retention(RetentionPolicy.RUNTIME) //런타임까지 애노테이션 정보가 남아있음 -> 리플렉션 등을 활용 가능
public @interface Login {
}
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
//파라미터에 Login 애노테이션이 있는지 확인
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
//파라미터 타입이 Member인지 확인
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType; //둘 다 true => resolveArgument 실행
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
Object member = session.getAttribute(SessionConst.LOGIN_MEMBER);
return member;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
//...
}
ArgumentResolver를 활용하면 공통 작업 시, 컨트롤러를 더욱 편리하게 사용 가능