로그인 기능을 구현하다고 해보자. 모든 컨트롤러에서는 비로그인 사용자의 접근을 차단하기 위해 로그인 로직이 필요하다. 하지만 로그인 로직을 모든 컨트롤러에 넣을 경우 로그인 로직이 바뀌면 모든 컨트롤러도 수정해야되는 문제가 발생한다.
여러 로직에서 공통으로 관심이 있는 로직을 공통 관심사(cross-cutting concern)이라 하며 공통 관심사(여기서는 로그인 로직)를 분리해 구현하면 위의 문제를 해결할 수 있다. 스프링은 공통 관심사를 분리하기 위해 AOP 기능을 제공하지만 웹과 관련된 공통 관심사는 서블릿 필터나 스프링 인터셉터를 사용하는 것이 좋다. 웹과 관련된 공통 관심사는 HTTP 요청 정보가 필요한데 서블릿 필터와 스프링 인터셉터는 바로 HttpServletRequest에 접근할 수 있기 때문이다.
Filter
필터란 J2EE 표준 스펙에 정의된 기술로 서블릿에 요청이 전달되기 전/후에 요청에 대해 부가작업을 수행할 수 있는 기능이다. 스프링에서 쓰일경우 위의 서블릿은 디스패처 서블릿이다. 필터는 서블릿와 같이 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너(서블릿 컨테이너)에 의해 관리가 되며 싱글턴으로 생성되고 관리된다.
필터를 사용하려면 Filter 인터페이스를 구현해야한다. 아래는 Filter 인터페이스이다.
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() : 필터 로직 수행 메서드
- destory() : 필터 객체 해제 메서드(서블릿 컨테이너가 종료될 때 호출)
아래는 모든 요청을 로그로 남기는 필터 코드이다.
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");
}
}
doFilter의 인자를 보면 ServletRequest와 ServletResponse가 있기에 요청 정보와 응답 정보에 접근할 수 있다. 또한 필터는 체인으로 동작한다. FilterChain 객체의 doFilter 메서드는 다음 필터가 있으면 필터를 호출하고 없으면 서블릿을 호출한다. FilterChain 객체의 doFilter 메서드 호출 전에는 다음 필터, 서블릿으로 요청이 전달되기 전의 로직을 작성하고 FilterChain 객체의 doFilter 메서드 호출 후에는 다음 필터, 서블릿으로 요청이 전달된 후의 로직을 작성한다. 만약 필터에서 FilterChain 객체의 doFilter 메서드가 호출되지 않으면 인자로 받은 ServletResponse 객체가 최종 응답 정보가 되어 응답된다. 참고로 FilterChain 객체의 doFilter 메서드는 인자로 ServletRequest, ServletResponse 객체를 넘길 수 있기에 다른 객체를 넣어도 잘 동작한다.
필터를 등록하는 방법은 여러 가지가 있지만 스프링 부트를 사용한다면 필터를 FilterRegistrationBean 타입 빈으로 등록하면 된다.
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;
}
}
- setFilter() : 등록할 필터 객체를 지정한다.
- setOrder() : 필터는 체인으로 동작한다. 따라서 순서가 필요하며 낮을수록 우선순위가 높다.
- addUrlPatterns() : 필터를 적용할 URL 패턴을 지정한다. 여러 패턴을 지정할수도 있다.
스프링 Interceptor
스프링 인터셉터는 스프링이 제공하기에 스프링이 제공하는 편리한 기능을 인터셉터에서 사용할 수 있다. 필터는 디스패처 서블릿이 호출되기 전에 호출된다면 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다. 필터와 마찬가지로 체인 구조로 동작한다.
스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
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 {
}
}
- preHandle : 컨트롤러 호출 전 메서드
- postHandle : 컨트롤러 호출 후 메서드
- afterCompletion : 요청 완료 후 메서드(뷰가 렌더링 된 이후)
인터셉터는 컨트롤러 호출 전(preHandle), 컨트롤러 호출 후(postHandle), 요청 완료 후(afterCompletion)로 호출될 로직이 잘 세분화되어 있다. 모든 메서드에 공통으로 있는 handler 인자에는 핸들러 객체가 있어 핸들러 정보에 접근할 수 있다. postHandle 인자에는 ModelAndView가 있어 뷰가 렌더링되기 전의 ModelAndView에 접근할 수 있으며 추가적인 Model 객체를 ModelAndView에 넣을수도 있다. preHandle 메서드에서 true를 리턴하면 다음 인터셉터 또는 핸들러가 호출되며 false를 리턴하면 인자로 받은 HttpServletResponse 객체가 최종 응답 정보가 되어 응답된다.
만약 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않지만 afterCompletion는 예외가 상관없이 무조건 호출된다.
아래는 모든 요청을 로그로 남기는 스프링 인터셉터 코드이다.
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("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);
if(ex != null) log.error("error", ex);
}
}
스프링 부트에서 인터셉터는 아래와 같이 등록할 수 있다.
package hello.login;
import hello.login.web.Interceptor.LogInterceptor;
import hello.login.web.Interceptor.LoginCheckInterceptor;
import hello.login.web.argumentResolver.LoginMemberArgumentResolver;
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 org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Filter;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.icon", "/error");
}
}
'Spring > Spring MVC' 카테고리의 다른 글
예외 처리와 오류 페이지 (0) | 2023.04.17 |
---|---|
ArgumentResolver 활용 (0) | 2023.04.17 |
스프링 검증 (2) (0) | 2023.04.02 |
스프링 검증 (1) (0) | 2023.03.27 |
스프링 MVC 기본 기능 (0) | 2023.02.28 |