Filter란
Web Application에서 관리되는 영역으로써 Spring Boot Framework에서 Client로 부터 오는 요청/ 응답에 대해서 최초/ 최종 단계의 위치에 존재하며, 이를 통해서 요청/ 응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/ 응답 값을 확인할 수 있고 유일하게 ServletRequest, ServletResponse의 객체를 변환할 수 있다.
주로 Spring Framework에서 request/ response의 Logging용도로 활용하거나, 인증과 관련된 Logic들을 해당 Filter에서 처리한다.
또한 요청으로 들어온 정보중에 노출되면 위험한 정보들이 들어 있을 수 있어 DispatcherServlet 이 처리하기 전에 Filter에서 인코딩을 해주는 식과 인증, 인가 처리를 하는 역할 또한 진행한다.
왼쪽 이미지를 보면 Filter 인터페이스는 servlet에 속해있는걸 알 수 있다.
따라서 스프링과 무관하게 전역적으로 처리해야하는 작업들을 처리할 수 있다.
즉, 웹 애플리케이션에서 전반적으로 사용되는 기능을 구현하기에 적합하다.
다시 말하자면 Filter는
- 제일 먼저 순수한 RequestBody와 ResponseBody를 확인을 할 수 있다.
- 유일하게 servletRequest, servletResponse의 객체를 변환 할 수 있다.
- 인증과 관련된 로직들을 해당 필터에서 처리한다.
라고 할 수 있다.
Filter의 Method
필터를 추가하기 위해서는 다음의 3가지 메소드를 가지고 있는 인터페이스를 구현해야한다.
1. init: 인스턴스 초기화
Filter 인스턴스가 초기화될 때 호출되는 메서드로 Filter가 처리해야 할 초기화 작업을 수행한다.
웹 컨테이너가 1회 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리된다.
default 메서드이기에 반드시 구현할 필요는 없다.
2. doFilter: 핵심로직 수행
HTTP 요청 및 응답이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드이며
요청 및 응답을 가공하거나 변경하는 등 핵심 로직을 수행한다.
FilterChain의 doFilter 메서드를 통해서 다음 대상으로 요청 전달하게 되고 이러한 doFilter메서드 전/후로 우리가 필요한 처리과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.
default 메서드가 아니기에 반드시 구현해야 한다.
3. destroy: 인스턴스 소멸
인스턴스가 소멸될 때 호출되는 메서드로 Filter가 사용한 자원을 정리하거나 해제하는 작업을 수행한다.
default 메서드이기에 반드시 구현할 필요는 없다.
Filter의 사용
필터를 사용할 때에는 jakarta.servlet 패키지에 들어있는 Filter를 구현한다.
위의 세 메서드 중에 init, destroy는 default로 구현하지 않아도 상관 없지만 doFilter는 반드시 구현해야한다.
또한 Filter를 적용시킬 때에는 임베디드 WAS경우 따로 설정할 것 없이 클래스 상단에 @Component만 붙여주면 자동으로 Filter가 등록된다. (필자는 인텔리제이를 사용하고 있어 임베디드 WAS환경이다.)
따라서 이를 위와같이 구현하고 프로젝트 생성 -> 요청 -> 종료를 하게되면 아래와 같은 결과가 나오게 된다.
그리고 이제 doFilter의 로직을 구현한다고 해보자. request에 들어온 데이터를 한 번 읽어들여 logging을 하고자 할 때, 단순하게 생각하면 아래와 같이 할 수 있을 것이다.
위 코드는 HttpServletRequest로 형변환 후 요청 URI와 Body에 담겨있는 데이터를 꺼내오는 코드이다.
위 코드가 문제 없이 잘 될거라고 예상되지만 막상 실행을 해보면 아래와 같은 에러가 발생한다.
"getReader() has already been called for this request" 와 같은 에러가 발생하는 이유는
이미 Filter에서 Reader를 한 번 끝까지 다 읽어 버렸기 때문에 Controller에서는 이를 읽을 수가 없어서 에러가 발생한다.
이 문제를 해결하기 위해 HttpServletRequest, HttpServletResponse 대신 ContentCachingRequestWrapper, ContentCachingResponseWrapper를 사용하여 처리를 한다.
ContentCachingRequestWrapper클래스는 cachedContent에 내용을 담아 몇번이고 다른 곳에서 읽을 수 있게 해준다.
이를 사용해서 다시 작성해준다.
그 후 다시 실행하면
에러 메시지는 사라졌지만 여전히 body는 읽을 수가 없는 상태이다.
이를 해결하기 위해 ContentCachingRequestWrapper클래스를 뜯어보면
생성자를 호출하면 입력한 내용이 cachedContet에 담길 것이라고 생각했지만 바이트의 길이만 담길 뿐 내용은 담기지 않는다.
실제로 cachedContent에 내용이 담길 때는 아래 사진에서 볼 수 있다.
cachedContent에 담길 때는 body의 contents를 읽어들일 때 이와 동시에 cachedContent에 담기게 된다.
다시말해서 내용을 전체 다 읽어야만 cachedContent에 온전히 담기게 된다는 것이다.
이 말은 즉 Controller에서 데이터를 전부 다 읽어 들여야만 cachedContent에 내용이 다 담기게 되니
그 후에 Logging을 해라 라는 뜻이 된다.
지금까지는 전부 전처리 단계에서 Controller에 데이터가 넘어가기 전에 읽어들이고 있었으니 순서가 잘못된 것이였다.
이를 참고해서 다시 작성해보면
chain.doFilter() 후에 Logging작업을 진행했다.
Logging하는 김에 Response값 까지 나오도록 추가해보았다.
이렇게 바꾼 코드의 결과를 확인해보면
위와 같이 Logging이 잘 되는 모습을 확인할 수 있다.
근데 한 가지 문제가 있다.
API테스트를 했던 Postman을 확인해보면
입력 값은 잘 들어갔고 status 200으로 문제 없고 Logging했을 때 데이터도 잘 찍혔지만 막상 반환하려고 하니 값이 반환되지 않는다.
이것도 위에서 값이 전달되지 않았던 이유와 똑같다. 이미 앞에서 다 읽어버렸기 때문에 Response로 내려가지 못하고 있는 것이다. 이를 해결하기 위해서는 또다시 ContentCachingResponseWrapper클래스를 뜯어보아야한다.
ContentCachingResponseWrapper에서 우리가 필요한 content에 값을 담아주는 부분은
copyBodyToResponse()라는 것을 알 수 있다. 이를 적용해서 다시 확인해보면
위와같이 잘 들어간 것을 확인할 수 있다.
여기까지가 Filter를 활용한 Logging의 시행착오이다.
Filter의 여러개 사용
만약 이러한 필터를 여러개 사용해야 할 때 사용할 수 있는 방법이 두 가지가 있다.
1. @Component + @Order: 적용 순서 설정
2. @ServletComponentScan + @WebFilter: 적용 URL 설정
3. FilterRegistrationBean 등록
1. @Order: 적용 순서 설정
클래스 위에 @Order(우선순위)를 원하는 순서에 맞게 넣어주면 (숫자가 낮을수록 높은 우선순위)
2. @WebFilter: 적용 URL 설정
지금까지와는 다르게 @Component를 제거하고 @WebFilter에 내가 적용하고자 하는 url를 작성하면 된다.
위와 같이 작성하면 /api 밑의 모든 api에 대해서 해당 필터를 적용하겠다는 뜻이다.
여기서 주의할 점은 단순히 @WebFilter만 작성하면 되는 것이 아니라 @SpringBootApplication으로 가서 @ServletComponentScan를 설정 해주어야 WebFilter를 읽어들일 수 있다.
하지만 위의 두 @Order 와 @WebFilter는
@Component 방식은 @Order로 순서는 지정할 수 있으나 url패턴 매핑이 불가
@ServletComponentScan + @WebFilter 방식은 url매핑은 가능하나 순서 지정 불가
하다는 점들이 있다.
내가 원하는 url에만 Filter를 적용하고 순서도 적용시키고 싶다면 다른 방법을 사용해야한다.
3. FilterRegistrationBean 등록
이 방법은 지금까지와는 다르게 위의 두 방법에서 붙였던 어노테이션들이 필요가 없다.
모든 어노테이션들을 제거하고
따로 해당 Filter 클래스를 Bean으로 등록하면서 urlPattern, order에 대한 설정을 해줘야한다.
@Configuration과 WebMvcConfigurer를 사용해서 @Bean 등록을 한다.
이때 Filter를 등록하는 제네릭 클래스는 FilterRegistrationBean<>으로 이름 그대로 Filter를 등록하는 Bean객체이다.
위에서는 하나만 사용했지만 여러개 등록이 가능하다.
'Back-End > Spring Boot' 카테고리의 다른 글
Spring Boot - RestTemplate를 사용한 Server To Server 연결(1) (0) | 2024.06.18 |
---|---|
Spring Boot Interceptor란? (0) | 2024.06.17 |
Spring Boot @RestControllerAdvice를 이용한 예외 처리 방법 (0) | 2024.06.09 |
AOP (0) | 2024.06.08 |
IoC와 DI (0) | 2024.06.08 |