필터,인터셉터

Single Ko·2023년 6월 4일
0

Spring 강의 정리

목록 보기
18/31

필터는 서블릿이 제공하는 기능이고, 인터셉터는 스프링이 제공하는 기능이다.

공통 관심사

이런 필터나 인터셉터라는 기술이 필요한 이유는 공통의 관심사항 때문이다. 공통의 관심사항이란 애플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사라고 한다.

로그인과 관련된 공통의 관심사(이 유저가 로그인한 유저인가...)가 있을때, 컨트롤러에 일일이 똑같은 유저 확인 로직을 넣어주면 유지 보수에서 너무 힘듬. 이런 공통 관심사를 해결하기 위해 필터나 서블릿을 이용하는 것이다.

물론 이런 공통 관심사는 스프링의 AOP를 통해 해결할 수도 있지만 웹과 관련된 공통의 관심사는 서블릿 필터나 스프링 인터셉터를 이용하는 것이 좋다. 그 이유는 웹과 관련된 공통의 관심사는 Http Header나 URL 정보들이 필요한데, 필터나 인터셉터는 HttpServletRequest를 제공한다.

서블릿 필터

서블릿이 지원하는 수문장과같은 역할

필터

  • 특졍 URL 패턴에 대해 적용할 수 있다.
  • 적절하지 않은 요청이라 판다하면 거기서 끝낼 수 있다.(로그인 여부 판단하기 좋음)
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자

필터 체인

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();

}
  • 필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.
  • init(): 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
  • doFilter(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
  • destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다

필터 구현

로그를 남기는 필터 구현

  • doFilter()의 ServletRequest는 HttpServletRequest의 부모 인터페이스인데, 별 기능이 없어서 HttpServletRequest로 다운캐스팅 해주면 된다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, 
						FilterChain chain) throws IOException, ServletException {
                        
	HttpServletRequest httpRequest = (HttpServletRequest) request;
	String requestURI = httpRequest.getRequestURI();
	String uuid = UUID.randomUUID().toString();
	try {
		log.info("REQUEST [{}][{}]", uuid, requestURI);
		chain.doFilter(request, response);
	} catch (Exception e) {
		throw e;
	} finally {
		log.info("RESPONSE [{}][{}]", uuid, requestURI);
	}
}
  • chain.doFilter(request, response);
    가장 중요한 부분. 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다. (AOP에서 프록시가 target객체를 호출해줘야 되는거 생각하면 됨)

위에서 만든 필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록하면 됨.

@Configuration
public class WebConfig {

	@Bean
 	public FilterRegistrationBean logFilter() {
 	FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
    
   	registrationBean.setFilter(new LogFilter());
   	registrationBean.setOrder(1);
  	registrationBean.addUrlPatterns("/*");

 	return registrationBean;
 	j}
}
  • setFilter(new LogFilter()) : 등록할 필터를 지정한다.
  • setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
  • addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다

스프링 인터셉터

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

  • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 된다. 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장하게 된다. 스프링 인터셉터도 URL 패턴을 적용할 수 있는데, 서블릿 URL 패턴과는 다르고, 매우 정밀하게 설정할 수 있다.

스프링 인터셉터 제한

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청, 컨트롤러 호출X) // 비 로그인 사용자

인터셉터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있다. 그래서 로그인 여부를 체크하기에 딱 좋다.

스프링 인터셉터 체인

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

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

지금까지 내용을 보면 서블릿 필터와 호출 되는 순서만 다르고, 제공하는 기능은 비슷해 보인다. 다만 여태껏 그래왔듯 스프링 인터셉터는 서블릿 필터보다 편리하고, 더 정교하고 다양한 기능을 지원한다.

스프링 인터셉터 인터페이스

스프링의 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.

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 {}
}
  • 서블릿 필터의 경우 단순하게 doFilter() 하나만 제공된다.

  • 인터셉터는 컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있다.

  • 서블릿 필터의 경우 단순히 request , response 만 제공했지만, 인터셉터는 어떤 컨트롤러( handler )가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있다

정상 흐름

  • preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.) preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다. false 인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림에서 1번에서 끝이 나버린다.

  • postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)

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

예외 발생시

  • preHandle : 컨트롤러 호출 전에 호출된다.

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

  • afterCompletion : afterCompletion 은 항상 호출된다. 이 경우 예외( ex )를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.

예외가 발생하면 postHandle() 는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 을 사용해야 한다. 예외가 발생하면 afterCompletion() 에 예외 정보( ex )를 포함해서 호출된다.(예외가 없으면 null)

정리

인터셉터는 스프링 MVC에 특화된 필터 기능을 제공한다. 꼭 필요한 경우가 아니라면 서블릿 필터 대신 스프링 인터셉터를 사용하는 것이 더 편리하다.

인터셉터 구현

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);
        
		//@RequestMapping: HandlerMethod
		//정적 리소스: ResourceHttpRequestHandler
        //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
		if (handler instanceof HandlerMethod) {
			HandlerMethod hm = (HandlerMethod) handler; 
		}
		log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
		return true; //false 진행X
	}
    
    @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();
        
        //preHandler에서 넘겼던 UUID를 request를 통해 받을 수 있다.
		String logId = (String)request.getAttribute(LOG_ID);
		log.info("RESPONSE [{}][{}]", logId, requestURI);
		if (ex != null) {
			log.error("afterCompletion error!!", ex);
		}
	}
}
  • preHandle 에서 지정한 값을 postHandle , afterCompletion 에서 함께 사용하려면 어딘가에 담아두어야 한다. LogInterceptor 도 싱글톤 처럼 사용되기 때문에 맴버변수를 사용하면 위험하다. 따라서 request 에 담아두었다. 이 값은 afterCompletion 에서 request.getAttribute(LOG_ID) 로 찾아서 사용한다.

  • return true : true 면 정상 호출이다. 다음 인터셉터나 컨트롤러가 호출된다

인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
	@Override
    public void addInterceptors(InterceptorRegistry registry) {
	 registry.addInterceptor(new LogInterceptor())
 		     .order(1)
 			 .addPathPatterns("/**")
 			 .excludePathPatterns("/css/**", "/*.ico", "/error");
 	}
}
  • WebMvcConfigurer를 상속받아 addInterceptors를 구현해주면 된다.
  • registry에서 addInterceptor에 우리가 만든 로그 인터셉터를 등록.
  • order(): 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.
  • addPathPatterns : 인터셉터를 적용할 URL 패턴 지정
  • excludePathPatterns: 인터셉터에서 제외할 패턴 지정

인터셉터는 addPathPatterns , excludePathPatterns 로 필터보다 매우 정밀하게 URL 패턴을 지정할 수 있다.

ArgumentResolver

번외편// ArgumentResolver를 통해 로그인 회원 관리 세션을 더 간편하게 구현 할 수 있다.

  1. 커스텀 애노테이션 만들기
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
  1. HandlerMethodArgmuentResolver(줄여서 ArgumentResolver) 구현
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

 	@Override
 	public boolean supportsParameter(MethodParameter parameter) {
    
    	//애노테이션을 가지고 있나(여기선@Login)
 		boolean hasLogin = parameter.hasParameterAnnotation(Login.class);
        
        //그 파라미터가 MemberClass인가
 		boolean hasMember = Member.class.isAssignableFrom(parameter.getParameterType());
        
        //둘을 만족할시 true로 argumentresolver 반환
		return hasLogin && hasMember;
 	}
    
 	@Override
 	public Object resolveArgument(MethodParameter parameter, 
    							  ModelAndViewContainer mavContainer,
    							  NativeWebRequest webRequest,
								  WebDataBinderFactory binderFactory)throws Exception {
                                  
 		HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
 		HttpSession session = request.getSession(false);
 		if (session == null) {
 			return null;
		}
 		return session.getAttribute(SessionConst.LOGIN_MEMBER);
 	}
}
  • supportsParameter() : @Login 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver가 사용된다.

  • resolveArgument(): 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다. 여기서는 세션에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다. 이후 스프링MVC는 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member 객체를 파라미터에 전달해준다

참고: 본 글은 김영한님의 스프링 강의를 공부목적으로 정리한 것입니다.

profile
공부 정리 블로그

0개의 댓글