티스토리 뷰

반응형
Filter와 Interceptor가 필요한 이유

로그인과 로그를 찍는 작업을 비롯하여 하나의 컨트롤러(핸들러)에게만 적용하는 것이 아닌, 서비스 단위로 공통적으로 처리해야 하는 작업들이 있다. AOP가 공통 관심사를 처리하는 역할을 했지만 웹 관련된 공통 관심사에 적용할 때는 HTTP 헤더 및 URL을 이용하여야 하기 때문에, 서블릿이 제공하는 필터 혹은 스프링의 인터셉터를 사용하는 것이 더 좋다

 

Servlet Filter

서블릿이 지원하는 Filter는 무언가를 여과시켜주는 필터처럼 클라이언트가 요청하는 호출을 컨트롤러단에 가기전에 미리 살펴보아 필터 처리를 해준다 필터의 흐름은 아래와 같다

 

• 필터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤

만약 로그인을 검증하는 필터를 만들었다고 해보자 그렇다면 필터는 개발자가 만들어 놓은 로직에 따라 정상적, 비정상적 접근을 구분하여 컨트롤러를 호출할지 아니면 호출하지 않을지 결정한다

 

• 필터 체인

필터는 체인으로 구성이 가능하기 때문에, 여러개의 필터를 설정하여 각기 다른 로직들을 검증하는 구조를 만들 수도 있다

Servlet Filter Interface
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, destroy와 같은 메서드들이 있는데 각 메서드들이 담당하는 역할은 아래와 같다

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

doFilter() : 고객의 요청이 올 때 마다 해당 메서드가 호출된다, 필터의 로직을 구현하는 부분이다

destroy() : 필터 종료 메섣, 서블릿 컨테이너가 종료될 때 호출된다

 

만약 사용자의 로그인을 체크하는 필터를 만든다고 하면 아래와 같다

@Slf4j
public class LoginCheckFilter implements Filter { //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();
       //화이트 리스트에 허용 가능한 리스트를 비교하기 위하여 클라이언트가 요청한 URI 정보를 받아준다
       
       HttpServletResponse httpResponse = (HttpServletResponse) response;
       //클라이언트에게 응답을 위한 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; //미인증 사용자는 return에 걸려 컨트롤러 호출이 되지 않는다
               }
        }
        chain.doFilter(request, response);
        //필터를 사용하기 위하여 꼭 필요한 doFilter이다, 이 메서드가 생략되면 다음 단계의 호출이 일어나지 않는다
        
        } catch (Exception e) {
        throw e; //예외 로깅 가능 하지만, 톰캣까지 예외를 보내주어야 함 
        
        } finally {
        log.info("인증 체크 필터 종료 {}", requestURI); 
        }
    }

    /**
    * 화이트 리스트의 경우 인증 체크X
    */
    private boolean isLoginCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}

중점적인 부분들만 간추려서 다시 정리해보겠다

 

request, response : 서블릿 필터의 인터페이스가 제공하는 request와 response의 경우 HttpServletRequest&Response의 부모인 ServlerRequest&Response이다 이렇게 만든 이유는 Http요청이 아닌 경우까지 참고해서 만들었다고 하는데, 보통 HTTP 요청을 사용하기 때문에 다운 캐스팅하여 사용되었다

 

whiteList : 필터에 적용하지 않을 리스트를 작성했다, 예를 들어 로그인을 한 사용자를 걸러내는데, 로그인을 제외하지 않으면 해당 사용자는 로그인을 하기 위해서 로그인을 한 상태가 되어야 할 것이다

 

redirect : 그냥 redirect라고 볼 수 있지만, 사용자가 마지막으로 접속하려 했던 URI를 쿼리파라미터로 함께 전송시킨다, 이로써 사용자는 로그인을 한 후 본인이 마지막에 접속하려했던 화면으로 바로 갈 수 있어 더욱 간편하게 서비스를 이용할 수 있다

 

return : 필터에서 컨트롤러까지 가는 호출을 막는 부분이다, 즉 필터를 더 이상 진행시키지 않아 서블릿, 컨트롤러가 호출되지 않는 것이다

 

doFilter : 필터 체인에서 다음 필터를 호출하고 만약 없다면 서블릿을 호출해주는 메서드이다, 만약 이 메서드가 생략이 된다면 다음 단계가 호출이 안되기 때문에 먹통이 된 것처럼 아무 일도 일어나지 않는다 (즉 컨트롤러랑 서블릿을 호출하지 못하는 것)

 

이렇게 작성된 필터를 사용하기 위해서는 빈으로 등록을 해주어야 한다

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
    
        //사용할 필터를 설정한다
        filterRegistrationBean.setFilter(new LoginCheckFilter());
    
        //필터 체인으로 구성되는 경우를 위하여 필터가 동작할 순서를 정한다
        filterRegistrationBean.setOrder(1);
    
        //필터를 적용한 URL 패턴을 입력한다, /*로 작성할 경우 모든 URL에 적용이 된다 (여기서 예외가 화이트 리스트)
        filterRegistrationBean.addUrlPatterns("/*");
    
        return filterRegistrationBean;
    }

}

 

Spring Interceptor

인터셉터의 경우 스프링이 제공하는 기능이다, 필터와 매우 유사하게 웹과 관련된 공통 관심사항을 해결할 수 있게 작동하지만 조금 차이점이 있기도 하다, 스프링이 제공하는 기능인만큼 특별한 경우를 제외하고서는 보통 인터셉터를 사용한다고 한다

 

• 인터셉터의 흐름

스프링이 제공하는 인터셉터의 경우 필터와 함께 사용할 수도 있다, 이럴 경우 아래의 흐름도와 같이 필터가 먼저 작동하고 인터셉터가 작동하게 된다, 인터셉터가 서블릿 뒤에 등장하는 이유는 "스프링"이 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 동작하게 된다

만약 스프링 인터셉터를 이용하여 정상적, 비정상적 접근을 구분한다면 아래와 같이 작동한다

 

• 인터셉터 체인

인터셉터 또한 필터와 마찬가지로 체인으로 구성할 수 있다

Spring Interceptor Interface

스프링 또한 인터셉터를 인터페이스로 제공한다

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 {
    }

}

필터의 경우 단순하게 doFilter() 하나의 메서드를 제공한 것과는 달리 인터셉터의 경우 총 세 가지의 메서드를 제공한다, 이는 컨트롤러가 호출되는 것을 기준으로 하여 작동한다

 

preHandle : 컨트롤러 호출 전

postHandle : 컨트롤러 호출 후

afterCompletion :  요청 완료 이후

 

• 인터셉터의 정상적 흐름

스프링의 인터셉터가 정상적으로 호출된다면 아래와 같은 흐름으로 작동하게 된다

흐름도를 보면 컨트롤러가 호출되기 전에는 "preHandle"이 작동하고, 호출된 후에는 "postHandle"이, 마지막으로 이러한 요청들이 완료된 이후에 "afterCompletion"이 작동하는 것을 볼 수 있다

 

• 인터셉터의 예외 흐름

만약 인터셉터가 비정상적인 접근으로 인하여 예외를 발생시키는 상황이라면 아래와 같이 작동한다

preHandle이 예외를 발생시키면 뒤에 있던 postHandle이 호출되지 않는다, 이때 주의할 점이 있는데 afterCompletion은 항상 호출이 된다는 것이다 마치 자바의 try catch문에서 finally가 항상 작동하는 것을 생각하면 이해가 좀 더 쉽겠다

 

인터셉터와 인터셉터의 체인까지 활용하여 사용자가 접근하는 로그와 로그인 상태를 체크하는 인터셉터를 만든다면 아래와 같다

 

• Log Interceptor

@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);
        //인터셉터는 싱글톤패턴이기 때문에 setAttribute를 활용하여 밑에 있는 afterCompletion에게 UUID값을 전달한다
                   
            //@RequestMapping: HandlerMethod & 정적 리소스: ResourceHttpRequestHandler 
            if (handler instanceof HandlerMethod) {
                HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
            }
        
            log.info("REQUEST  [{}][{}][{}]", uuid, requestURI, handler);
            return true; //false면 다음 호출이 진행되지 않는다
        
        }
    
    @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 logId = (String)request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}]", logId, requestURI);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    } 
}

중점적인 부분들만 요약해보겠다

request.setAttribute : 인터셉터의 경우 호출 시점이 분리되어 있기 때문에 preHandle에 지정한 값을 함께 사용하려면 어딘가에 담아두어야 하는데, 싱글톤처럼 사용하기 때문에 지역변수를 사용하기 위험하다, 이에 request에 값을 담아 전달하는 방법을 사용하였다

 

HandlerMethod : 핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다 스프링에서 일반적으로 사용하는 @Controller, @RequestMapping의 경우 핸들러 정보가 HandlerMethod가 넘어오며 정적 리소스의 경우 ResourceHttpRequestHandler가 넘어온다

 

afterCompletion : 종료 로그가 afterCompletion에 있는 이유는 항상 호출이 되기 때문이다, 만약 preHandle에서 예외가 발생한다면 postHandle이 호출되지 않기 때문에 항상 호출이 되는 afterCompletion에 종료 로그를 작성하여, 항상 종료 로그가 찍힐 수 있도록 하였다

 

• 인터셉터 등록

필터와 마찬가지로 인터셉터 또한 사용을 위하여 등록이 필요하다

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //적용할 인터셉터를 선택한다
        registry.addInterceptor(new LogInterceptor())
                //인터셉터 체인에서 작동할 순서를 설정한다
                .order(1)
                //인터셉터를 적용할 URL을 설정한다              
                .addPathPatterns("/**")
                //인터셉터 적용을 제외 할 URL을 설정한다
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }
    //...
}

등록에서 인터셉터가 필터보다 편리한 이유가 등장하는데, 필터가 whilteList를 작성하여 적용 제외할 URL을 설정하였다면, 인터셉터의 경우 등록할 때 제외할 URL들을 위와 같이 설정할 수 있다, 이로써 필터보다 훨씬 더 정교하고 편리하게 URL 패턴을 설정할 수 있다

 

서블릿과 인터셉터의 경우 URL을 설정하는 표현식이 완전히 다르다

//서블릿 필터
/*

//스프링 인터셉터
/**

인터셉터의 URL 패턴을 설정하는 표현식은 공식문서에서 자세하게 살펴볼 수 있다

(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html)

 

• LoginCheck Interceptor

로그인 체크를 하는 인터셉터를 만든다면 아래와 같다

@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(false);
        
        //인증이 불가능한 사용자의 접근 검증
        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            
            //사용자가 로그인을 할 수 있게 로그인 페이지로 redirect 
            response.sendRedirect("/login?redirectURL=" + requestURI); 
            
            //컨트롤러 호출을 막기 위해 false 반환
            return false;
            
        }
        
        //인증이 가능한 사용자는 다음 컨트롤러 호출
        return true;
        
    }
}

 

필터가 모든 메서드를 호출한 것과는 다르게 인터셉터는 컨트롤러가 호출되기 전인 preHandle만 작성하여도 된다

이제 로그인 체크를 위한 인터셉터를 등록해보자

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        //로그를 출력하는 인터셉터
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
        
        //로그인 체크 인터셉터
        registry.addInterceptor(new LoginCheckInterceptor())
                //로그 인터셉터 뒤에 작동할 수 있도록 설정
                .order(2)
                //인터셉터를 적용할 URL 패턴 설정
                .addPathPatterns("/**")
                //인터셉터 적용을 제외 할 URL 패턴 설정
                .excludePathPatterns("/", "/members/add", "/login", "/logout",
                    "/css/**", "/*.ico", "/error");
        }
        //...
}

이렇게 인터셉터를 사용하면 필터보다 더욱 편리하고 정밀하게 웹과 관련한 공통 관심사를 처리할 수 있다

 

 

핵심정리

로그인, 클라이언트 로그 기록 등 웹과 관련된 공통 관심사를 처리하기 위하여 서블릿이 제공하는 필터와, 스프링이 제공하는 인터셉터를 사용한다, 이 둘은 같이 사용할 수 있으며 스프링 MVC의 흐름상 필터가 앞단에 위치하고 인터셉터가 뒤에 위치하게 된다, 스프링이 직접 제공하는 기능인 인터셉터가 주로 사용된다고 한다

 

인터셉터의 경우 컨트롤러의 호출을 기준으로 preHandle(컨트롤러 호출 전), postHanlde(컨트롤러 호출 후), afterCompletion(요청 완료 후)를 제공하며, preHandle에서 예외가 발생했을 시 postHandle의 호출이 되지 않는 반면 afterCompletion의 경우 예외와 상관없이 항상 호출이 된다

반응형
댓글