Spring

MVC2 - Filter, Interceptor

코드파고 2022. 10. 5. 18:34

쇼핑몰을 예를 들어 보자면, 주문 현황 페이지는 게스트가 아닌 회원만이 접속할 수 있어야 한다.

만약 게스트가 주문 현황 페이지에 접근하게 되면 타 페이지로 리다이렉트되야 할 것이다

하지만 위와 같이 회원만이 접근할 수 있는 페이지가 여러 개일 경우 일일이 컨트롤러에서 로그인 여부를 체크해서 리다이렉션해주어야 할까?😅

모든 컨트롤러에서 매번 권한을 체크하고 리다이렉션해주어야 한다면 👎실수가 발생할 확률이 높아질 것이고, 👎로직이 변경/추가될 때 일일이 변경해줘야 하는 수고가 생긴다.

이처럼 여러 곳에 걸쳐서 공통의 관심사가 적용되는 경우를 '공통 관심사(Cross-cutting Concern)'라고 한다.

위 쇼핑몰과 같은 예시는 인증에 대한 공통 관심사가 존재한다고 말할 수 있겠다.

공통... 관심사? AOP로도 해결 가능하겠지만 웹 관련 공통 관심사는 주로 서블릿 필터 / 스프링 인터셉터를 사용한다.

 

서블릿 필터

필터는 마치 DispatcherServlet과 같이 수문장 역할을 한다 😊

 

필터 플로우

HTTP 요청 👉 WAS 👉 필터(1번째, 2번째, 3번째) 👉 서블릿 👉 컨트롤러

정말 정수기의 필터처럼... 잘못된 호출의 경우 필터에서 걸러져 서블릿이 호출되지 않는다.

여러 필터를 체인처럼 사용할 수도 있다. 그리고 필터는 싱글톤으로 생성됨을 주의하자

 

필터 구현(+생명주기)

init() 👉doFilter() 👉 destroy()

init()

config 객체를 저장한다. 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드이다.

웹 컨테이너가 1회 init 메소드를 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리된다.

 

doFilter()

doFilter 메소드는 url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드이다.

doFilter의 파라미터로는 FilterChain이 있는데, FilterChain의 doFilter 통해 다음 대상으로 요청을 전달하게 된다.

chain.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.

dofilter 내에서 dofilter()를 실행해 주어야 다음 단계(필터 혹은 서블릿)로 넘어간다!

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    log.info("log filter dofilter");
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    	// HttpServletRequest를 이용 시 다운캐스팅 해주기
  	chain.doFilter(request, response);
}

 

destroy()

destroy 메소드는 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드이다.

이는 웹 컨테이너에 의해 1번 호출되며 이후에는 이제 doFilter에 의해 처리되지 않는다.

 

필터의 등록

앞서 구현한 Filter를 등록해 보자😊

스프링 부트 사용시 FilterRegistrationBean을 사용한다

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean logFilter(){
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter()); // 등록 필터 지정
        filterRegistrationBean.setOrder(1); // 필터 순서
        filterRegistrationBean.addUrlPatterns("/*"); // 필터 적용 URL 패턴
        return filterRegistrationBean;
    }
}

 

위와 같이 필터를 등록하면 필터가 적용된 URL 패턴에서는 필터가 동작하는 것을 확인할 수 있다.

 

스프링 인터셉터

서블릿이 아닌 MVC가 제공하는 기술

HTTP 요청 👉 WAS 👉 필터 👉 디스패처 서블릿 👉 스프링 인터셉터 👉 컨트롤러

적절치 못한 요청이 들어오면 컨트롤러를 호출하지 않는다

인터셉터는 필터보다 정교하고, 편리하다

 

인터셉터 구현

HandlerInterceptor를 구현하면 된다.

public class LogInterceptor implements HandlerInterceptor {

 

preHandle()

컨트롤러 호출 전에 호출

@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); // 정보 담기
    ..
    if (handler instanceof HandlerMethod) {
        HandlerMethod hm = (HandlerMethod) handler;// 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다. 
    }

 

이 때, handler의 종류에 따라 분기를 나누어 처리할 수 있음을 기억하자

정적 리소스가 호출될 경우 👉 ResouceHttpRequestHandler
@Controller, @RequestMapping을 사용한 핸들러 매핑의 경우 👉 HandlerMethod

 

postHandle()

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

컨트롤러에서 예외가 발생하지 않을 시 호출

 

afterCompletion()

@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI(); // preHandle에서 이용한 값 꺼내기
        String uuid = (String) request.getAttribute(LOG_ID);

예외가 발생하든, 발생하지 않든 완료 후 항상 호출

 

preHandle에서 사용한 값을 postHandle, afterCompletion에서 사용하고자 한다면 HttpServletRequest에 담아서 이용하자.

HttpServletRequest에 대해 아래 내용을 복기🙃 

HttpServletRequest는 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소의 특징을 가진다

preHandle에서 setAttribute를 통해 key, value 형식으로 정보를 저장하고

request.setAttribute(LOG_ID,uuid);

 

afterCompletion에서 getAttribute를 통해 정보를 가져오자

String uuid = (String) request.getAttribute(LOG_ID);

 

구현한 인터셉터 등록

WebMvcConfigurer - addInterceptors를 구현

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor()) // 특정 인터셉터 등록
                .order(1) // 인터셉터 순서
                .addPathPatterns("/**") // 인터셉터를 적용할 패턴
                .excludePathPatterns("/css/*", "/*.ico", "/error"); // 인터셉터를 적용하지 않을 패턴
    }

 

인터셉터는 화이트리스트 같은 것을 코드로 구현하지 않고, 위와 같이 .excludePathPatterns()를 이용하면 된다.

앞단의 로직만을 관리하며 인터셉터 적용 패턴 부분이 간단하다는 면에서 인터셉터의 구현이 필터보다는 용이함을 알 수 있다.

 

ArgumentResolver

컨트롤러의 파라미터에 대해 ArgumentResolver를 적용해보자

아래의 Member 변수가 타깃🔍

@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {

애노테이션 생성

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}

@Target(ElementType.PARAMETER) : 파라미터 타입에 적용

@Retention(RetentionPolicy.RUNTIME) : 런타임까지 애노테이션 정보 유지

 

ArgumentResolver 구현

HandlerMethodArgumentResolver를 상속받아 구현

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

 

supportsParameter

1️⃣ 애노테이션이 붙어 있는지

2️⃣ 적용하고자 하는 타입이 맞는지 확인한다

@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;
}

 

resolveArgument

컨트롤러 호출 직전에 호출 되어 파라미터 정보 생성

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    log.info("resolveArgument 실행");
    HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); // HttpServletReqeust로 변환
    HttpSession httpSession = request.getSession(false);
    if (httpSession == null) { // 세션이 없다면 null 반환 -> loginMember가 없다
        return null;
    }
    return httpSession.getAttribute(SessionConst.LOGIN_MEMBER);
}

 

ArgumentResolver 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }