Web/Spring

SpringMVC 2 - 서블릿 필터와 인터셉터

now0204 2023. 11. 15. 23:42

- 서블릿을 호출하기 전에 특정한 처리를 해야할 때 (공통처리) 필터를 사용함

- 여러 로직에서 공통으로 관심 있는 것을 공통 관심사라고 함 (ex 인증)

- 공통 관심사는 AOP를 사용할 수도 있지만, 웹과 관련된 관심사는 필터나 인터셉터를 사용하는 것이 좋다.

   (웹과 관련된 부가 기능을 제공해줌!)

 

* 필터나 인터셉터 구현체를 스프링 빈으로 등록한 뒤에 등록해도됨 

  만약 필터나 인터셉터 구현체를 만들기 위해 여러 의존객체가 필요하다면 빈으로 등록한 뒤 등록하도록 하자! 

 

1. 서블릿 필터

 

> HTTP 요청 > WAS > 필터 > 서블릿 > 컨트롤러 순으로 작동함

> 필터에 특정 URL 패턴을 적용할 수 있다.

> 필터는 우선순위를 설정해서 여러개 둘 수 있다. (로그필터 > 로그인 필터)

> 필터 인터페이스는 아래와 같음 

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

> 필터 또한 싱글톤으로 생성 관리됨 

 

1.1 요청 로그 필터 

 

public class LogFilter implements Filter{



	public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException{

		HttpServletRequest req = (HttpServletRequest) request;
        String requestURI = req.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);
        }

}
}

 

- uuid 하나의 트랜잭션에 부여한 아이디 

- 필터는 HTTP요청이 아닌 경우 까지 고려한 인터페이스 (HTTP 요청에선 request 다운캐스팅해서 사용하자)

- chain.doFilter를 통해 다음 Filter 호출 가능

 

* 디스팻쳐에서 발생한 에러는 필터를 통과해서 WAS까지 전달됨 중간에 오류처리하면, WAS까지 오류가 못가서 추후에 오류를 보기 위해 지금은 오류가 나면 던지게 함 

 

1.2 필터 설정 

 

@Configuration

public class WebConfig{

		@Bean
        public FilterRegisterBean logFilter(){
        
        	FilterRegisterBean<Filter> fb = new FilterRegisterBean<>();
            fb.setFilter(new LogFilter());
            fb.setOrder(1);
            fb.addUrlPatterns("/*");
            return fb;
        }
}

 

- 스프링 부트에서 필터는 위와 같이 설정함 (FilterRegisterBean이용)

- 우선순위를 지정할 수 있음 (URL 패턴의 룰은 서블릿 URL 패턴 룰)

- 어노테이션 기반 필터도 설정 가능하지만, 순서조절이 어렵다 (@WebFilter)

 

* 같은 요청에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc를 검색해보자 

 

2. 인증 체크 필터 

 

public class LoginCheckFilter implements Filter {


	private static final String[] whitelist = {"/","/members/add","/login","/logout","/css/*"};
    
	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{
         
         	if(isLoginCheckPath(requestURI)){
            	HttpSession session = httpRequest.getSession(false);
                if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
                	//미인증 유저 
                   httpResponse.sendRedirect("/login?redirectURL=" +requestURI);
                   return; //미인증 사용자는 다음 진행 없이 끝 
                }
            }
            chain.doFilter(request,response);
         }catch(Exception e){
         	throw e;
         }finally{
         	//끝
         }
	}

		
        private boolean isLoginCheckPath(String requestURI){
        	return !PatternMatchUtils.simpleMatch(whitelist,requestURI);
        }

}

 

- whitelist : 로그인과 관계 없이 접근 할 수 있는 요소지정 (ex css리소스)

- isLoginCheckPath() : 화이트 리스트 제외한 모든 경우 인증 체크 로직 검사 (*PatternMatchUtils 좋다)

- 미인증 사용자는 다시 홈화면으로 리다이렉트 : 근데 로그인 이후에 로그인 전에 접근한 경로를 달아주자 

   -> 추후 로그인 성공후 redirect 경로를 로그인 전에 시도했던 경로로 보내주자! 

- return -> 필터가 더 이상 진행하지 않는다 (서블릿,컨트롤러 호출 x) redirect 후에 요청 종료 

 

2.1 필터 등록

 

@Bean

public FilterRegisterationBean loginCheck(){
	FilterRegistrationBean<Filter> fb = new FilterRegistrationBean<>();
    fb.setFilter(); // 동일한 과정 
    fb.setOrder();
    fb.addUrlPatterns();
    return fb;
}

 

 

2.2 컨트롤러

 

@PostMapping("/login")
public String loginV4(
	@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
    @RequestParam(defaultValue = "/") String redirectURL,
    HttpServletRequest request{
    
    	if(bindingResult.hasErrors()){
        	return "login/loginForm";
        }
        
        Member loginMember = loginService.login(form.getLgoinId(),form.getPassword);
        
        if(loginMember == null){
        	bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }
        
        HttpSession session = request.getSession();
        session.setAttriute(SessionConst.LOGIN_MEMBER,loginMember);
        //redirectURL 적용
 		return "redirect:" + redirectURL;
    }
	
)

 

 

 

> 필터 덕에 로그인 x 사용자는 특정 경로 외에는 접근 불가 -> 로그인 정책 변경시 조금만 수정하면 됨 

 

*참고로 필터는 request,response 다음 서블릿이나 필터에게 넘겨줄 때 다른 객체로 바꿔서 넘겨줄 수 있다.

(잘 사용하지 않음 참고)

 

 

3. 스프링 인터셉터

 

- 서블릿 필터와 마찬가지로 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.

   (스프링 MVC가 제공하는 기술임)

- 공통 관심사를 처리하지만, 순서와 범위 및 사용법이 다르다.

 

출처: 스프링 MVC2 (김영한) 인프런

 

- 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 됨

  (스프링 MVC 기능 -> 서블릿 이후에 등장)

- 인터셉터도 URL 패턴을 적용할 수 있다. 서블릿 URL 패턴과 다름 (매우 정밀한 설정 가능)

 

출처: 스프링 MVC2 (김영한) 인프런

 

- 인터셉터에서 적절하지 않은 요청으로 판단되면, 컨트롤러 호출 없이 끝낼 수 있다 (필터와 비슷)

 

출처: 스프링 MVC2 (김영한) 인프런

- 또한 인터셉터도 체인이 자유롭다.

- 필터와 매우 비슷하지만, 더욱 정교하고 다양한 기능 지원함 

 

3.1 인터셉터 인터페이스 

 

- 스프링 인터셉터는 HandletInterceptor 인터페이스를 구현하면 된다.

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

 

- preHandle : 컨트롤러 호출 전 (핸들러 어댑터 호출 전), 위 결과가 true면 다음 진행 false면 진행 안함 

                      나머지 인터셉터와 핸들러 어댑터도 호출하지않음 

 

- postHandle: 컨트롤러 호출 후 (핸들러 어댑터 호출 후)

 

- afterCompletion : 요청 완료 이후 (뷰가 렌더링 된 이후)

 

- 인터셉터에는 handler 정보와, modelAndView의 응답 정보도 확인할 수 있다. 

 

3.2 인터셉터 예외 상황 

 

출처: 스프링 MVC2 (김영한) 인프런

 

 > 예외 발생시 : postHandle 호출 x (컨트롤러 완료x) , afterCompletion은 항상 호출 (ex 파라미터로 예외 볼 수 있음)

 

* 예외와 무관한 처리는 afterCompletion을 사용하자 

 

 

3.3 요청 로그 인터셉터 

 

public class LofInterceptor implements HandlerInterceptor{


		public static final String LOG_ID = "logId";
        
        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);
            if(handler instanceof HandlerMethod){
            	handlerMethod hm = (HandlerMethod) handler;
            }
            
            return true;
		}
        
        @Override
 		public void postHandle(HttpServletRequest request, HttpServletResponse 
		response, Object handler, ModelAndView modelAndView) throws Exception{
        	log.info("postHandle [{}]", modelAndView);
        }
        
        public void afterCompletion(HttpServletRequest request, HttpServletResponse 
		response, Object handler, Exception ex) throws Exception {
        	String requestURI = requestURI();
            String logId = (String)request.getAttribute(LOG_ID);
          	 log.info("RESPONSE [{}][{}]", logId, requestURI);
 			if (ex != null) {
 			log.error("afterCompletion error!!", ex);
 			}
        }
}

 

- request.setAttribute(LOG_ID,uuid) 

  서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전 분리 

  따라서 preHandle에서 따로 지정한 값을 다른 메서드로 보내려면 어딘가에 담아야함 

  싱글톤으로 관리됨 -> 멤버변수는 위험 -> request와 같은 객체 사용하자 

 

*인터셉터 메서드 간 데이터 주고 받을 때는 request에 담아서 주고 받자! 시점이 다르다

 

if (handler instanceof HandlerMethod) {
 HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가
포함되어 있다.
}

 

- handler의 종류를 확인하는 곳임 

 

> HandlerMehod : 어노테이션 기반 (@Controller) 핸들러일 경우 HandlerMehod가 handler로 지정됨

> ResourceHttpRequestHandler: 정적 리소스 호출은 이 핸들러가 지정되서 넘어옴 

 

3.4 인터셉터 등록

 

@Configuration

public class WebConfig implements WebMvcConfigurer{

	public void addInterceptors(InterceptorRegistry registry){
    	registry.addInterceptor(new LogInterceptor())
        		.order(1)
                .addPathPatterns("/**");
                .excludePathPatterns("/css/**","/*.ico","/error");
    }
}

 

- WebMvcConfigurer를 통해 등록할 수 있다.

- 패턴을 매우 세밀하게 지정할 수 있다. (스프링 URL)

 

4. 인증 체크 인터셉터 

 

public class LoginCheckInterceptor implements HandlerInterceptor{

	public boolean preHandle(HttpServletRequest request, HttpServletResponse 
	response, Object handler) throws Exception{
		
        String requestURI = request.getRequestURI();
        HttpSession session = request.getSession(false);
        
        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER)== null)
        {
        response.sendRedirect("/login?redirectURL=" + requestURI);
 		return false;
        }
        return true;
	}

}

 

- 필터에 비해 매우 간결함 컨트롤러 호출 전에만 호출하면 됨 

 

@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)
 .addPathPatterns("/**")
 .excludePathPatterns(
 "/", "/members/add", "/login", "/logout",
 "/css/**", "/*.ico", "/error"
 );
 }
 //...
}

 

- addPathPatterns와 exclude만 작성하면 쉽게 사용가능! 또한 세밀하게 패턴 조정 가능하다.

 

* 필터와 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술임

   필터에 비교해서 인터셉터가 작성하기 훨 편리함 -> 특별한 문제가 없다면 인터셉터 활용하자