Spring Security 필터 체인 등록 문제 해결하기: 필터 순서의 중요성
결제를 요청하는데 사용되는 토큰을 검증하는 PaymentTokenAuthenticationFilter와 JwtAuthenticationFilter를 구성하다 마주친 문제와 이 오류의 원인을 파악하면서 Spring Security의 내부 동작에 대해 깊이 이해하게 된 경험을 공유합니다.
문제 상황: 왜 갑자기 필터 체인이 동작하지 않을까?

2025년 5월 16일 로그인 API를 개발하며 JWT 인증 방식을 추가하기 위해 커스텀 필터를 설정했는데, 다음과 같은 오류가 발생했습니다:
[org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: The Filter class com.fisa.pg.config.security.filter.PaymentTokenAuthenticationFilter does not have a registered order and cannot be added without a specified order문제의 원인: 필터 순서는 생각보다 중요합니다
먼저 오류가 발생했던 코드는 다음과 같습니다:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final PaymentAuthenticationFilter paymentAuthenticationFilter;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// ...중략...
.addFilterBefore(jwtAuthenticationFilter, PaymentTokenAuthenticationFilter.class) // PaymentTokenAuthenticationFilter 필터 전에 JWT 필터 추가
.addFilterBefore(paymentTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // UsernamePasswordAuthenticationFilter 필터 전에 PaymentTokenAuthenticationFilter 필터 추가
.build();
}
// ...중략...
}그리고 해결한 코드는 다음과 같습니다:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final PaymentAuthenticationFilter paymentAuthenticationFilter;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// ...중략...
.addFilterBefore(paymentTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // UsernamePasswordAuthenticationFilter 필터 전에 PaymentTokenAuthenticationFilter 필터 추가
.addFilterBefore(jwtAuthenticationFilter, PaymentTokenAuthenticationFilter.class) // PaymentTokenAuthenticationFilter 필터 전에 JWT 필터 추가
.build();
}
// ...중략...
}왜 이전 코드에서 오류가 발생했을까요?
간단히 말하면, Spring Security는 필터 체인을 구성할 때 참조되는 필터가 이미 체인에 등록되어 있어야 합니다.
이전 코드에서는 PaymentTokenAuthenticationFilter가 아직 체인에 추가되지 않은 상태에서 참조했기 때문에 오류가 발생했습니다.
Spring Security 내부 들여다보기: 필터 체인은 어떻게 구성될까?
Spring Security가 어떻게 필터 체인을 구성하는지 코드 레벨에서 깊게 살펴보겠습니다. 이 과정을 이해하면 왜 필터 추가 순서가 중요한지 명확해질 것입니다.
1. 필터 순서 관리 방식
Spring Security는 FilterOrderRegistration이라는 클래스를 통해 필터의 순서를 관리합니다.
이 클래스는 각 필터의 순서를 filterToOrder라는 Map에 저장하는데, 표준 필터만 기본으로 등록되어 있습니다.
final class FilterOrderRegistration {
private static final int INITIAL_ORDER = 100;
private static final int ORDER_STEP = 100;
private final Map<String, Integer> filterToOrder = new HashMap<>();
FilterOrderRegistration() {
put(DisableEncodeUrlFilter.class, order.next());
put(ForceEagerSessionCreationFilter.class, order.next());
put(ChannelProcessingFilter.class, order.next());
// ...중략...
put(UsernamePasswordAuthenticationFilter.class, order.next());
// ...중략...
put(FilterSecurityInterceptor.class, order.next());
put(AuthorizationFilter.class, order.next());
put(SwitchUserFilter.class, order.next());
}
void put(Class<? extends Filter> filter, int position) {
this.filterToOrder.putIfAbsent(filter.getName(), position);
}
// ...중략...
}그리고 커스텀 필터는 이 filterToOrder라는 Map에 등록되어 있지 않습니다.
2. 커스텀 필터 등록 과정
이제 표준 필터가 어떻게 등록되어 있는지 알았으니, 그 다음으로 우리가 직접 만든 커스텀 필터를 등록하는 과정을 살펴보겠습니다.
커스텀 필터를 등록할 때 사용하는 HttpSecurity의 addFilterBefore 메서드는 다음과 같이 동작합니다:
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity> implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> {
// ...중략...
@Override
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) {
return addFilterAtOffsetOf(filter, -1, beforeFilter);
}
private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
Integer registeredFilterOrder = this.filterOrders.getOrder(registeredFilter);
if (registeredFilterOrder == null) {
throw new IllegalArgumentException("The Filter class " + registeredFilter.getName() + " does not have a registered order");
}
int order = registeredFilterOrder + offset;
this.filters.add(new OrderedFilter(filter, order));
this.filterOrders.put(filter.getClass(), order);
return this;
}
// ...중략...
}실제 코드를 보면 addFilterBefore 메서드는 내부적으로 addFilterAtOffsetOf 메서드를 호출하고 있습니다. 이 메서드의 동작 방식을 단계별로 살펴보겠습니다:
-
addFilterBefore는 새 필터를 특정 필터 ‘앞에’ 추가하려 할 때addFilterAtOffsetOf를 offset-1로 호출합니다. (앞에 위치하려면 순서 값이 더 작아야 하기 때문입니다) -
addFilterAtOffsetOf메서드는 다음 작업을 수행합니다:filterOrders.getOrder(registeredFilter)를 호출해서 참조하는 필터의 순서를 조회합니다.- 만약 참조하는 필터가 등록되지 않았다면(순서가 null이면) 예외를 던집니다.
- 참조 필터의 순서에 offset을 더해 새 필터의 순서를 계산합니다.
- 새 필터를
OrderedFilter로 감싸서filters목록에 추가합니다. - 새 필터의 클래스와 계산된 순서를
filterOrders에 등록합니다. 이렇게 하면 나중에 이 필터를 참조할 수 있게 됩니다!
오류 다시 살펴보기: 근본 원인 분석
이제 Spring Security의 내부 코드를 분석했으니, 처음 만났던 오류를 다시 살펴보겠습니다:
The Filter class com.fisa.pg.config.security.filter.PaymentTokenAuthenticationFilter does not have a registered order and cannot be added without a specified order이 오류 메시지는 PaymentTokenAuthenticationFilter 클래스가 등록된 순서(registered order)를 가지고 있지 않아서 발생했다고 말하고 있습니다.
.addFilterBefore(jwtAuthenticationFilter, PaymentTokenAuthenticationFilter.class)를 호출했을 때,
PaymentTokenAuthenticationFilter가 아직 등록되지 않았기 때문에 filterOrders에서 순서를 찾을 수 없었고, 그 결과 예외가 발생한 것입니다.
해결 방법은 간단했습니다. PaymentTokenAuthenticationFilter를 UsernamePasswordAuthenticationFilter 필터 앞에 추가하고,
JwtAuthenticationFilter를 PaymentTokenAuthenticationFilter 앞에 추가하는 것이었습니다.
마무리: 깊은 이해가 보안을 강화합니다
Spring Security의 필터 체인을 설정할 때는 단순히 코드를 작성하는 것 이상으로, 내부 메커니즘을 이해하는 것이 중요합니다. 이번 경험을 통해 Spring Security의 내부 동작 방식을 더 깊이 이해하게 되었고, 이를 바탕으로 더 안전한 결제 시스템을 구축할 수 있게 되었습니다.