1. 스프링 인터셉터의 특징과 흐름
스프링 인터셉터는 스프링 MVC가 제공하는 기술이다. 서블릿 인터셉터와 마찬가지로 웹의 공통 관심 사항을 처리하지만, 좀 더 정밀하게 설정이 가능하다. 인터셉터의 적용 순서는 다음과 같다.
HTTP 요청 -> WAS -> 필터 -> 서블릿(DispatcherServlet) -> 스프링 인터셉터1 ->
스프링 인터셉터 2-> ... -> 컨트롤러
서블릿 필터에 비해서, request, response만 받는 것이 아니라 어떤 컨트롤러(handler)가 호출되는지 호출 받을 수 있고, modelAndView도 받을 수 있다. 그리고 preHandle, postHandle, afterCompletion 메서드 순으로 흐름이 진행된다. 구조 자체가 스프링 MVC에 특화되어 있으므로, 스프링 MVC를 이용하는 경우 인터셉터를 사용하는 것이 좋다.
각 메서드의 특징은 다음과 같다.
- preHandle : boolean을 반환한다. 핸들러 호출 전에 호출되어 true이면 다음 인터셉터를 호출하고, 정상적인 흐름일 시에 핸들러를 호출한다. false이면 중단한다.
- postHandle : 핸들러에서 예외 발생 시, 예외 사항은 DispatcherServlet에 전달되지만, postHandle 메서드는 호출되지 않는다.
- afterCompletion : 항상 호출되며, 예외 발생 시 'ex'를 파라미터로 받아서 예외를 로그로 출력할 수 있다.
2. 로그 필터 개발
서블릿 필터로 했던 방식과 똑같이, 스프링 인터셉터로 로그 필터를 개발해본다.
로그 인터셉터 : HandlerInterceptor
바로 인터셉터 파일을 작성해본다. LogInterceptor 파일을 작성하고 HandlerInterceptor를 implements 받는다. 이후 각 메서드를 Override 한다.
setAttribute활용
URI와 uuid를 로그로 남기기 위해 값을 가져온다. 그리고 request.setAttribute에 담아준다. 이렇게 하는 이유는, 만약 예외가 발생하는 경우 postHandler이 호출되지 않아서 afterCompletion에서 따로 requestURI와 uuid를 받아와야 하기 때문이다. 이를 위해 전역 변수로 URI, uuid를 얻어오면 안된다. 싱글톤 형태로 LogInterceptor를 사용할 것이기 때문에 공유 변수를 만들어버리기 때문이다. 그래서, request.setAttribute를 활용한다.
HandlerMethod 지정
컨트롤러로 유연하게 받아온 handler를 HandlerMethod로 캐스팅할 수 있다. 이렇게 선언된 HandlerMethod에 get... 메서드들로 핸들러에 포함된 모든 정보를 조회할 수 있다.
modelAndView
postHandler에 지정된 파라미터로 modelAndView를 받아올 수 있다. 로그 출력 화면에서 확인해보겠지만, 여기서 view와 model 객체가 전부 담긴다.
상수 선언 단축키
LOG_ID는 여러 군데에서 같은 값으로 사용하기 위해서 static final로 선언하였다. 이 부분은 "logId"라고 되있는 곳에 커서를 둔 채 [Ctrl + Alt + C] 단축키를 통해서 상수로 선언할 수 있다.
java/hello/login/web/interceptor/LogInterceptor.java
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("postHandler [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
Object logId = request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
|
cs |
빈 등록 : WebMvcConfigurer
서블릿 필터에서와 마찬가지로 설정 정보인 WebConfig 클래스에 인터셉터를 위한 빈을 등록한다. 다만 여기서는 WebMvcConfigurer를 implements 받아오도록 한다.
체이닝 문법
문법을 외울 필요는 없다. 파라미터로 받아온 registry에 addInterceptor()로 지정한 인터셉터를 불러온다. 그리고 .order, .addPathPAtterns, .excludePathPatterns 메서드를 체이닝 형식으로 불러온다. addPathPatterns는 필터를 적용할 urlPath들을 지정하고, .excludePathPatterns는 필터를 적용하지 않을 urlPath들을 지정한다. urlPath 작성 규칙은 아래 스프링 공식문서에서 확인이 가능하다.
java/hello/login/web/login/WebConfig.java
1
2
3
4
5
6
7
8
9
10
|
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("css/**", "/*.ico", "/error");
}
|
cs |
스프링 : PathPatterns 공식문서
로그 출력 결과
출력 결과를 살펴보자.
우선 맨 처음 설명했던대로 필터 -> 서블릿 -> 인터셉터 순으로 로그가 출력되는 것을 확인할 수 있다. 그리고 ModelAndView 객체가 view, model 객체로 분리되어 postHandler 부분에서 출력되는 부분도 확인할 수 있다.
3. 인증 체크 인터셉터 개발
인터셉터 작성
서블릿 필터로 작성했던 방식과 비슷하게 session 검증을 하면 된다. HandlerInterceptor를 상속받고, default 메서드를 제외한 preHandle 메서드를 Override 한다. 코드가 서블릿 필터에 비해 간결하다. HttpServletRequest, -Response로 캐스팅 하는 부분도 없고, try-catch-finally 구문도 사용하지 않는다. 그리고 어떤 urlPath를 허용/비허용 할지를 작성하는 부분도 설정쪽으로 넘긴다. 서블릿 필터와 스프링 인터셉터는 기본 개념은 같지만, 바로 이런 개발자 편의성 때문에 보통 스프링 인터셉터를 많이 사용한다.
java/hello/login/web/interceptor/LoginCheckInterceptor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@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;
}
}
|
cs |
인터셉터 추가
설정 정보에 인터셉터를 추가한다. 인터셉터 적용은 모든 path("/**")에 하되, 로그인이 안됬을 때도 접근이 필요한 path는 .excludePathPatterns에 등록해준다.
java/hello/login/web/login/WebConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@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");
}
|
cs |
4. ArgumentResolver 적용
스프링 MVC 1편에서 요청이 핸들러에 접근하기 전에 메시지 컨터버로 작용하는 것이 ArgumentResolver라고 배웠다. 따라서 ArgumentResolver를 활용하여 사용자의 로그인 정보를 확인할 수 있는 방법이 있다.
컨트롤러 수정
HomeController에 homeLoginV3Spring 대신 homeLoginV3ArgumentResolver를 만든다. @SessionAttribute를 적용하는 대신 @Login 어노테이션을 작성하고 직접 어노테이션을 만든다.
java/hello/login/web/HomeController.java
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
|
// @GetMapping("/")
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
|
cs |
@Login 어노테이션 적용
@Login 어노테이션을 작성한다. 여기서 @Target 어노테이션은 어떤 타입에 적용가능한 어노테이션인지를 정의한다. 이번에는 파라미터에 적용할 것이므로 ElementType.PARAMETER로 지정해준다. @Retention 어노테이션은 런타임까지 어노테이션 정보가 남아있도록 해준다. 리플렉션 등을 활용할 수 있도록 한다고 하는데, 나중에 스프링 전반에 대한 이해도가 있어야 정확히 이해가 될 것 같다.
java/hello/login/web/argumentresolver/Login.java
1
2
3
4
|
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
|
cs |
ArgumentResolver 작성
이제 객체의 정보와 세션 정보를 검사하는 ArgumentResolver 파일을 만든다. 메서드와 파라미터 등이 모르는 문구가 많아서 완전한 이해가 어렵다. 이런 프로세스가 있다는 것 정도만 이해해야할 것 같다.
HandlerMethodArgumentResolver를 상속받는다. 여기서 supportsParameter, resolveArgument를 Override 할 수 있다.
supportsParameter 메서드에서는 파라미터를 받아와서 해당 파라미터가 Login.class 어노테이션을 갖고 있는지 검사한다. 다시 말해, HomeController의 Member 타입 파라미터 앞에 @Login 어노테이션이 있는지 검사한다는 말이다. 그리고, isAssignableFrom 메서드로 해당 파라미터가 Member.class를 상속받은 클래스인지 검사한다.
resolveArgument 메서드에서는 supportsParameter를 통과한 경우에 대해 요청 정보를 처리한다. NativeRequest 정보를 캐스팅하여 HttpServletRequest로 바꾸고, session을 검사하여 세션이 없으면 null을, 있으면 세션의 속성값인 쿠키를 들고오게 한다.
java/hello/login/web/argumentresolver/LoginMemberArgumentResolver.java
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
|
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasParameterAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasParameterAnnotation && 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();
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
|
cs |
빈 등록
마지막으로 빈 등록을 한다. WebConfig 파일에 addArgumentResolver 메서드를 Override 해주면 된다.
java/hello/login/web/login/WebConfig.java
1
2
3
4
5
6
7
|
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
|
cs |
ArgumentResolver를 활용하여 필터 기능을 구현할 수 있다는 것을 알아보았다. 결국 기본 원리는 서블릿 전후로 필터나 인터셉터, 메시지 컨버터 등에서 컨트롤러(핸들러) 전에 요청 정보를 판단한다는 것이다. 그리고 세션 정보를 얻어오는 연습도 해봤다. 로그인 처리는 이 정도로만 마무리 한다. 향후 Spring Security 등을 이해하는데 도움이 될 것이다.
참조
1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC-2] 15. 예외 - 인터셉터와 스프링 부트의 오류 페이지 (0) | 2021.09.08 |
---|---|
[스프링 웹MVC-2] 14. 예외 - 서블릿 오류 페이지 (0) | 2021.09.06 |
[스프링 웹MVC-2] 12. 로그인 - 서블릿 필터 (0) | 2021.09.01 |
[스프링 웹MVC-2] 11. 로그인 - 세션 활용 (0) | 2021.08.29 |
[스프링 웹MVC-2] 10. 로그인 - 기본 기능, 쿠키 적용 (0) | 2021.08.29 |