Spring ‐ 필터와 인터셉터 - dnwls16071/Backend_Study_TIL GitHub Wiki

[ 필터 / 인터셉터 기준에서의 흐름 정리 ]

HTTP 요청 → 내장 톰켓 서버(WAS) → 필터 → 디스패처 서블릿 → 인터셉터 → 컨트롤러

📚 서블릿 필터(Filter)

  • 필터를 적용하면 필터가 호출된 다음 서블릿이 호출되기 때문에 서블릿에 도달하기 전에 부가적인 작업을 먼저 처리할 수 있다.
  • 스프링 하위 프레임워크인 스프링 시큐리티 역시 필터 체인으로 구성이 되어 있다.

스크린샷 2025-01-29 오전 3 25 50

  • init
    • 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
  • doFilter
    • 요청이 들어올 때마다 호출, 필터 로직을 구현하는 부분
  • destroy
    • 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출

[ 필터 작성 ]

@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);
    }
}
  • ServletRequest, ServletResponse
    • 필터 등록시 HTTP 요청이 들어오면 doFilter() 메서드가 호출된다.
    • ServletRequest, ServletResponse는 HttpServletRequest, HttpServletResponse 부모로 HTTP 요청이 아닌 경우까지 고려한 인터페이스이다.
  • chain.doFilter(request, response)
    • 다음 필터가 있으면 다음 필터를 호출하고 다음 필터가 없다면 서블릿을 호출한다.
    • 해당 코드를 기준으로 앞쪽을 서블릿 호출 전, 뒤쪽을 서블릿 호출 후라고 볼 수 있다.
    • 해당 코드가 없으면 필터를 호출하지 않고, 서블릿도 호출하지 않게 된다.
  • HttpServletResponse - sendRedirect() → 해당 메서드를 호출할 경우 상태 코드는 SC_FOUND(302)가 된다.

[ 필터 등록 ]

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<jakarta.servlet.Filter>();
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}
  • setFilter() : 등록하고자하는 필터를 지정
  • setOrder() : 필터 체인의 순서를 등록
  • addUrlPatterns() : 어떤 URL 패턴에 필터를 적용할지 지정할 수 있으며 이 때, 여러 패턴 지정이 가능

📚 스프링 인터셉터(Interceptor)

  • Servlet이 자바 기반의 서버 사이드 컴포넌트라면 Interceptor는 스프링 프레임워크 기반의 기술이다.
  • 필터와 마찬가지로 공통 관심 사항 및 부가 기능을 담당하지만 컨트롤러 앞에서 동작하고 필터보다 더 편리하고 정교하며 더 많은 기능을 제공한다.
  • 필터보다는 인터셉터를 사용하는 것을 권장한다.
  • 인터셉터 역시 필터와 마찬가지로 컨트롤러 호출 전에 동작해서 적절하지 않은 요청이라면 컨트롤러를 호출하지 않고 끝낼 수 있다.

스크린샷 2025-01-29 오전 10 54 15

  • prehandle
    • 컨트롤러 호출 전에 호출
    • 응답 값이 true이면 그대로 진행되고 응답 값이 false이면 더 이상 진행되지 않는다.
  • posthandle
    • 컨트롤러 호출 후에 호출
  • afterCompletion
    • 뷰가 렌더링 된 이후에 호출

afterCompletion()은 예외가 발생하더라도 호출이 된다. 따라서 공통 처리를 하려면 afterCompletion()을 호출해야 한다.

[ 인터셉터 작성 ]

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

[ 인터셉터 등록 ]

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor()) // 인터셉터 등록
                .order(1) // 순서
                .addPathPatterns("/**") // 등록 패턴
                .excludePathPatterns("/css/**", "/*.ico", "/error"); // 제외 패턴
    }
}

📚 ArgumentResolver

@Target(ElementType.PARAMETER) // 파라미터에만 적용
@Retention(RetentionPolicy.RUNTIME) // 리플렉션 등을 활용할 수 있도록 런타임까지 정보가 남음
public @interface Login {
}
  • 이전에도 공부를 했었지만 우리가 편리하게 사용하는 어노테이션인 @RequestBody, @RequestMapping 등등은 모두 ArgumentResolver에 의해 동작이 된다.
  • 우리가 개발한 커스텀 어노테이션을 사용하려면 ArgumentResolver를 만들어서 추가해주어야 한다.

[ ArgumentResolver 작성 ]

@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_MEMBER);
    }
}
  • supportsParameter
    • LoginMemberArgumentResolver가 어떤 파라미터를 지원하는가?
    • parameter.hasParameterAnnotation : 파라미터에 해당 어노테이션이 붙어있는가?
    • isAssignableFrom : 파라미터 타입을 체크
  • resolveArgument
    • 구현하고자하는 실질적인 로직을 구현하는 곳으로 여기선 서버 측 세션 저장소에 있는 로그인 멤버를 가져와서 반환한다.
    • supportsParameter가 true가 나온 경우 resolveArgument가 동작하게 된다.

[ 커스텀 ArgumentResolver 등록 ]

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // ...

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
}
⚠️ **GitHub.com Fallback** ⚠️