[Spring] Filter 중복 호출되는 경우와 OncePerRequestFilter를 통한 처리
목표
- 하나의 request에서 Filter의 중복 호출되는 사례들을 알아보자
- OncePerRequestFilter를 사용함으로써 Filter 중복 호출 방지
Spring Boot를 사용해 web application을 개발하다보면 Filter를 구현해서 적용하는 일이 반드시 생기게 됩니다. Filter를 사용하다보면 Filter가 중복 호출되는 경우가 발생하게 되는데요. 어떤 경우에 이런 Filter 중복 호출 현상이 발생하는지에 대해 알아보고 OncePerRequestFilter를 통해서 이러한 현상을 방지하는 것까지 정리해보려합니다.
(참고로 예시 코드는 kotlin 입니다.)
📌 1. 기본적인 Filter 구성
class FirstFilter : Filter { companion object : KLogging() override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { val req = request as HttpServletRequest logger.info { "#### [FirstFilter] [${req.dispatcherType}] request uri ${req.requestURI} ####" } chain.doFilter(request, response) logger.info { "#### [FirstFilter] after doFilter ####" } } } class SecondFilter : Filter { companion object : KLogging() override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { val req = request as HttpServletRequest logger.info { "#### [SecondFilter] [${req.dispatcherType}] request uri ${req.requestURI} ####" } chain.doFilter(request, response) logger.info { "#### [SecondFilter] after doFilter ####" } } }
기본적인 서블릿 Filter 인터페이스를 구현하였고 테스트를 위해 두 개의 Filter를 만들었습니다.
@Configuration class FilterConfig { @Bean fun firstFilter(): FilterRegistrationBean<Filter> { return FilterRegistrationBean<Filter>().apply { this.filter = FirstFilter() this.order = 1 } } @Bean fun secondFilter(): FilterRegistrationBean<Filter> { return FilterRegistrationBean<Filter>().apply { this.filter = SecondFilter() this.order = 2 } } }
따로 Config file 구성해서 따로 Spring Bean으로 두 개의 필터를 등록했습니다. "setOrder"를 통해 순서까지만 설정하였습니다.
@Controller @RequestMapping("/api/filter") class SpringFilterController { @ResponseBody @GetMapping("/test") fun filterTest(): String { return "ok" } }
테스트를 위한 api 호출을 위해 test controller를 만들고 테스트용 handler도 하나 적용했습니다.(GET Method)
위와 같이 세팅하고 application을 실행하여 api를 호출해보면 다음과 같이 로그 결과가 나와야 합니다.

test api 호출시 request 유입에서는 config file에 적용했던 순서대로 FirstFilter > SecondFilter 순으로 호출이 되고 response 내보내는 과정에서는 반대로 SecondFilter > FirstFilter 순으로 호출되는 것을 확인할 수 있습니다.
기본적인 Filter의 호출되는 과정을 봤습니다.
이렇게 일반적인 케이스에서는 Filter가 한 번씩만 호출되지만 하나의 request에서 하나의 Filter가 중복으로 여러 번 호출되는 경우가 존재합니다. 이러한 케이스에 대해서 한 번 알아보겠습니다.
📌 2. Filter 중복 호출 사례
🔖 2-1. forward 처리시
forward는 redirect와 비교내용으로 많이 언급되는 페이지 전환 기법입니다. 둘의 차이점에 대해서는 구글에 검색해보시면 자세한 내용 확인할 수 있을 것입니다.(요즘은 ChatGPT로...)
forward 방식은 쉽게 말해서 client 최초 요청 그대로 다른 url로 바로 전달하는 방식입니다. 새로운 url로 새로운 요청정보를 가지고 client에서 다시 request를 보내는 redirect 방식과 다르게 서버 내부에서 요청정보 그대로 새로운 url로 요청을 알아서 전달해준다는 차이점이 있습니다.
여기에서 하나의 request에서 Filter 중복 호출 이슈가 발생할 수 있습니다.
@Bean fun firstFilter(): FilterRegistrationBean<Filter> { return FilterRegistrationBean<Filter>().apply { this.filter = FirstFilter() this.order = 1 this.setDispatcherTypes(DispatcherType.REQUEST) } } @Bean fun secondFilter(): FilterRegistrationBean<Filter> { return FilterRegistrationBean<Filter>().apply { this.filter = SecondFilter() this.order = 2 this.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD) } }
Filter를 등록했던 config file에서 SecondFilter에는 DispatcherType으로 FORWARD 타입을 추가해서 등록합니다.
@GetMapping("/forward") fun forward(): String { return "forward:/api/filter/test" }
그 다음에 위에서 만들었던 Controller에 Spring의 forward 기능을 적용한 api handler를 추가합니다.
이렇게 되면 하나의 요청에서 GET /api/filter/forward 요청시 > GET /api/filter/test로 forwarding 될 것입니다.
한 번 결과를 확인해볼까요.

SecondFilter가 두 번 호출된 것을 확인할 수 있습니다.
forwarding시 filter들이 다음과 같이 처리됨을 알 수 있습니다.
1. GET /api/filter/forward 요청
2. FirstFilter (REQUEST)
3. SecondFilter (REQUEST)
4. api 내부에서 /api/filter/test api로 forwarding 처리
5. SecondFilter (FORWARD)
6. GET /api/filter/test handler 내부에 "ok" return
7. SecondFilter (FORWARD) - filterChain doFilter 이후 프로세스 진행
8. SecondFilter (REQUEST) - filterChain doFilter 이후 프로세스 진행
9. FirstFilter (REQUEST) - filterChain doFilter 이후 프로세스 진행
10. client에 response 반환
FORWARD dispatcherType을 등록한 SecondFilter에 대해서 하나의 요청에 대해 두 번 호출된 것을 확인할 수 있었습니다.
🔖 2-2. Spring MVC error 처리시
Spring MVC는 구현한 api 내부 로직에서 exception 발생시 try catch로 따로 잡아내지 않으면 tomcat까지 에러내용이 전달됩니다.
김영한님의 스프링 MVC 2편 강의에서 Spring MVC의 error handling에 대해서 자세하게 설명해주고 있는데요.
1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error) -> View
컨트롤러 내부 로직에서 Exception 발생시 따로 handling하지 않으면 tomcat(WAS)까지 해당 예외가 전달이되고 default로 설정되어 있는 /error url을 다시 요청하게 됩니다. 이 과정에서 같은 필터가 중복 호출이 될 수 있습니다.
이부분에 대해서 한 번 실제로 확인해보겠습니다.
@Bean fun secondFilter(): FilterRegistrationBean<Filter> { return FilterRegistrationBean<Filter>().apply { this.filter = SecondFilter() this.order = 2 this.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR) } }
SecondFilter에 dispatcherType으로 ERROR type을 추가 적용해보겠습니다.
@GetMapping("/error") fun error() { throw RuntimeException("error test") }
RuntimeException을 던지는 api handler를 하나 추가해보겠습니다. 해당 api를 요청하면 다음과 같은 결과가 나옵니다.

SecondFilter가 REQUEST 때 한 번 호출되고 Exception 발생 이후에 ERROR type으로 한 번 더 호출되는 것을 확인할 수 있습니다.
위의 두 개의 사례로 하나의 요청에서 같은 Filter가 중복 호출될 수 있음을 알 수 있었습니다. 위의 사례는 테스트용으로 보여드리기 위해 간단한 상황을 코드로 적용해본 것인데요. 실제로는 아주 복잡한 상황에서 복잡한 프로세스에 의해 Filter가 중복 호출될 수 있기 때문에 개발시 이부분에 대해서 따로 체크를 하시는 것이 좋습니다.
불필요한 중복 호출을 방지하기 위해 OncePerRequestFilter을 제공하고 있는데요. 이것을 한 번 적용해보겠습니다.
📌 3. OncePerRequestFilter를 통한 중복 호출 방지 적용해보기
class OnceFilter : OncePerRequestFilter() { private val log = KotlinLogging.logger {} override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain, ) { log.info { "#### [*OnceFilter*] [${request.dispatcherType}] request uri ${request.requestURI} ####" } filterChain.doFilter(request, response) log.info { "#### [*OnceFilter*] after doFilter ####" } } }
두 개의 Filter(FirstFilter, SecondFilter)와 다르게 OncePerRequestFilter를 구현하고 있습니다. 위와 같이 간단하게 새로운 필터를 만들고 다음과 같이 설정파일에 Bean으로 적용해줍니다.
@Bean fun onceFilter(): FilterRegistrationBean<Filter> { return FilterRegistrationBean<Filter>().apply { this.filter = OnceFilter() this.order = 3 this.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR) } }
SecondFilter와 똑같이 dispatcherType으로 REQUEST, FORWARD, ERROR 모두 적용해줍니다.
이런 상태에서 forward, exception 발생 상황에 대해서 중복 호출이 발생하는지 확인해보겠습니다.


SecondFilter와 똑같은 dispatcherType을 적용했음에도 forward, exception 발생상황에서 OncePerRequestFilter를 구현한 Filter는 한 번만 호출된 것을 확인했습니다.
📌 정리
OncePerRequestFilter가 Filter 중복 호출을 방지하고 클래스 이름 그대로 하나의 Request에 한 번만 호출되도록 하는 필터인 것은 전에도 알고 있던 내용이었고 그대로 적용했었는데요.
중요한 것은 어떤 상황에서 하나의 요청에서 같은 Filter가 중복호출이 되는지에 대해서 막연하게만 알았는데 이번을 계기로 좀더 구체적으로 알게 되었습니다. 간단하게 구글링해서 나온 코드를 단순히 복붙으로 적용하는 것보다는 왜 이러한 것이 나오게 됐는지에 대한 배경과 이유를 알고 사용하는 것이 좋다고 생각해서 장황하게 정리를 해보았습니다.
필터 중복 호출은 불필요한 리소스 낭비차원에서 방지해야한다고 생각할 수 있지만,
그것보다 인증, 인가 과정에서 하나의 요청에 대해 불필요한 인증 작업을 두 번이상 진행할 수도 있는 점을 고려해보았을 때 요청 처리 과정에서 치명적인 결함이 발생할 수 있기 때문에 중복 호출 이슈는 가볍게 넘기고 가야할 이슈는 아니라고 생각합니다.
상황에 따라 필수적으로 하나의 요청에 단 한 번만 필터가 적용해야할 필요가 있다면 OncePerRequestFilter를 구현해서 적용하시면 될 것 같습니다.
'Spring' 카테고리의 다른 글
[Vault] Spring Boot에 vault secret 정보를 적용해보자 (0) | 2023.09.05 |
---|---|
[Spring] HTTP Request, Response 내용을 logging 적용해보기 (2) | 2023.05.17 |
[Spring] 설정파일과 Bean 사이의 순환참조(circular references) 이슈 및 해결 (0) | 2022.10.31 |
[Spring Core #2] 스프링 컨테이너와 스프링 빈 (스프링 핵심 원리 강의정리) (0) | 2022.07.12 |
[Spring Core #1] 스프링의 객체 지향 원리 적용 (스프링 핵심 원리 강의정리) (0) | 2022.07.01 |
댓글
댓글(Github)
다른 글
-
[Vault] Spring Boot에 vault secret 정보를 적용해보자
[Vault] Spring Boot에 vault secret 정보를 적용해보자
2023.09.05이번 게시글은 Spring Boot Applicaiton에 vault secret 데이터들을 적용했던 내용을 정리하는 글입니다. vault 서버가 준비가 안되어있다면 이전에 제가 작성한 글이나 구글링을 통해 vault 설치를 먼저하시는 것을 추천드립니다. https://beaniejoy.tistory.com/100 Vault 서버를 설치해보자(AWS, Lightsail에 vault 서버 구축해보기) Spring Boot 애플리케이션을 개발하다보면 민감한 정보들을 설정해야할 때가 있습니다. DB 연동시 필수적으로 입력해야 하는 jdbc url, username, password 정보도 있고 Security 인증 관련해서 토큰 발급을 beaniejoy.tistory.com 📌 1. Spring Boot 프로… -
[Spring] HTTP Request, Response 내용을 logging 적용해보기
[Spring] HTTP Request, Response 내용을 logging 적용해보기
2023.05.17목표 - Spring Boot Application에서 HTTP Request, Response 내용에 대해서 logging을 적용해본다. 효과 - logging 적용을 통해 HTTP 요청마다 요청 전문과 응답 전문 내용 확인이 가능해진다. - logging을 통해 오류에 대한 디버깅과 원인 추적을 더욱 쉽고 빠르게 할 수 있다. 실무든 개인 프로젝트든 애플리케이션을 개발하거나 운용할 때 여러 에러를 마주하게 됩니다. 에러를 마주하게 되면 원인을 알아야 해결방법을 생각할 수 있기 때문에 에러 발생 원인을 찾는 것이 아주 중요합니다. 실제 업무하면서도 에러 발생 원인을 찾는데에 많은 시간을 할애하게 됩니다. 여러 단서들을 이곳 저곳에서 확인하고 원인을 추적해나가는데요. http request, respon… -
[Spring] 설정파일과 Bean 사이의 순환참조(circular references) 이슈 및 해결
[Spring] 설정파일과 Bean 사이의 순환참조(circular references) 이슈 및 해결
2022.10.31Spring Security만을 사용해서 개인 프로젝트에 간단한 회원가입과 인증 프로세스를 개발하면서 부딪혔던 내용 중 하나를 정리하고자 합니다. Spring Security 설정파일 작성 후 애플리케이션 실행 시 발생했던 순환참조(circular references, dependency cycle)에 대해 기록한 내용입니다. 📌 1. 개발했던 내용 오직 Spring Security 내용을 가지고 인증 프로세스를 구현했던 내용을 정말 간단하게 요약하고 문제상황을 보여드리는게 좋을 것 같습니다. /** * 실제 인증 절차 수행 * @property userDetailsService email로 계정 찾기 */ @Component class ApiAuthenticationProvider( private va… -
[Spring Core #2] 스프링 컨테이너와 스프링 빈 (스프링 핵심 원리 강의정리)
[Spring Core #2] 스프링 컨테이너와 스프링 빈 (스프링 핵심 원리 강의정리)
2022.07.12Index 📌 스프링 컨테이너 생성 - 스프링 컨테이너 생성 과정 📌 컨테이너에 등록된 모든 빈 조회 📌 스프링 빈 조회 - 기본 - 스프링 빈 조회 (동일한 타입 둘 이상인 경우) - 스프링 빈 조회 (상속 관계) 📌 BeanFactory와 ApplicationContext - BeanFactory - ApplicationContext 📌 다양한 설정 형식 지원 - XML, 설정 클래스 파일 📌 스프링 빈 설정 메타 정보 - BeanDefinition 📌 정리 해당 내용은 강의 내용을 기억하기 위한 정리글입니다. 자세한 내용은 강의에서 확인하실 수 있습니다. (저는 코틀린 베이스로 강의를 진행하였고 게시글의 코드 예시는 대부분 코틀린으로 이루어져 있습니다.) 스프링 핵심 원리 - 기본편(김영한님) #광…
댓글을 사용할 수 없습니다.