[Spring] Filter, Interceptor, ArgumentResolver

olsohee·2023년 6월 7일
0

Spring

목록 보기
9/12
post-custom-banner

1. 필터

공통 관심 사항

로그인 한 사용자만 상품 관리 페이지에 들어갈 수 있어야 한다고 가정해보자. 이때 로그인을 하지 않은 사용자는 상품 관리 페이지의 url을 직접 호출하는 경우에도 상품 관리 페이지로 들어갈 수 없어야 한다.

컨트롤러에서 로그인 여부를 체크하는 로직을 넣어주어, 로그인 한 사용자라면 요청에 따른 올바른 페이지를, 로그인 하지 않은 사용자라면 로그인 페이지를 보여줄 수 있다.

그러나 이 경우, 상품 등록, 수정, 삭제, 조회 등 상품관리의 모든 컨트롤러 로직에 로그인 여부를 체크하는 로직을 넣어주어야 한다. 그리고 향후 추가되는 기능에도 해당 컨트롤러에 로그인 여부 체크 로직을 넣어주어야 한다. 뿐만 아니라, 향후 로그인과 관련된 로직이 변경되는 경우, 작성한 모든 로직은 수정해야 한다.

이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사라고 한다. 위 예시에서는 상품 등록, 수정, 삭제, 조회 등 여러 로직에서 공통으로 인증에 대해 관심을 갖고 있다.

이러한 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.

필터 흐름

Http 요청 -> WAS -> 필터 -> 서블릿(디스패처 서블릿) -> 컨트롤러

필터를 적용하면 흐름은 위와 같다. 필터가 호출된 다음 서블릿이 호출되기 때문에, 만약 해당 url로 http 요청을 한 모든 사용자의 로그를 남기고 싶다면 필터를 사용하면 된다.

만약 필터에서 적절하지 않은 요청이라 판단하면, 서블릿을 호출하지 않고 흐름이 중단된다.

필터 체인

Http 요청 -> WAS -> 필터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() {}
}
  • 필터를 적용하려면 Filter 인터페이스를 구현하면 된다. 인터페이스를 구현하고 등록하면, 서블릿 컨테이너가 해당 필터를 싱글톤 객체로 생성하고 관리한다.

  • init()
    필터 초기화 메소드로, 서블릿 컨테이너가 생성될 때 호출된다.

  • doFilter()
    고객의 요청이 올 때 마다 해당 메소드가 호출된다. 필터의 로직을 구현하면 된다.

  • destroy()
    필터 종료 메소드로, 서블릿 컨테이너가 종료될 때 호출된다.

/**로그인 체크 필터*/
@Slf4j
public class LoginCheckFilter implements Filter {

    private static final String[] whitelist = {"/", "/main", "/signup", "/login", "/logout", "/css/*"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();

        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);
                    //로그인으로 리다이렉트
                    httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                    return; //미인증 사용자는 다음으로 진행하지 않고 끝
                }
            }

            chain.doFilter(request, response); //다음 필터 호출
        } catch (Exception e) {
            throw e; //톰캣까지 예외를 보내주어야 함
        } finally {
            log.info("인증 체크 필터 종료 {}", requestURI);
        }
    }

    private boolean isLoginCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
    }
}
/**필터 등록*/
@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        return filterFilterRegistrationBean;
    }
}

2. 인터셉터

스프링에서 제공하는 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다. 둘 다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위와 사용방법이 다르다.

인터셉터는 HandlerInterceptor 인터페이스를 구현하여 사용하면 된다. Spring은 HandlerInterceptor를 다음과 같이 설명한다.

Intercept the execution of a handler.

인터셉터 흐름

Http 요청 -> WAS -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러

인터셉터를 적용하면 흐름은 위와 같다.

인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장한다. 스프링 MVC의 사작점이 디스패처 서블릿이라고 생각하면 된다.

인터셉터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝나고, 컨트롤러를 호출하지 않는다. 그래서 로그인 여부를 체크하기에 좋다

인터셉터 체인

Http 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 1 -> 인터셉터 2 -> 컨트롤러

인터셉터는 체인으로 구성되는데, 중간에 인터셉터를 추가할 수 있다. 따라서 예를 들어 로그를 남기는 인터셉터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 인터셉터를 적용할 수 있다.

인터셉터 인터페이스

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)와 같이 단계적으로 세분화되어 있다.

  • 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView가 반환되는지 응답 정보도 받을 수 있다.

인터셉터 호출 흐름

  • preHandle
    컨트롤러 호출 전에 호출된다.
    preHandle의 응답 값이 true이면 다음으로 진행하고, false이면 더 진행하지 않는다. false인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다.

  • postHandle
    컨트롤러 호출 후에 호출된다.

  • afterCompletion
    뷰가 렌더링 된 이후에 호출된다.

예외 발생 상황

만약 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않는다.

반면 afterCompletion은 예외 발생 여부와 무관하게 항상 호출된다. 이 경우 예외(ex)를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다. 따라서 예외와 무관하게 공통 처리를 하려면 afterCompletion을 사용해야 한다.


3. ArgumentResolver

ArgumentResolver는 어떤 요청이 컨트롤러에 들어왔을 때, 요청에 들어온 값으로부터 원하는 객체를 만들어 줄 수 있다.

예를 들어 컨트롤러에서 쿼리 파라미터의 값을 변수에 바인딩할 때는 @RequestParam을, 가변적인 경로를 변수에 바인딩할 때는 @PathVariable을, Http Body를 변수에 바인딩할 때는 @RequestBody를 사용한다.

반면 Http Header, Session, Cookie 등 직접적이지 않은 방식 혹은 외부 데이터 저장소로부터 데이터를 바인딩해야 할 때는 ArgumentResolver를 사용하면 된다. ArgumentResolver를 사용하면 컨트롤러 메소드의 파라미터 중 특정 조건에 맞는 파라미터가 있다면, 요청에 들어온 값을 원하는 객체로 만들어 바인딩해줄 수 있다.

ArgumentResolver 생성

ArgumentResolver를 만들기 위해서는 HandlerMethodArgumentResolver를 구현해야 한다. Spring은 HandlerMethodArgumentResolver를 다음과 같이 설명한다.

Strategy interface for resolving method parameters into argument values in the context of a given.

즉 HandlerMethodArgumentResolver는 인터페이스이며, HandlerMethodArgumentResolver를 구현하는 클래스는 다음 두개의 메소드를 구현해야 한다.

  • supportsParameter(): 주어진 메소드의 파라미터가 이 ArgumentResolver에서 지원하는 타입인지 검사한다. 지원하면 true를, 지원하지 않으면 false를 반환한다.

  • resolverArgument(): supportsParameter()의 리턴 값이 true인 경우에만 호출되는 메소드이다. 대상이 되는 파라미터가 원하는 형태로 정보를 바인딩하여 반환한다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    //파라미터에 @Login 애노테이션이 있고, Member 타입이면 해당 ArgumentResolver가 사용된다.
    @Override
    public boolean supportsParameter(MethodParameter parameter) {

        log.info("supportsParameter 실행");

        //파라미터에 @Login 애노테이션이 있는지 확인
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);

        //파라미터가 Member 타입인지 확인
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation && hasMemberType;
    }

    //컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보(member 객체)를 생성해준다.
    @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_MEMBER);
    }
}

생성한 ArgumentResolver를 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
}

https://hudi.blog/spring-argument-resolver/
https://tecoble.techcourse.co.kr/post/2021-05-24-spring-interceptor/

profile
공부한 것들을 기록합니다.
post-custom-banner

0개의 댓글