김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요
Session을 이용해 로그인 한 사용자만, 상품 관리 페이지에 들어갈 수 있게 잘 만들었다공통관심사 : 애플리케이션 여러 로직에서, 공통으로 관심이 있는 것AOP로???필터나, 인터셉터로 하자!AOP에는 없는 HTTP헤더, URL정보들이
필터,인터셉터에는 들어있기 때문에,웹과 관련된공통 관심사는서블릿 필터나스프링 인터셉터를 이용하자
필터와, 인터페이스 모두
인터페이스이므로 구현해서 사용해야 한다
필터 : 서블릿이 지원하는 수문장Filter인터페이스를 구현해서 사용해야 한다DispatchServlet이다필터 인터페이스
init() : 싱글톤 객체 생성doFilter() : 고객 요청 -> 필터 로직destory() : 컨테이너 종료 -> 필터도 종료모든 기능에 로그를 찍는 코드를 구현 해보자
@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");
}
}
Filter를 구현한 LogFilter라는 파일을 만든다init, doFilter, destroy메서드를 오버라이드 한다doFilter을 보자!!!httpServletRequest의 부모인 ServletRequest를 파라미터로 받는다httpServletRequest를 사용하기 때문에, downCasting을 해준다randomUUID를 이용해, 임의의 토큰 아이디를 생성하고chain.doFilter(request,respone)을 호출해, 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다response로그도 찍어준다WebConfig - 필터 설정
setFilter(new LogFilter()) : 등록할 필터를 지정한다.setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.Filter는 servlet이 지원하기 때문에, 스프링에서 사용하려면 @Bean 등록은 해줘야 한다@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
@Slf4j
public class LoginCheckFilter implements Filter {
private 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_NUMBER)==null){
log.info("미인증 사용자 요청 {}", requestURI);
// 로그인으로 redirect
httpResponse.sendRedirect("/login?redirectURL="+requestURI);
// 현재 페이지의 URI를 포함해서, 로그인이성공하면 다시 이 페이지로 리다이렉트를 해준다
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);
// 하이트리스트에 안드는거 false
}
}
HttpServletRequest, HttpServletResponse로 다운 캐스팅해준다HttpSession session = httpRequest.getSession(false)
getSession 가져온다세션쿠키로 저장되지 않아서,,, 세션이 없어 ->null이라면???httpResponse.sendRedirect("/login?redirectURL="+requestURI);
sendRedirect를 이용해 리다이렉트 해버린다chain.doFilter(request,response);
doFilter해주기finally로 인증필터 종료 log찍어주기logback mdc로 같은 요청이면 로그에 같은 식별자로 나오게 하는 방법도 있다whitelist로 검사를 안할 페이지를 정해주고, requestURI와 PatternMatchUtils.simpleMatch를 통해, 로그인 로직을 검사해준다!!!WebConfig - loginCheckFilter() 추가
@Bean
public FilterRegistrationBean loginCheckFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
이제 로그인 없이, 로그인이 필요한 페이지에 접근하면 ->
필터 공통관심사처리에 의해서 -> 다시 로그인 페이지로sendRedirect된다.

doFilter()와 다르게 인터셉터는 호출전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)로 분화되있다Handler정보와 modelAndView까지 응답 정보로 받을 수 있다
preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.)postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)afterCompletion : 뷰가 렌더링 된 이후에 호출된다.
preHandle : 컨트롤러 호출 전에 호출된다.postHandle : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.afterCompletion : afterCompletion 은 항상 호출된다. 이 경우 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.postHandle() 는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 을 사용해야 한다.afterCompletion() 에 예외 정보( ex )를 포함해서 호출된다.preHandler만 오버라이드 하면 된다default가 붙은 메서드기 때문에, 굳이 모두 구현하지 않아도 된다@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_NUMBER)==null){
log.info("미인증 사용자 요청");
// 로그인으로 리다이렉트
response.sendRedirect("/login?redirectURL="+requestURI);
return false;
}
return true;
}
HandlerInterceptor를 구현한다!!!인터셉터는 HttpServletRequest,HttpServletResponse가 파라미터라 다운 캐스팅 안해도 된다getSession을 통해 세션이 있는지 확인한다sendRedirect로 로그인 페이지로 리다이렉트하고false를 반환해 컨트롤러 자체를 실행하지 않는다
SRP!!! 기능과 책임을 나누자, 인터셉터에서는 오직 세션이 있는지만 확인한다
addPathPatterns 와 excludePathPatterns으로 url을 추가 및 제외 한다@Configuration 있는 webConifg파일을 봐보자@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/members/add", "/login",
"/logout", "/css/**", "/*ico", "/error");
}
WebMvcConfigurer를 구현한다!!!addInterceptors메서드를 오버라이드 한다addInterceptor로 방금 만든, 인터셉터 객체를 생성해 추가해준다order : 로 순위를 정하고addInterceptors : 로 모든 경로를 추가하고excludePathPatterns : 로 로그인 로직을 적용하지 않는 주소를 적어준다
Argument Resolver가 있기 때문이다HandlerMethodArgumentResolver 인데 줄여서 ArgumentResolver 라고 부른다.ArgumentResolver 의 supportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크하고, resolveArgument() 를 호출해서 실제 객체를 생성한다. 그리고 이렇게 생성된 객체가 컨트롤러 호출시 넘어가는 것이다
- 인터페이스니까 확장해서 사용해야지
supportsParameter(),resolveArgument()두개만 오버라이드해서 사용하면 되겠다!!!
먼저 에노테이션 @Login을 만든다!
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@Target(ElementType.PARAMETER) : 파라미터에만 사용@Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음다음은 HandlerMethodArgumentResolver을 구현한 LoginMemberA rgumentResolver이다
@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_NUMBER);
}
}
HandlerMethodArgumentResolver를 구현한 LoginMemberArgumentResolver를 만든다 supportsParameter(), resolveArgument() 두개만 오버라이드하자supportsParameter() : @Login 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver가 사용된다.resolveArgument() : 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다. hasParameterAnnotation으로 @Login에노테이션이 있는지 확인하고isAssignableFrom 로 파라미터의 타입이 parameter.getParameterType() 같은지 확인 한 후에getNativeRequest로 다운캐스팅해 HttpSerlvetRequest를 받고, 안에서 getSession으로 저장된 Member변수를 반환한다
@Configuration으로 설정정보를 저장하던, webConfig파일에WebMvcConfigurer을 구현하고addArgumentResolver을 오버라이드해서, 그 안에 add를 이용해 내가 만든 LoginMemberArgumentResolver() 객체를 추가해주자!!!@Bean으로 등록해, 파라미터로 @Login 에노테이션을 쓸 수 있게 해준다!!!