ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SpringMVC 2 - 서블릿 필터와 인터셉터
    Web/Spring 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만 작성하면 쉽게 사용가능! 또한 세밀하게 패턴 조정 가능하다.

     

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

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

     

     

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

    Spring MVC (2) - 스프링 포맷터  (0) 2023.11.21
    Spring MVC(2) - 스프링 타입 컨버터  (0) 2023.11.21
    Spring MVC 2 - 검증 (2) BeanValidation  (0) 2023.10.25
    SpringMVC 2 - 검증  (0) 2023.10.24
    Spring MVC 2 - 국제화/메시지  (0) 2023.10.24
Designed by Tistory.