스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - (41) 스프링 인터셉터 - 요청 로그, 로그인 체크
인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술편을 학습하고 정리한 내용 입니다.
요청 로그 인터셉터
요청 로그 필터 를 구현한 것처럼 인터셉터 로 구현해보자.

다음과 같이 web.interceptor패키지에 만들었다.
hello.login.web.interceptor.LogInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@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;
}
@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 uuid = (String)request.getAttribute(LOG_ID);
String requestURI = request.getRequestURI();
log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error", ex);
}
}
}
HttpServletRequest request: 필터는ServletRequest servletRequest를 캐스팅 해서 사용했지만 , 인터셉터는HttpServletRequest를 기본으로 제request.setAttribute(LOG_ID, uuid)- 서블릿 필터에서는 try~catch~finaly를 사용해서 메서드 실행 전, 후 로그를 남겼다.
- 하지만 인터셉터는 호출 시점이 완전히 분리 되어있다.
- 따라서
preHandle에 지정한 값을postHandle,afterCompletion에서 함께 사용하려면 어딘가담아두어야 한다. LogInterceptor도 싱글톤 처럼 사용되기 때문에 멤버 변수를 사용하면 위험하다.- 따라서
request에 담아두고afterCompletion에서request.getAttribute(LOG_ID)로 찾아서 사용한다.
return truetrue면 정상 호출. 다음 인터셉터나, 컨트롤러가 호출된다.false면 인터셉터에서 끝나버린다.
1
2
3
4
5
// @RequestMapping : HandlerMethod
// 정적 리소스 : ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
}
HandlerMethod
핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다. 스프링을 사용하면 일반적으로
@Controller, @RequestMapping을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod가 넘어온다.
ResourceHttpRequestHandler
@Controller가 아니라 /resources/static와 같이 정적 리소스가 호출 되는 경우
ResourceHttpRequestHandler가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리가 필요하다.
postHandle, afterCompletion
종료 로그를 postHandle이 아니라 afterCompletion에서 실행하는 이유는, 예외가 발생하는 경우 postHandle은 호출되지 않기 때문. afterCompletion은 예외가 발생해도 호출 되는 것을 보장한다.
로그 인터셉터 등록
필터를 등록했던 WebConfig에서 똑같이 인터셉터를 등록해 보자.
먼저 WebMvcConfigurer를 implements 한다.
hello.login.WebConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
//@Bean
public FilterRegistrationBean<Filter> logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1); // 필터 순서
filterRegistrationBean.addUrlPatterns("/*"); // 모든 URL
return filterRegistrationBean;
}
}
인터셉터와 필터가 중복되지 않도록 필터를 등록하기 위한 logFilter() 의 @Bean은 주석 처리 했다.
WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 인터셉터를 등록한다.
registry.addInterceptor(new LogInterceptor()): 인터셉터를 등록한다.order(1): 인터셉터의 호출 순서를 지정, 낮을 수록 먼저addPathPatterns("/**"): 인터셉터를 적용할 URL (필터에선 전체는 :/*)excludePathPatterns("/css/**", "/*.ico", "/error"): 인터셉터에서 제외할 패턴을 지정.
필터에서는 먼저 /*로 적용한 다음에 직접 필터 안에서 화이트 리스트로 제외를 했다면,
인터셉터에서는 등록 할 때 addPathPatterns, excludePathPatterns를 사용해서 매우 정밀하게 URL 패턴을 지정할 수 있다.

스프링이 제공하는 URL 경로는 서블릿 기술이 제공하는 URL 경로와 완전히 다르다. 더욱 자세하고, 세밀하게 설정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
? 한 문자 일치
* 경로(/) 안에서 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
공식 문서 링크
인증 체크 인터셉터
이제 로그인 인증 체크를 하는 인터셉터를 구현해 보자.
hello.login.web.interceptor.LoginCheckInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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;
}
}
서블릿 필터 코드와 비교해 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String requestURI = request.getRequestURI();
HttpServletResponse response = (HttpServletResponse) servletResponse;
try {
log.info("인증 체크 필터 시작 {}", requestURI);
if (isLoginCheckPath(requestURI)) {
log.info("인증 체크 로직 실행 {}", requestURI);
HttpSession session = request.getSession();
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청 {}", requestURI);
// 로그인페이지로
redirect response.sendRedirect("/login?redirectURL="+requestURI);
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 로직 종료 {}", requestURI);
}
}
/**
* 화이트리스트인 경우 인증 체크 X
* */
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
일단 매우 간결해 졌다. 인증이라는 것은 컨트롤러 호출 전에만 호출 되면 된다.
따라서 preHandle만 구현하면 된다. 그리고 화이트 리스트에 경우도, 등록 때 하면 되기 때문에 코드가 생략 됐다.
로그인 체크 인터셉터 등록
WebConfig에 addInterceptors에 추가로 등록하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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");
}
//@Bean
public FilterRegistrationBean<Filter> loginCheckFilter() {
...
}
인터셉터와 필터가 중복되지 않도록 필터를 등록하기 위한 loginCheckFilter()의 @Bean은 주석처리하자.
인터셉터를 적용하거나 하지 않을 부분은 addPathPatterns, excludePathPatterns에 작성하면 된다.
기본적으로 모든 경로(/**)에 해당 인터셉터를 적용하되 홈(/), 회원가입(/members/add), 리소스, 오류 등 로그인 필터를 적용하지 않는다.
서블릿 필터와 비교하면 매우 편리해 졌다.

로그인 하지 않고 /items에 접근하니 잘 걸러졌다.

로그에서도 잘 나오는 것을 볼 수 있다.
정리
서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술이다.
서블릿 필터와 비교해서 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다는 것을 코드로 이해 했다.
특별한 문제가 없다면 인터셉터를 사용하는 것이 좋다.
댓글남기기