오늘은 handler interceptor 에 대한 이야기를 조금 해보고자 한다.

Filter 는 자바 servlet 에서 제공하는 기술이지만 interceptor는 spring에서 제공하는 기술로써

세션 인증과 관련된 부분을 공부하다가 적어본다.

 

▶ 그래서 interceptor 가 뭔가요?

spring MVC 에서 인터셉터란 결국

지정된 URI에 관한 호출을 가로채는 역할을 하는 기능입니다.

이러한 호출을 가로채서 뭐하냐 싶지만

우리는 로그인을 한다거나 어떠한 게시물에 조회수를 올린다거나 하는 작업을 할 때 와 같은

공통된 관심사를 컨트롤 하기 위해서 사용하는 것 이다.

 

이는 이전 게시글인 Filter 와 같은데 두개의 차이점은 확실히 존재한다.

  Filter 와 Interceptor의 차이점

Filter와 Interceptor는 그 기능이 매우 흡사하여 나와같은 초보들이 가장 많이 헷갈려하는게

두 기능은 확연한 차이가 존재한다.

 

spring MVC request LifeCycle

위 사진을 보면 해당 차이를 조금 더 확실하게 알 수 있는데

Filter는 요청을 보내면 Tomcat과 같은 WAS 서버에 들어오자마자 작동되는 것으로

Spring의 영역에 접근하여 사용하기가 어렵습니다.

 

반면에 interceptor는 Spring 내에서 관리되는 기능이기에 Spring의 영역 내에서 사용되고 있는 것을 볼 수 있습니다.

 

이것으로 인해서 알 수 있는 차이는 생성된 Bean에 접근이 용이한가로 볼 수 있습니다.

 

◆ 차이점 정리

▶ Interceptor와 Filter는 호출 시점의 차이

  • Filter는 Dispatcher Servlet 호출 전(WAS의 단계)
  • Interceptor은 Dispatcher Servlet 호출 후 실행 됨

 

▶ Interceptor 의 용도

  • 인증 인가 등과 같은 공통 작업
  • Controller로 넘어가는 정보의 가공
  • API 호출에 대한 로깅 또는 감사

▶ Filter의 용도

  • 보안 관련 공통 작업
  • 모든 요청에 대한 로깅 
  • 이미지, 데이터 압축 및 문자열 인코딩

이제 이어지는 사용법을 알아보자

 

Handler Interceptor는 Interface 이므로 class 에 implements 하여 사용하면 된다.

public interface HandlerInterceptor {
    // controller 호출 전에 실행 (핸들러 어댑터 실행 전)
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

	// controller 호출 후에 실행 (핸들러 어댑터 호출 후)
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

	// view의 랜더링 이후 실행
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

위 코드가 HandlerInterceptor의 기본 모습이며 사용처는 내부에 적어두었다.

 

이를 사용하여 만든 간단한 예문을 적고 마무리 이 글을 마무리 하고자 한다.

 

▶ 예문

LOG 를 찍어 흐름 확인하기

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override // controller 호출 전에 실행 (핸들러 어댑터 실행 전)
    public boolean preHandle(HttpServletRequest request,
    			HttpServletResponse response,
                Object handler) throws Exception {

        String reqURI = 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("REQ [{}][{}][{}]",uuid, reqURI,handler);


        return true;
    }

    @Override // controller 호출 후에 실행 (핸들러 어댑터 호출 후)
    public void postHandle(HttpServletRequest request, 
    			HttpServletResponse response, 
                Object handler,
                ModelAndView modelAndView) throws Exception {
                
        log.info("postHandle [{}]", modelAndView);
        
    }

    @Override // view의 랜더링 이후 실행
    public void afterCompletion(HttpServletRequest request,
                HttpServletResponse response, 
                Object handler, 
                Exception ex) throws Exception {
                
        String reqURI = request.getRequestURI();
        String logId = (String) request.getAttribute(LOG_ID);
        log.info("RES [{}][{}]", logId, reqURI);
        if (ex != null) {
            log.error("afterCompletion error!", ex);
        }
        
    }
}

 

※ Interceptor를 정의한 클레스

 

@Configuration
public class WebConfig implements WebMvcConfigurer {
	
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // logInterceptor register
        registry.addInterceptor(new LogInterceptor()) // 인터셉터 등록
                .order(1) // 인터셉터의호출 순서를 지정, 낮을수록 먼저 호출
                .addPathPatterns("/**") // 인터셉터를 적용할 URL패턴 지정
                .excludePathPatterns("/css/**", "/*.ico", "/error"); //인터셉터에서 제외할 패턴 지정


}

 

※ Interceptor 등록

 

 

 

♣ 참고 및 강의

인터페이스 차이점과 정의 관련 참고 블로그

 

https://inf.run/GMo43 - 김영한의 스프링MVC2 

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 - 인프런

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

'Web > spring' 카테고리의 다른 글

servlet filter  (0) 2024.01.19

2024.01.19

오늘 정리하고자 하는 내용은 서블릿 필터에 관한 내용이다.

김영한의 MVC2 강의 내용정리

우리는 흔히 로그인 시스템을 만들었을 때 로그인 유지 및 그에 따른 사용자 구분 등을 어떻게 해결할 지 고민하게 되는데

이 고민을 해결해 줄 부분이 servlet filter 이다. servlet filter 는 각 로직에서 공통적으로 사용되는 공통 관심사

(영어로는 cross-cutting) 를 하나로 통일시켜 우리를 훨씬 편하게 만들어줄 수 있는 친구다.

 

물론 이러한 부분에 대해서는 AOP 가 정말 대표적이지만 필자는 아직 AOP 공부안해봤음...ㅎ...

 

어쨋든 공부하다가 보니 웹에서는 servlet filter나 spring interceptor 을 사용하는게 좋다고 하길래 적는 내용.

 

필터는 javax.servlet 으로 import 할 수 있는 인터페이스다.

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() 의 경우 필터 종료 메서드로, 서블릿 컨테이너가종료될 때 호출된다.

 

이러한 필터는 체인 형태로 구성되어 중간중간에 자유롭게 추가가 가능하다.

 

이게 무슨소린가 싶은 미래의 나를 위해 그림 추가 ↓

 

 

더보기
필터의 전체적인 흐름
필터의 작동 흐름
필터 체인의 자유로운 추가

 

 


사용법

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 {
//        Filter.super.init(filterConfig);
        log.info("log filter init");
    }

    /**
     * HTTP요청 시 doFilter 가 호출
     */
    @Override
    public void doFilter(ServletRequest request, 
    			ServletResponse response, 
                FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // public void doFilter(ServletRequest request...) 
        // 이 부분은 HTTP요청이 아닌 경우 (HTTPS 등) 까지 고려해서 만든 인터페이스이다.
        // HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // 로 다운케스팅 해서 사용하면 된다.

        String requestUrl = httpServletRequest.getRequestURI();

        String uuid = UUID.randomUUID().toString();
        //요청 구분을 위한 임의의 uuid 생성

        try {

            log.info("REQ [{}] [{}]", uuid, requestUrl);
            chain.doFilter(request, response);
            // 다음 필터가 존재하면 필터를 호출, 필터가 없으면 서블릿을 호출
            // 만일 이부분이 없으면 다음 단계로 진행되지 않음.

        } catch (Exception e) {
            throw e;
        } finally {
            log.info("RES [{}] [{}]", uuid, requestUrl);
        }
    }

    @Override
    public void destroy() {
//        Filter.super.destroy();
        log.info("log filter destroy");
    }
}

위 와 같이 필터로 지정할 메서드를 하나 만들어주고

 

package hello.login;

...

import javax.servlet.Filter;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean 
        							= new FilterRegistrationBean<>();

        filterFilterRegistrationBean.setFilter(new LogFilter());
        // 등록할 필터 지정
        filterFilterRegistrationBean.setOrder(1);
        // 필터는 체인으로 동작하는데 이 때 순서가 필요하다. 낮을 수록 먼저 동작하게 된다.
        filterFilterRegistrationBean.addUrlPatterns("/*");
        // 필터를 적용할 URL패턴을 지정. 한번에 여러 패턴을 지정할 수 있다.


        return filterFilterRegistrationBean;
    }

}

@Bean을 통해 bean 등록을 진행해준다.

 

★ 만약 스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록

 

이러한 필터를 적용할 때 중요한 부분이 있는데 이것이

whitelist

이다.

 

항상 코딩하면서 생각하는거지만 별거아니라고 생각되는 무언가가 제일 날 화나게해..

 

Filter 메서드를 Override 해보면 알겠지만 이 whitelist란 해당 웹사이트에서 이 필터가 적용되지 말아야 할 부분은 어디인가를 제외하는 문자열 배열이다.

 

public class LoginCheckFilter implements Filter {

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

 

 

이런식으로 작성해서 해당 클레스에서만 사용되도록 되어있는데 따로 config로 빼서 관리해도 좋을 듯 싶다. (개인적인 생각)

 

이렇게 빈으로써 등록해두면 어디에 접속하든 로그에

2024-01-19 20:53:34.742  ...  : RES [16eacd30-94dc-4f11-b046-663d3c291f21] [/login]
2024-01-19 20:53:39.443  ...  : REQ [69525432-505e-4cac-9076-f34c996176ea] [/]

이런식으로 찍히는 걸 볼 수 있다.

 

이렇게 찍히는게 보였으면 filter 적용완료이다.

 

 

 

 

'Web > spring' 카테고리의 다른 글

servlet interceptor  (0) 2024.01.22

+ Recent posts