어떤 부분을 리팩터링하려 하나요?
보안 필터 3개가 서블릿 컨테이너와 스프링 시큐리티 필터 체인에 이중으로 등록되고 있습니다. 현재는 실행이 중복되지 않지만, 우연한 조건에 의존하고 있어 필터를 빈으로 두지 않고 SecurityConfiguration에서 직접 생성해 사용하도록 변경하려 합니다.
현재 상황
ExceptionHandlerFilter, SignOutCheckFilter, TokenAuthenticationFilter는 모두 @Component로 선언되어 있고, 동시에 SecurityConfiguration#filterChain에서 addFilterBefore()로 시큐리티 필터 체인에 추가됩니다.
security/filter/ExceptionHandlerFilter.java:20
security/filter/SignOutCheckFilter.java:18
security/filter/TokenAuthenticationFilter.java:21
security/config/SecurityConfiguration.java:74-76
Spring Boot는 컨텍스트에 등록된 Filter 타입 빈을 ServletContextInitializerBeans를 통해 서블릿 컨테이너에도 자동 등록합니다. 이를 막는 FilterRegistrationBean 설정이 없으므로(현재 프로젝트의 유일한 사용처는 common/config/web/WebMvcConfig.java:53), 세 필터 모두 서블릿 컨테이너 + 시큐리티 필터 체인에 이중 등록된 상태입니다.
실행은 왜 중복되지 않는가
다음 두 가지가 겹쳐서 중복 실행을 막고 있습니다.
- 등록 순서 —
springSecurityFilterChain(FilterChainProxy)은 SecurityProperties.DEFAULT_FILTER_ORDER(-100)로 등록되고, 자동 등록된 @Component 필터는 Ordered.LOWEST_PRECEDENCE입니다. 따라서 시큐리티 체인이 바깥쪽에서 먼저 실행되고, 자동 등록된 복제본은 그 안쪽에서 나중에 실행됩니다.
OncePerRequestFilter — 세 필터 모두 이를 상속합니다. 시큐리티 체인에서 먼저 실행될 때 <빈이름>.FILTERED 요청 어트리뷰트가 설정되고, 뒤에 도달하는 컨테이너 등록분은 동일 인스턴스·동일 빈 이름이라 어트리뷰트 이름이 같아 doFilterInternal을 건너뜁니다.
실제 요청 흐름:
HttpLoggingFilter (HIGHEST_PRECEDENCE)
└─ FilterChainProxy (-100)
└─ ExceptionHandlerFilter → SignOutCheckFilter → TokenAuthenticationFilter → ... → 인가
└─ 자동 등록된 복제본 3개 (LOWEST_PRECEDENCE, FILTERED 어트리뷰트로 인해 no-op)
└─ DispatcherServlet
즉 인증이 두 번 수행되거나 SecurityContext가 덮어써지는 실질적 버그는 현재 없습니다.
그럼에도 개선이 필요한 이유
현재의 안전성은 위 두 조건에 우연히 의존하고 있습니다.
- 필터 중 하나라도
OncePerRequestFilter가 아닌 Filter / GenericFilterBean으로 변경되면 즉시 두 번 실행됩니다.
web.ignoring()을 추가하거나 securityMatcher를 가진 두 번째 SecurityFilterChain을 도입하면, 시큐리티 체인을 타지 않는 경로에서도 컨테이너에 등록된 복제본이 실행되어 의도치 않게 인증/차단 로직이 동작합니다.
- 컨테이너 레벨의 세 필터는 order가 모두 동일해서
ExceptionHandlerFilter → SignOutCheckFilter → TokenAuthenticationFilter 순서가 보장되지 않습니다. 예외 변환 필터가 뒤로 밀리면 CustomException이 JSON 응답으로 변환되지 않을 수 있습니다.
AS-IS
- 보안 필터 3개가
@Component로 선언되어 서블릿 컨테이너와 시큐리티 필터 체인에 이중 등록됨
- 중복 실행은 필터 등록 순서와
OncePerRequestFilter의 FILTERED 어트리뷰트에 암묵적으로 의존해 막히고 있음
TO-BE
- 보안 필터는 빈으로 등록되지 않고
SecurityConfiguration에서 직접 생성되어 시큐리티 필터 체인에만 추가됨
- 이중 등록 가능성 자체가 사라져, 필터 구현이나 시큐리티 설정이 바뀌어도 중복 실행 위험이 없음
해결 방법
세 필터의 프로덕션 사용처는 SecurityConfiguration 하나뿐이므로, 컴포넌트 스캔 대상으로 둘 이유가 없습니다. @Component를 제거하고 filterChain() 안에서 직접 생성합니다.
@Bean으로 선언하면 동일하게 자동 등록되므로, 반드시 filterChain() 내부에서 생성해 그 자리에서 소비해야 합니다.
영향 없는 항목
- 성능 —
filterChain()은 컨텍스트 초기화 시 한 번만 호출되므로 인스턴스 수와 수명이 기존 싱글톤 빈과 동일합니다. 오히려 컨테이너에 등록되던 no-op 필터 3개가 사라져 요청당 필터 통과가 3번 줄어듭니다.
- 스레드 안전성 — 세 필터 모두 가변 상태 없이 주입 의존성만 필드로 가지며, 지금도 단일 인스턴스를 공유하고 있습니다.
OncePerRequestFilter 동작 — 빈이 아니므로 getFilterName()이 빈 이름 대신 클래스명으로 대체되지만, 유일성이 보장되어 영향이 없습니다. initFilterBean()을 오버라이드한 필터도 없습니다.
작업 상세 내용
참고할만한 자료(선택)
HttpLoggingFilter는 @Component이면서 WebMvcConfig.java:52-58에서 FilterRegistrationBean으로도 등록되는데, Spring Boot가 등록 빈이 참조하는 필터를 "이미 처리됨"으로 표시하므로 중복 등록되지 않고 HIGHEST_PRECEDENCE로 한 번만 등록됩니다. 이 부분은 문제 없습니다.
org.springframework.boot.web.servlet.ServletContextInitializerBeans
org.springframework.boot.autoconfigure.security.SecurityProperties#DEFAULT_FILTER_ORDER
어떤 부분을 리팩터링하려 하나요?
현재 상황
ExceptionHandlerFilter,SignOutCheckFilter,TokenAuthenticationFilter는 모두@Component로 선언되어 있고, 동시에SecurityConfiguration#filterChain에서addFilterBefore()로 시큐리티 필터 체인에 추가됩니다.security/filter/ExceptionHandlerFilter.java:20security/filter/SignOutCheckFilter.java:18security/filter/TokenAuthenticationFilter.java:21security/config/SecurityConfiguration.java:74-76Spring Boot는 컨텍스트에 등록된
Filter타입 빈을ServletContextInitializerBeans를 통해 서블릿 컨테이너에도 자동 등록합니다. 이를 막는FilterRegistrationBean설정이 없으므로(현재 프로젝트의 유일한 사용처는common/config/web/WebMvcConfig.java:53), 세 필터 모두 서블릿 컨테이너 + 시큐리티 필터 체인에 이중 등록된 상태입니다.실행은 왜 중복되지 않는가
다음 두 가지가 겹쳐서 중복 실행을 막고 있습니다.
springSecurityFilterChain(FilterChainProxy)은SecurityProperties.DEFAULT_FILTER_ORDER(-100)로 등록되고, 자동 등록된@Component필터는Ordered.LOWEST_PRECEDENCE입니다. 따라서 시큐리티 체인이 바깥쪽에서 먼저 실행되고, 자동 등록된 복제본은 그 안쪽에서 나중에 실행됩니다.OncePerRequestFilter— 세 필터 모두 이를 상속합니다. 시큐리티 체인에서 먼저 실행될 때<빈이름>.FILTERED요청 어트리뷰트가 설정되고, 뒤에 도달하는 컨테이너 등록분은 동일 인스턴스·동일 빈 이름이라 어트리뷰트 이름이 같아doFilterInternal을 건너뜁니다.실제 요청 흐름:
즉 인증이 두 번 수행되거나
SecurityContext가 덮어써지는 실질적 버그는 현재 없습니다.그럼에도 개선이 필요한 이유
현재의 안전성은 위 두 조건에 우연히 의존하고 있습니다.
OncePerRequestFilter가 아닌Filter/GenericFilterBean으로 변경되면 즉시 두 번 실행됩니다.web.ignoring()을 추가하거나securityMatcher를 가진 두 번째SecurityFilterChain을 도입하면, 시큐리티 체인을 타지 않는 경로에서도 컨테이너에 등록된 복제본이 실행되어 의도치 않게 인증/차단 로직이 동작합니다.ExceptionHandlerFilter → SignOutCheckFilter → TokenAuthenticationFilter순서가 보장되지 않습니다. 예외 변환 필터가 뒤로 밀리면CustomException이 JSON 응답으로 변환되지 않을 수 있습니다.AS-IS
@Component로 선언되어 서블릿 컨테이너와 시큐리티 필터 체인에 이중 등록됨OncePerRequestFilter의FILTERED어트리뷰트에 암묵적으로 의존해 막히고 있음TO-BE
SecurityConfiguration에서 직접 생성되어 시큐리티 필터 체인에만 추가됨해결 방법
세 필터의 프로덕션 사용처는
SecurityConfiguration하나뿐이므로, 컴포넌트 스캔 대상으로 둘 이유가 없습니다.@Component를 제거하고filterChain()안에서 직접 생성합니다.영향 없는 항목
filterChain()은 컨텍스트 초기화 시 한 번만 호출되므로 인스턴스 수와 수명이 기존 싱글톤 빈과 동일합니다. 오히려 컨테이너에 등록되던 no-op 필터 3개가 사라져 요청당 필터 통과가 3번 줄어듭니다.OncePerRequestFilter동작 — 빈이 아니므로getFilterName()이 빈 이름 대신 클래스명으로 대체되지만, 유일성이 보장되어 영향이 없습니다.initFilterBean()을 오버라이드한 필터도 없습니다.작업 상세 내용
@Component제거SecurityConfiguration이 필터 대신 필터의 의존성(AuthenticationManager,AuthorizationHeaderParser,BlacklistChecker,ObjectMapper)을 주입받고,filterChain()안에서 필터를 직접 생성@Autowired로 주입받던 테스트 3개(TokenAuthenticationFilterTest,SignOutCheckFilterTest,ExceptionHandlerFilterTest) 수정참고할만한 자료(선택)
HttpLoggingFilter는@Component이면서WebMvcConfig.java:52-58에서FilterRegistrationBean으로도 등록되는데, Spring Boot가 등록 빈이 참조하는 필터를 "이미 처리됨"으로 표시하므로 중복 등록되지 않고HIGHEST_PRECEDENCE로 한 번만 등록됩니다. 이 부분은 문제 없습니다.org.springframework.boot.web.servlet.ServletContextInitializerBeansorg.springframework.boot.autoconfigure.security.SecurityProperties#DEFAULT_FILTER_ORDER