어플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사
라고 한다.
AOP로도 해결할 수 있지만 웹과 관련한 공통 관심사 처리는 HTTP 헤더, URL 정보 등이 필요하므로 HttpServletRequest
를 제공하는 서블릿 필터
or 스프링 인터셉터
를 사용하자!
HTTP 요청 -> WAS -> 필터 -> 서블릿(디스패처 서블릿) -> 컨트롤러
참고로 필터는 특정 URL 패턴에 적용할 수 있다.
ex) /*
: 모든 요청에 필터 적용
로그인 유저: 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()
: 필터 종료 메서드로 서블릿 컨테이너가 종료될 때 호출
chain.doFilter(request, response)
FilterRegistrationBean
을 사용한다.
setFilter(new LogFilter())
: 등록할 필터(LogFilter)를 지정setOrder(1)
: 필터는 체인이라 순서 필요, 낮을 수록 먼저 동작addUrlPatterns("/*")
: 필터를 적용할 URL 패턴 지정, 한 번에 여러 패턴도 지정 가능참고
@ServletComponentScan
+
@WebFilter(filterName = "logFilter", urlPatterns = "/*")
는 필터 등록은 되는데 순서 조절 불가능- 같은 요청 로그에 같은 식별자 남기는 법:
logback mdc
@Slf4j
public class LoginCheckFilter implements Filter {
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);
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return;
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
}
}
whiteList = {...}
isLoginCheckPath()
httpResponse.sendRedirect("/login?redirectURL=" + requestURI)
return
서블릿 필터는 서블릿이 제공, 인터셉터는 스프링 MVC가 제공한다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
로그인 유저: 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, Object handler, @Nullable Exception ex) throws Exception {}
}
preHandle
true
면 다음으로 진행 false
면 다음 인터셉터 or 핸들러어댑터로 진행 XpostHandle
afterCompletion
예외 발생 시
postHandle()
은 호출되지 않음afterCompletion()
은 항상 호출 + 예외 정보도 포함되어 호출됨afterCompletion()
사용 request.setAttribute(LOG_ID, uuid)
preHandle
에서 지정한 UUID를 postHandle, afterCompletion
에서 사용하기 위해 담아줌return true
: 다음 인터셉터 or 컨트롤러 호출afterCompletion
에서 실행@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 ...)
: 인터셉터를 등록order(1)
: 인터셉터 호출 순서addPathPatterns("/**")
: 인터셉터 적용할 URL 패턴excludePathPatterns(...)
: 인터셉터 제외할 패턴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("미인증 사용자 요청");
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");
}
}
@Login
이 있으면 ArgumentResolver가 동작하여
자동으로 세션에서 로그인 회원을 찾고, 없으면 null을 반환
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@Target(ElementType.PARAMETER)
: 파라미터에만 사용@Retention(RetentionPolicy.RUNTIME)
: 리플렉션 등을 사용할 수 있도록 런타임까지 어노테이션 정보가 남아있음HandlerMethodArgumentResolver
를 구현해보자.
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@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;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
supportsParameter()
: @Login
어노테이션이 있으면서 Member
타입이면 해당 ArgumentResolver
사용resolverArgument()
: 컨트롤러 호출 직전 파라미터 정보를 생성, 세션에 있는 member
객체를 찾아 반환하고 컨트롤러 메서드를 호출하면서 파라미터로 전달된다.@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
ArgumentResolver
를 활용하여 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용 가능!