스프링 인터셉터
스프링 인터셉터는 서블릿 필터와 비슷한 기능을 제공하지만 훨씬 더 많은 기능을 제공한다
스프링 인터셉터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
- 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 호출됨.
- 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장하게 된다. 스프링 MVC의 시작점이 디스패처 서블릿이다.
스프링 인터셉터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출X)
//비 로그인 사용
스프링 인터셉터 체인
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
이를 이용하여 예를 들어서 로그를 남기는 인터셉터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 인터셉터를 만들 수 있다.
스프링 인터셉터를 사용하려면 'HandlerInterceptor' 인터페이스를 구현하면 된다. 서블릿 필터는 'doFilter'만 제공하지만, 스프링 인터셉터는 컨트롤러 호출 전(preHandle), 호출 후 (postHandle), 요청 완료 이후 (afterCompletion)와 같이 구성된다.
스프링 인터셉터 호출 흐름
디스패처 서블릿 이후
1. 디스패처 서블릿 preHandle. preHandle의 응답값이 true이면 진행. false면 더는 진행X
2. handle(handler). 핸들러 어댑터 호출 -> 핸들러(컨트롤러) 호
3. 핸들러 어댑터가 ModelandView 반환
4. postHandle
5. render(model)호출 -> HTML 응답
6. 뷰가 렌더링된 이후에 afterCompletion 호출
핸들러에서 예외가 발생하면 postHandler는 호출이 되지 않지만, afterCompletion은 항상 호출된다. 이 경우에 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();
//afterCompletion에 uuid를 넘겨야하는데
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 requestURI = request.getRequestURI();
String logId = (String)request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler); //afterCompletion에서 종료로그를 호출해야 항상 종료 로그가 호출됨.
if(ex != null) {
log.error("afterCompletion error!!", ex); //오류는 중괄호 필요없음
}
}
}
preHandle에서 지정한 값을 postHandle이나 afterCompletion에서 사용하려면 어딘가에 담아두어야 한다. 그래서 request에 uuid를 담아둔 것이다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**") //하위에 있는 모두
.excludePathPatterns("/css/**", "/*.ico", "/error"); //제외하고
}
...
스프링의 PathPatterns
? 한 문자 일치
* 경로(/) 안에서 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
인증 체크
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
//로그인체크의 경우는 preHandle만 구현하면 됨.
@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;
}
}
@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");
}
서블릿 필터와는 달리 WebConfig에서 whitelist를 적용해주므로 인터셉터의 코드가 간결해졌다!
ArgumentResolver
애노테이션 기반의 컨트롤러는 HttpServletRequest, Model, @RequestParam, @ModelAttribute같은 애노테이션 그리고 @RequestBody, HttpEntity같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 가지고 있다. 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.
ArgumentResolver는 supportsParameter()를 호출해서 해당 파라미터를 지원하는지 체크하고, 지원하면, 컨트롤러가 필요로 하는 다양한 파라미터의 값을 생성하고, 컨트롤러를 호출하면서 값을 넘겨준다.
원래 로그인한 경우의 홈화면을 보여주는 'homeLoginV3Spring'은
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)Member loginMember, Model model)
이와같이 복잡한 파라미터를 가지고 있었다.
하지만 ArgumentResolver를 이용해 간단하게 바꿀 수 있다.
ArgumentResolver 만들기
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
//Login 애노테이션이 파라미터에 있냐
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
//getParameterType은 Member 클래스가 들어오게됨.
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;
}
//없으면 null, 있으면 멤버 반환
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
WebConfig에 애노테이션 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
...
HomeController에서 사용
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//Session에서 값을 찾아서 loginMember에 넣어줌
//세션에 회원 데이터가 없으면 home
if(loginMember == null) {
return "home";
}
//세션이 유지되면 로그인 홈으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
'Spring' 카테고리의 다른 글
API 예외 처리 (0) | 2024.01.13 |
---|---|
예외 처리, 오류 페이지 (1) | 2024.01.11 |
서블릿 필터 (0) | 2024.01.09 |
쿠키와 세션 (1) | 2024.01.08 |
검증2 - Bean Validation (0) | 2024.01.06 |