공통 관심사
- 애플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사(cross-cutting concern)라고 한다.
- 공통 관심사는 스프링의 AOP로 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
- 예) 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증해야 하는 것 (로그인 여부 체크 로직)
- 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공한다.
서블릿 필터
- 필터는 서블릿이 지원하는 수문장이다.
- 만약에 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다.
- 필터는 특정 URL 패턴에 적용할 수 있다. /* 라고 하면 모든 요청에 필터가 적용된다.
- 스프링을 사용하는 경우 서블릿은 스프링의 디스패처 서블릿을 의미한다.
- 필터를 등록할 때 FilterRegistrationBean을 사용하자. @SerletComponentScan, @WebFilter(filterNmae="", urlPattersn="") 등으로 필터 등록이 가능하지만 필터 순서를 지정할 수 없다.
🔍 필터 흐름
- HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
- 필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다.
🔍 필터 제한
- HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
- 예) 로그인 사용자
- HTTP 요청 → WAS → 필터 (적절하지 않은 요청이라 판단, 서블릿 호출x)
- 예) 비 로그인 사용자
- 필터에서 적절하지 않은 요청이라고 판단하면 거기서 끝을 낸다. 그래서 로그인 여부를 체크하기에 좋다.
🔍 필터 체인
- HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러
- 필터는 체인으로 구성될 수 있다.
- 필터를 여러개 추가 하는 형식으로 필터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() : 필터 종료 메서드. 서블릿 컨테이너가 종료될 때 호출된다.
- doFilter() 와 destroy() 메서드는 default 메서드이기에 필수로 구현하지 않아도 된다.
🔍 서블릿 필터 - 요청 로그
- 가장 단순한 필터인 모든 요청을 로그로 남기는 필터 만들기
📌 LogFilter (로그 필터)
package hello.login.web.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("REQUEST [{}] [{}]", uuid, requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}] [{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
- public class LogFilter implements Filter {}
- 필터를 사용하려면 필터 인터페이스를 구현해야 한다.
- doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- HTTP 요청이 오면 doFilter() 메서드가 호출된다.
- ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스다.
- HTTP를 사용하면 HttpServletRequest httpRequest = (HttpServletRequest) request; 처럼 다운 케스팅을 해야한다.
- String uuid = UUID.randomUUID().toString();
- HTTP 요청을 구분하기 위해 요청당 임의의 uuid를 생성한다.
- log.info("REQUEST [{}][{}]", uuid, requestURI);
- uuid와 requestURI를 출력한다.
- chain.doFilter(request, response);
- 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다.
- 만약 이 로직이 없으면 다음 단계로 진행되지 않는다.
📌 WebConfig (필터 설정)
package hello.login;
import hello.login.web.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
- 필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록한다.
- filterRegistrationBean.setFilter(Filter filter)
- 등록할 필터를 지정한다.
- filterRegistrationBean.setOrder(int order)
- 필터의 우선순위를 정한다.
- 필터는 체인으로 동작한다. 따라서 순서가 필요하다.
- int order값이 낮을수록 우선순위가 높다.
- addUrlPatterns(String... urlPatterns)
- 필터를 적용할 URL 패턴을 지정한다.
- 한 번에 여러 패턴을 지정할 수 있다.
- 모르면 URL 패턴을 검색할 것
- URL 패턴을 /* 으로 등록하면 모든 URL을 의미한다. (모든 요청에 해당 필터가 적용된다.)
🔍 서블릿 필터 - 인증 체크
- 로그인이 되지 않은 사용자는 상품 관리 페이지에 못 들어가도록 해야 한다.
📌 LoginCheckFilter (로그인 체크 필터)
package hello.login.web.filter;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@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);
// 로그인으로 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);
}
}
- private static final String[] whiteList = {"/", "/members/add", "/login", "/logout", "/css/*"};
- whiteList 배열을 선언하여 인증 필터가 적용되지 않는 url을 적어놓는다.
- 로그인이 필요없어도 접속할 수 있는 곳을 의미한다.
- isLoginCheckPath(requestURI)
- 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다.
- whiteList에 없는 url 경로일 경우 true 값이 나오도록 설정한다.
- httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
- 미인증 사용자는 로그인 화면으로 리다이렉트 한다.
- 로그인 완료 후 다시 원래 보던 페이지로 이동시키기 위해 원래 보던 페이지의 URI 주소를 로그인 페이지로 보낼 때 쿼리 파라미터로 전달한다.
- /login 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능을 추가로 개발해야한다.
📌 WebConfig (필터 설정)
package hello.login;
import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
- 로그인체크 필터는 /*으로 URL을 모두 허용해준 상태이다. 필터 내부에서 화이트리스트가 있기 때문에 따로 불필요한 검사를 해주지 않는다.
📌 컨트롤러 (로그인)
@PostMapping("/login")
public String loginV4(@Validated @ModelAttribute LoginForm form,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 로그인 성공 처리
// 세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}
- /login 컨트롤러에서 redirectURL 요청 파라미터를 추가로 요청했다. 이 값을 사용해서 로그인 성공시 해당 경로로 고객을 redirect 한다.
- 로그인이 성공했을 경우 redirectURL 이라는 @RequestParam 을 조회해 만약 다은 페이지로 접근을 시도하다가 로그인 페이지로 온 경우 다시 되돌아가기 위해 사용한다.
🧷 참고
- 필터는 스프링 인터셉터에서 제공하지 않는 기능을 가지고 있다.
- 필터는 chain.doFilter(request, response) 메서드를 통해서 다음 필터 또는 서블릿을 호출한다. 이 때 파라미터인 request 와 response를 다른 객체로 바꿔서 전달할 수 있다.
- 다른 객체를 전달하려면 ServletRequest 와 ServletResponse를 구현한 객체이어야 한다.
- 많이 쓰이는 기능은 아니지만 알고는 있자.
스프링 인터셉터
- 웹과 관련된 공통 관심사를 처리하기 위해 서블릿에는 필터가 있다면 스프링에는 인터셉터가 있다.
- 서블릿 필터는 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
- 둘 다 웹과 관련된 공통 관심사를 처리하지만, 적용 되는 순서, 범위 그리고 사용방법이 다르다.
- 스프링 인터셉터는 URL 패턴을 적용할 수 있다. 서블릿 필터의 URL 패턴과 달리 매우 정밀하게 설정 할 수 있다.
- 스프링 인터셉터는 서블릿 필터보다 편리하고 더 정교하고 다양한 기능을 지원한다.
- 서블릿 필터보다 인터셉터를 사용하는 것이 더 좋다.
🔍 스프링 인터셉터 흐름
- HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
- 스프링 인터셉터는 디스패치 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 된다.
🔍 스프링 인터셉터 제한
- HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
- 예) 로그인 사용자
- HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 (적절하지 않은 요청이라 판단, 컨트롤러 호출X)
- 예) 비 로그인 사용자
- 인터셉테에서 적절하지 않은 요청이라고 판단하면 컨트롤러를 호출하지 않고 거기서 끝낸다.
- 로그인 여부를 체크하기에 좋다.
🔍 스프링 인터셉터 체인
- HTTP 요청 → WAS → 필터 → 서블릿 → 인터셉터1 → 인터셉터2 → 인터셉터3 → 컨트롤러
- 스프링 인터셉터는 체인으로 구성되어 있다. 중간에 인터셉터를 자유롭게 추가할 수 있다.
🔍 스프링 인터셉터 인터페이스
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 {}
}
- 스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.
- 인터셉터는 컨트롤러 호출전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 단계적으로 세분화되어있다.
- 서블릿 필터는 단순히 request, response만 제공했지만, 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 받을 수 있다.
- 스프링 인터셉터는 어떤 modelAndView가 반환되는지 응답 정보를 받을 수 있다.
🔍 스프링 인터셉처 호출 흐름
💡 정상 흐름![](https://blog.kakaocdn.net/dn/beZXG5/btrvS7HDfKF/gIwL8myFRbgD9r7KPWPfWK/img.jpg)
- preHandle
- 컨트롤러가 호출되기 전에 호출된다. (정확히는 핸들러 어댑터가 호출되기 전에 호출된다.)
- preHandle의 반환타입은 boolean이다. 반환 값이 true 일 경우 다음 단계로 진행된다. false 일 경우 더 이상 진행되지 않는다.
- postHandle
- 컨트롤러 호출 후에 호출된다. (정확히는 핸들러 어댑터가 호출된 후에 호출된다.)
- afterCompletion
- 뷰가 렌더링 된 후에 호출된다.
- 정상 작동시 예외 파라미터에는 null 값이 들어온다.
💡 예외 상황
- preHandle
- 컨트롤러가 호출되기 전에 호출된다.
- postHandle
- 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않는다.
- afterCompletion
- afterCompletion은 항상 호출된다. (예외가 발생해도 호출된다.)
- 예외를 파라미터로 받을 수 있어서 예외 발생시 어떤 예외가 발생했는지 로그로 출력할 수 있다.
- 예외와 무관하게 공통 처리를 하려면 afterCompletion() 메서드에 로직을 작성해야 한다.
- 스프링 인터셉터 혹은 컨트롤러에서 예외가 발생하면 각 시점에 따라 호출 여부가 달라진다.
🔍 스프링 인터셉터 - 요청 로그
📌 LogInterceptor (요청 로그 인터셉터)
package hello.login.web.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@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 requestURI = request.getRequestURI();
String uuid = (String )request.getAttribute(LOG_ID);
log.info("REQUEST [{}] [{}] [{}]", uuid, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error", ex);
}
}
}
- String uuid = UUID.randomUUID().toString()
- 요청 로그를 구분하기 위해 uuid를 생성한다.
- request.setAttirbute(LOG_ID, uuid)
- 서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있다.
- preHandle에서 지정한 값을 postHandle, afterCompletion에서 함께 사용하기 위해 request에 담아두었다.
- 스프링 인터셉터는 WAS에 의해 싱글톤으로 관리된다. 그래서 멤버변수를 사용하면 안된다.
- request에 담아둔 값은 request.getAttribute(LOG_ID) 로 찾는다.
- return true;
- true면 정송 호출이다. 다음 인터셉터나 컨트롤러가 호출된다.
📌 핸들러 정보
// @RequestMapping : HandlerMethod
// 정적 리소스 : ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
- ResourceHttpRequestHandler
- @Controller 가 아닌 /resources/static 와 같은 정적 리소스가 호출된 경우 스프링 인터셉터의 Object handler 매개변수에 ResourceHttpRequestHandler가 핸들러 정보로 넘오온다.
📌 WebConfig (인터셉터 등록)
package hello.login;
import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import hello.login.web.interceptor.LogInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Filter;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
- 인터셉터를 등록하려면 WebMvcConfigurer 인터페이스를 구현하고 addInterceptors() 메서드를 사용한다.
- registry.addInterceptor(HandlerInterceptor interceptor)
- 인터셉터를 등록한다.
- order(int order)
- 인터셉터의 호출 순서를 지정한다. 낮을수록 우선순위가 높다.
- addPathPatterns(String... patterns)
- 인터셉터를 적용할 URL 패턴을 지정한다.
- excludePathPatterns(String... patterns)
- 인터셉터에서 제외할 URL 패턴을 지정한다.
- 서블릿 필터와 비교해보면 인터셉터는 addPathPatterns, excludePathPatterns로 매우 정밀하게 URL 패턴을 지정할 수 있다.
🧷 스프링의 URL 경로 패턴
- 스프링이 제공하는 URL 경로 패턴은 서블릿 기술이 제공하는 URL 경로 패턴과 다르다.
- 스프링이 제공하는 URL 경로 패턴이 더 자세하고 세밀하게 설정할 수 있다.
PathPattern 공식 문서
? ▶ 한 문자 일치
* ▶ 경로(/) 안에서 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
PathPattern (Spring Framework 5.3.16 API)
Representation of a parsed path pattern. Includes a chain of path elements for fast matching and accumulates computed state for quick comparison of patterns. PathPattern matches URL paths using the following rules: ? matches one character * matches zero or
docs.spring.io
🔍 스프링 인터셉터 - 인증 체크
📌 LoginCheckInterceptor (로그인 인터셉터)
package hello.login.web.interceptor;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@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;
}
}
- 인증이라는 것은 컨트롤러 호출 전에만 호출하면 된다. 따라서 preHandle만 구현하면 된다.
- 서블릿 필터와 비교해서 코드가 매우 간결하다.
📌 WebConfig (인터셉터 등록)
@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");
}
}
🧷 정리
- 서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술이다.
- 서블릿 필터와 비교해서 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다.
ArgumentResovler 활용
- 클라이언트로 받은 Request 정보에서 Session 정보를 꺼내 해당하는 세션키로 로그인 정보를 찾는 방법을 다양하게 사용할 수 있다.
🔍 기존 방식
💡 HttpServletRequest에서 직접 꺼내기
@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
// 세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
// 세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
💡 @SessionAttirbute 애노테이션 활용하기
@GetMapping("/")
public String homeLoginV4(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false)Member loginMember, Model model) {
// 세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
// 세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
- 애노테이션 기반의 매핑방식이 직접 로직을 구현하는 것보다 간결하다.
- 하지만 매번 속성(name, required)을 작성해주는 것이 번거롭고 해당 애노테이션을 통해 해당 객체에 대한 명시성이 부족하다.
🔍 ArgumentResolver 사용하기
📌 @Login
package hello.login.web.argumentresolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login { }
- @Target(ElementType.PARAMETER)
- 파라미터만 붙일 수 있는 애노테이션이다.
- @Retention(RetentionPolicy.RUNTIME)
- 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있도록 한다.
📌 LoginMemberArgumentResolver
package hello.login.web.argumentresolver;
import hello.login.domain.member.Member;
import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@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("resolverArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session==null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
- ArgumentResolver는 HandlerMethodArgumentResolver 인터페이스를 구현해야 한다.
- supportsParameter()
- 컨트롤러 호출시 매개변수들은 ArgumentResolver에 의해 매핑이 되는데, 많은 ArgumentResolver가 각각 대응할 수 있는 객체는 제한되어 있는데, 이를 책임 사슬 패턴을 이용해 처리한다.
- 각각 ArgumentResolver는 supportsParameter() 메서드를 이용해 매핑 가능 여부를 boolean 타입으로 반환한다.
- @Login 애노테이션이 붙어있으면서 Member 객체인 경우 ArgumentResolver가 사용된다.
- resolverArgument()
- 컨트롤러가 호출되기 전에 해당 메서드가 호출되어 필요한 파라미터 정보를 생성한다.
- 컨트롤러에 필요한 파라미터 정보를 생성한다.
- 여기서는 세션에서 로그인 회원 정보인 member객체를 찾아 반환한다.
- 이후 스프링 MVC에서 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member 객체를 파라미터로 전달한다.
- 컨트롤러가 호출되기 전에 해당 메서드가 호출되어 필요한 파라미터 정보를 생성한다.
📌 HomeController (홈 컨트롤러)
@GetMapping("/")
public String homeLoginV4ArgumentResolver(@Login Member loginMember, Model model) {
String result = "loginHome";
// 세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
// 세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return result;
}
- @Login 애노테이션이 있으면 직접 만든 ArgumentResolver가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고, 만약에 세션이 없다면 null을 반환하도록 개발해야한다.
📌 WebConfig (설정 추가)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
- 서블릿 필터나 스프링 인터셉터처럼 WebMvcConfigurer에 리졸버를 등록해야 한다.
🧷 정리
- 애노테이션을 활용하면 더 명시적이고 간결하게 회원정보를 찾아서 매핑해줄 수 있다.
- 만약 회원 객체(Member)의 구조가 바뀐다면 Resolver 쪽만 수정하면 된다.
- 직접 객체 매핑 로직을 구현하면 코드가 너무 길고 다른 개발자가 보기 쉽지 않다.
- @SessionAttribute 애노테이션을 활용하면 좀 나아지지만 목적 자체가 로그인 정보를 찾는다는 특정 목적의 애노테이션이 아니기 때문에 다른 개발자가 보기 힘들다.
- 하지만 ArgumentResolver를 이용해 애노테이션으로 요청 매핑 핸들러 어댑터를 구현해주면 하나의 특정 애노테이션으로 가독성도 좋고 편리하게 회원 정보를 조회할 수 있다.
- ArgumentResolver를 활용하면 공통작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.
👀 참고 자료
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의
웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있
www.inflearn.com
https://catsbi.oopy.io/9ed2ec2b-b8f3-43f7-99fa-32f69f059171
7. 로그인 처리2 - 필터, 인터셉터
목차
catsbi.oopy.io
'[ Spring ] > SpringMVC 2편' 카테고리의 다른 글
[Spring] API 예외 처리 (0) | 2022.03.15 |
---|---|
[Spring] 예외 처리와 오류 페이지 (0) | 2022.03.15 |
[Spring] 로그인 처리1 - 쿠키, 세션 (0) | 2022.03.12 |
[Spring] 검증2 - Bean Validation (0) | 2022.03.11 |
[Spring] 검증1 - Validation (0) | 2022.03.07 |