2024.09.24.화.TIL Filter Intercepter

김기남·2024년 9월 24일
post-thumbnail

안녕하세요, 오늘은 Filter 와 Intercepter 에 대해 정리해 보았습니다.

공통 관심사

💡 Filter가 나오게된 이유는 공통 관심사(Cross Cutting Concern) 때문이다. ex) 요구사항에 따라서 로그인 한 유저만 특정 API(게시글 작성)를 사용할 수 있어야 한다면?
  1. 단순히 화면에서 로그인 하지 않으면 API를 사용하지 못하도록 막는것은 한계가 있다.
    1. 유저가 HTTP 요청을 마음대로 조작하면?
  2. 게시글 작성 Controller에서 로그인 여부를 체크하는 Logic을 작성하면?
    1. 실제로는 수정, 삭제, 조회 등 모든 컨트롤러에 공통으로 로그인 여부를 체크해야 한다.
    2. 로그인 로직이 변경될 때 마다 로그인 여부를 체크하는 Logic 또한 변경될 가능성이 높다.
@Controller
@RequestMapping("/post")
public class PostController {

	@PostMapping("/")
	public void create(CreateRequestDto requestDto) {
		// 로그인 여부 확인 로직
	}

	@PutMapping("/")
	public void update(UpdateRequestDto requestDto) {
		// 로그인 여부 확인 로직
	}

	@DeleteMapping("/")
	public void delete(DeleteRequestDto requestDto) {
		// 로그인 여부 확인 로직
	}

}
  1. 위와같이 여러가지 로직에서 공통으로 관심이 있는 부분(로그인 등)을 공통 관심사 라고 한다.

    // 로그인 여부 확인 로직 -> 공통 관심사 (인증 : 로그인)
    1. 해당 예시에서 공통 관심사는 인증이다.
    2. 공통 관심사는 로그인뿐만 아니라 더 큰범위를 의미한다.
  2. 공통 관심사는 Spring AOP를 활용할 수 있다.

    1. 부가적인 기능은 많이 없다.
    2. HTTP 요청, 응답 (request, response)
  3. Web과 관련된 공통 관심사는 Servlet FilterSpring Intercepter를 사용한다.

    1. HttpServletRequest 객체를 제공하기 때문에 HTTP 정보나 URL 정보에 접근하기 쉽다.
    2. ex) HTTP Header Cookie → 인증
    3. ex ) 특정 URL은 인증을 할것이다. → URL 정보 필요

Servlet Filter

💡 Servlet Filter는 보안, 로깅, 인코딩, 인증/인가 등 다양한 작업을 처리하기 위해 사용된다. ex) 필터를 사용하여 모든 요청과 응답에 대한 로그를 저장할 수 있다.
  1. 공통 관심사 로직 → 중앙 집중적인 로직 구현
  2. HTTP 요청, 응답을 중간에 필터링할 수 있다.
  3. Filter Chain → 여러 개의 필터가 순차적으로 적용될 수 있다.
    1. filterChain.doFilter(request, response); 다음 필터로 제어를 전달한다.
  4. doFilter → 실제 필터링 작업을 수행하는 주요 메소드

Filter 적용 후

  • Filter를 적용하면 Servlet이 호출되기 이전에 Filter를 항상 거치게된다.
  • 공통 관심사를 필터에만 적용하면 모든 요청 or 응답에 적용된다 .
  • Filter는 특정 URL Pattern에 적용할 수 있다.
    • ex) /* : 모든 요청 필터
  • Spring을 사용하는 경우 Servlet은 Dispatcher Servlet이다.

인증/인가 필터 예시

  • 로그인 성공시 정상적으로 Controller를 호출한다.

  • Filter에서 적절하지 않은 요청이면 Controller를 호출하지 않는다.

Filter Chain

  • Filter는 Chain 형식으로 구성된다.
  • Filter는 개발자가 자유롭게 추가할 수 있다.
  • Filter는 순서를 지정하여 추가할 수 있다.

Filter Interface

  • jakarta.servlet.Filter
package jakarta.servlet;

import java.io.IOException;

public interface Filter {
	
	// default
	public default void init(FilterConfig filterConfig) {
	}

	public void doFilter(
		SevletRequest request, 
		SevletResponse response,
		FilterChain chain
	) throws IOException, ServletException;
	
	// default
	public default void destroy() {
	}

}
  • Filter Interface를 Implements하여 구현하고 등록하면
    Servlet Container가 Filter를 Singleton 객체로 생성 및 관리한다.

  • doFilter()

    • Client에서 요청이 올 때 마다 doFilter() 메서드가 호출된다.
      • doFilter() 내부에 필터 로직(공통 관심사 로직)을 구현하면 된다.
    • WAS에서 doFilter() 를 호출해주고 하나의 필터의 doFilter()가 통과된다면
    • Filter Chain에 따라서 순서대로 doFilter() 를 호출한다.
    • 더이상 doFilter() 를 호출할 Filter가 없으면 Servlet이 호출된다.
  • init()

    • Filter를 초기화하는 메서드이다.
    • Servlet Container가 생성될 때 호출된다.
    • default method이기 때문에 implements 후 구현하지 않아도 된다.
  • destroy()

    • 필터를 종료하는 메서드이다.
    • Servlet Container가 종료될 때 호출된다.
    • default method이기 때문에 implements 후 구현하지 않아도 된다.

Servlet Filter 구현

  • Filter 구현체
@Slf4j
public class CustomFilter implements Filter {

    @Override
    public void doFilter(
			ServletRequest request, 
			ServletResponse response, 
			FilterChain chain
		) throws IOException, ServletException {
	       
				// Filter에서 수행할 Logic
				HttpServletRequest httpRequest = (HttpServletRequest) request;
				String requestURI = httpRequest.getRequestURI();
				
				try {
					log.info("request URI={}", requestURI);
					
					chain.doFilter(request, response); // chain 이 없으면 Servlet을 바로 호출
				} catch(Exception e) {
					throw e;
				}
			
    }

}
  • ServletRequest 는 기능이 별로 없다.
    • 대부분 기능이 많은 HttpServletRequest 를 사용한다.(다운 캐스팅)
  • Filter 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
		
		@Bean
		public FilterRegistrationBean customFilter() {
				FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
				filterRegistrationBean.setFilter(new CustomFilter()); // Filter 등록
				filterRegistrationBean.setOrder(1); // Filter 순서 설정
				filterRegistrationBean.addUrlPatterns("/*"); // 전체 URL에 Filter 적용
				
				return filterRegistrationBean;
		}
}
  • Postman 호출
    • GET /test 에 Mapping되는 Controller는 만들지 않았다.

  1. filterRegistrationBean.addUrlPatterns("/*")
    1. 모든 Request는 Custom Filter를 항상 지나간다.
  2. 존재하지 않는 URL Mapping(”/test”)이지만 Console에 URI가 log로 출력된것을 볼 수 있음.
    • 즉, Controller보다 앞에서 Filter가 동작한다.
  3. Filter의 doFilter 로직이 끝나면 다음 Filter의 doFilter가 호출된다.(위 코드엔 Filter가 하나밖에 없음)
  4. 모든 Filter의 doFilter() 메서드가 정상적으로 실행되면 Controller를 호출한다.

정리

  1. Filter를 사용하려면 Filter Interface를 Implements 하여 구현해야 한다.

  2. HTTP 요청이 오면 doFilter() 메서드가 호출된다.

    1. ServletRequest는 기능이 별로 없으니 HttpServletRequest로 다운캐스팅 해야한다.
  3. chain.doFilter(request, response)

    1. (순서를 설정해둔)다음 필터가 있으면 Filter를 호출한다.

    2. 다음 실행할 필터가 없으면 Servlet을 호출한다.

    3. 해당 메서드를 호출하지 않으면 다음 단계로 진행되지 않는다.

      → 다음 필터나 Servlet을 호출하지 않는다.

  • Filter를 등록하는 방법은 여러가지가 있다. SpringBoot의 경우 FilterRegistrationBean 사용
    • setFilter()
      • 등록할 필터를 파라미터로 전달하면 된다.
    • setOrder()
      • Filter는 Chain 형태로 동작한다.
      • 즉, 실행될 Filter들의 순서가 필요하다.
      • 파라미터로 전달될 숫자에 따라 우선순위가 정해진다.
      • 숫자가 낮을수록 우선순위가 높다.
    • addUrlPatterns()
      • 필터를 적용할 URL 패턴을 지정한다.
      • 여러개 URL 패턴을 한번에 지정할 수 있다.
      • 규칙은 Servlet url-pattern과 같다.
  • @WebFilter 어노테이션은 필터 등록이 가능하지만 순서 조절이 안되니 사용하지 않는다.

Servlet Filter - 인증

💡 Servlet Filter를 사용하여 인증 처리를 하는 방법에 대해 알아보자.

Servlet Filter 구현

  • Login Filter 구현체
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;

@Slf4j
public class LoginFilter implements Filter {

		// 인증을 하지 않아도될 URL Path 배열
		private static final String[] whiteListURL = {"/", "/user/signup", "/login", "/logout"};

    @Override
    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 {
					log.info("로그인 필터 로직 실행");
					
					// 인증 필터 로직
					// 로그인을 체크 해야하는 URL인지 검사
					// whiteListURL이 아닌경우 true 반환
					// 즉, 로그인 여부를 확인해야하는 URL인 경우
					if (isLoginPath(requestURI)) {
						
						// 로그인 확인 -> 로그인하면 session에 값이 저장되어 있다는 가정.
						// 세션이 존재하면 가져온다. 세션이 없으면 session = null
						HttpSession session = httpRequest.getSession(false);
						
						// 로그인하지 않은 사용자인 경우
						if (session == null || session.getAttribute("sessionKey값") == null) {
							throw new RuntimeException("로그인 해주세요.");
						}

						// 로그인 성공 로직

					}
					
					// 1번경우 : whiteListURL에 등록된 URL 요청이면 바로 chain.doFilter()
					// 2번경우 : 필터 로직 통과 후 다음 필터 호출 chain.doFilter()
					// 다음 필터 없으면 Servlet -> Controller 호출
					chain.doFilter(request, response);

				} catch(Exception e) {
					// 예외처리
				}

    }
	
		// 로그인 여부를 확인하는 URL인지 체크하는 메서드
		private boolean isLoginPath(String requestURI) {
			// request URI가 whiteListURL에 포함되는지 확인
			// 포함되면 !true = false 반환
			// 포함되지 않으면 !false = true 반환
			return !PatternMatchUtils.simpleMatch(whiteListURL, requestURI);
		}

}

Filter 등록

@Configuration
public class WebConfig {
		
		@Bean
		public FilterRegistrationBean customFilter() {
				FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
				filterRegistrationBean.setFilter(new CustomFilter()); // Filter 등록
				filterRegistrationBean.setOrder(1); // Filter 순서 1 설정
				filterRegistrationBean.addUrlPatterns("/*"); // 전체 URL에 Filter 적용
				
				return filterRegistrationBean;
		}

		@Bean
		public FilterRegistrationBean loginFilter() {
				FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
				filterRegistrationBean.setFilter(new LoginFilter()); // Filter 등록
				filterRegistrationBean.setOrder(2); // Filter 순서 2 설정
				filterRegistrationBean.addUrlPatterns("/*"); // 전체 URL에 Filter 적용
				
				return filterRegistrationBean;
		}
}
  • loginFilter() 메서드 내부에서 addUrlPatterns() 를 활용하여 URL 패턴을 지정할 수 있다.

    • 내부에 whiteListURL이 존재하기 때문에 적지않음
    • addUrlPatterns() 에 URL 패턴으로 지정해두면 추후 수정할 가능성이 높음
      • 새로운 URL Mapping이 있는 Controller에 인증 확인을 해야할 경우
      • ex) “/post/*”
  • Filter가 많아질수록 성능적으로 문제가 발생하지는 않나요?

    • 이정도는 괜찮습니다 여러분. 거의 없습니다.
    • 애초에 여태까지 배웠던 Spring MVC 구조를 보았다시피
      요청이 한번오면 수십 수백 수천가지의 메서드가 호출됩니다.
  • Postman

    • 이미 만들어져있는 /api/save API 호출을 가정한다.

    • 로그인하지 않고 (session이 비어있는 채로 요청) 요청한다.

throw new RuntimeException("로그인 해주세요."); 실행

  • 필터가 순서대로 출력되는것을 확인
  1. CustomFilter의 Log인 request URI 출력
  2. LoginFilter의 Log인 로그인 필터 로직 실행 출력
  • 위 코드예시 동작흐름

정리

  1. whiteListURL
    1. LoginFilter를 적용하지 않을 URL 배열
  2. isLoginPath()
    1. wihteListURL 을 제외한 모든 경우에 인증 체크를 하도록 만든 메서드
  3. throw new RuntimeException()
    1. Login하지 않은 경우 == Session이 없는 경우 예외처리
  • Servlet Filter를 사용한 덕분에 로그인 인증 처리를할 수 있었다.
  • 공통 관심사를 Servlet Filter로 해결하였다.
  • 추후에 로그인 인증 로직이 바뀌거나 whiteListURL이 추가되어도 LoginFilter만 수정하면 된다.
    • 즉, 단일책임 원칙을 잘 지켰다.
  • Filter를 여기까지만 배워도 Spring Security 혹은 어떠한 인증 로직이던 다 직접 구현하면 된다.

Spring Interceptor

💡 Servlet Filter와 같이 Web과 관련된 공통 관심사를 해결할 수 있는 기술중에 하나이다. Servlet Filter는 Servlet이 제공하는 기술이다. Spring Interceptor는 Spring이 제공하는 기술이다.

두 기술의 차이점은 적용 순서, 범위, 사용방법 등이 있다. 아래 내용을 통해 알아보자

  • 기능 제공수를 따지고보면 Spring Interceptor가 더 많이 제공한다.

동작 흐름

  • 참고 : 아래 이미지에서 Servlet은 Dispatcher Servlet이다.

  • Request → WAS → Filters → Servlet → Spring Intercepter → Controller
  • Spring Intercepter는 Dispatcher Servlet과 Controller 사이에 존재한다.(호출된다.)
    • Spring MVC의 Front Controller인 Dispatcher Servlet 이후에 호출.
  • Spring Intercepter 또한 URL Pattern을 적용할 수 있다.
    • 위에서 나왔던 Servlet URL Pattern 과는 다르다.
    • 더욱 정밀하게 설정할 수 있다.

Spring Interceptor 인증/인가

  • 로그인 O

    • 인증 성공시 Controller 정상호출
  • 로그인 X

    • 인증 실패시 Controller 호출하지 않음

Spring Interceptor 또한 Filter와 같이 인증 역할을 수행할 수 있다.

Spring Interceptor Chain

  • Servlet Filter와 마찬가지로 Chain으로 구성된다.

  • Interceptor를 자유롭게 추가하여 순서대로 적용할 수 있다.
  • Servlet Filter와 다른점
    1. 호출되는 순서
    2. 편리하고 다양한 기능 제공

Spring Interceptor Interface

  • HandlerIntercepter Interface를 구현하면 Spring Intercepter를 사용할 수 있다.

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

	// 요청 완료 이후
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}
  • Servlet Filter는 doFilter() 메서드 기능 하나만 제공이 된다.
    • request, response 제공
  • Intercepter의 기능
    • 어떤 Controller(Handler)가 호출되었는지 호출 정보를 받을 수 있다.
    • 어떤 ModelAndView 객체가 반환되는지 Response 정보를 받을 수 있다.
    1. preHandle() : Controller 호출 전
    2. postHandl() : Controller 호출 후
    3. afterCompletion() : 요청 완료 이후

Spring Interceptor의 동작 흐름

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

	// 요청 완료 이후
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}
  1. preHandle

    1. HandlerAdapter가 호출되기 전에 호출
    2. HandlerAdapter 호출 후에 Controller 호출
    3. preHandle의 응답값이 true면 다음으로 진행
    4. preHandle의 응답값이 false면 진행하지 않음
      1. 나머지 Intercepter, HandlerAdapter, Controller 모두 호출되지 않는다.
  2. postHandle

    1. Controller 호출된 후에 HandlerAdapter 호출
    2. HandlerAdapter가 호출된 후에 호출
  3. afterCompletion

    1. View가 Rendering 된 이후 호출

Spring Interceptor 예외

💡 Controller에서 예외가 발생하면 어떻게 동작하는가? 예외 상황을 통해 PostHandle과 AfterCompletion의 차이점을 이해해보자.

  1. preHandle
    1. 정상 호출
  2. postHandle
    1. Controller에서 예외가 발생하면 호출되지 않는다.
  3. afterCompletion
    1. 예외가 발생하거나 발생하지 않거나 항상 호출된다.
    2. Exception을 파라미터로 받아서 어떤 Exception인지 출력이 가능하다.
    3. 아래 코드에서 파라미터 @Nullable Exception을 확인할 수 있다.
    4. 정상적인 요청과 응답이 이루어졌다면 Exception은 파라미터로 넘어오지 않는다.

public interface HandlerIntercepter {

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

// 요청 완료 이후
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
		@Nullable Exception ex) throws Exception {
}

}

정리

  1. 예외가 발생하면 postHandle 은 호출되지 않는다.
    1. 즉, 예외가 발생하여도 공통 관심사를 처리하고 싶다면 afterCompletion 을 사용해야한다
    2. 예외가 발생하면 afterCompletion() 의 파라미터에 Exception이 전달된다.
  2. Spring Intercepter는 Spring MVC 구조에 알맞는 Filter 기능이라고 생각하면 된다.
    1. Spring에서는 ServletFilter를 꼭 사용해야하는 이유가 없다면 Intercepter가 더 편리하다.
  3. Intercepter의 구현은 시간 관계상 우선 생략.
  4. 요청 Mapping에 따라서 파라미터로 Handler 정보가 파라미터로 함께 전달된다.
  • 설정 예시
@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new CustomInterceptor())
						.order(1)
						.addPathPatterns("/**")
						.excludePathPatterns("/error");
	}
}
  1. WebMvcConfigurer가 제공하는 addInterceptors() 사용하여 인터셉터를 등록하면 된다.
  2. Order() : 필터체인과 마찬가지로 인터셉터의 호출 순서를 지정할 수 있다.
  3. addPathPatterns() : 인터셉터를 적용할 URL Patterns을 지정한다.
  4. excludePathPatterns() : 인터셉터에서 제외할 URL Patterns를 지정한다.

오늘의 코드카타

import java.util.ArrayList;
import java.util.List;

class Solution {

    public String[] solution(String my_str, int n) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < my_str.length(); i += n) {
            list.add(my_str.substring(i, Math.min(i + n, my_str.length())));
        }
        return list.toArray(new String[list.size()]);
    }
}
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {

        Solution solution = new Solution();

        System.out.println(Arrays.toString(solution.solution("abc1Addfggg4556b", 6)));
        System.out.println(Arrays.toString(solution.solution("abcdef123", 3)));
    }
}

잘라서 배열로 저장하기

profile
새로운 시작~!

0개의 댓글