2026년 08월 21일

Spring Boot 버전 업그레이드 후 Deprecated된 SecurityFilterChain 보안 설정 리팩토링

Spring Boot 2.7 이후로 WebSecurityConfigurerAdapter를 상속하는 방식이 공식적으로 deprecated 처리되었고, Spring Boot 3.x로 넘어오면서는 아예 제거되었다. 기존 방식에 익숙했던 프로젝트라면 버전 업그레이드 직후 컴파일 에러 혹은 런타임 오류를 마주치게 된다. 이 글은 그 전환 과정에서 실제로 마주치는 문제들과 리팩토링 방법을 단계별로 정리한다.

WebSecurityConfigurerAdapter가 사라진 이유

Spring Security 팀이 WebSecurityConfigurerAdapter 상속 방식을 deprecated한 핵심 이유는 컴포넌트 기반 설계로의 전환이다. 기존 방식은 설정 클래스가 추상 클래스를 상속받아야 했기 때문에 특정 메서드를 오버라이드하는 구조였다. 이는 여러 설정을 조합하거나 테스트 환경에서 부분적으로 교체하기 어렵게 만들었다.

반면 SecurityFilterChain을 빈으로 등록하는 방식은 스프링의 일반적인 빈 관리 메커니즘을 그대로 활용한다. 여러 SecurityFilterChain 빈을 등록해 경로별로 다른 보안 정책을 적용하기도 수월하고, 테스트 시 특정 빈만 교체하는 것도 자연스럽다.

기존 코드 구조와 문제 파악

업그레이드 전 전형적인 보안 설정 클래스는 다음과 같은 형태였다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .logout()
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Spring Boot 3.x 환경에서 이 코드를 그대로 두면 WebSecurityConfigurerAdapter 클래스 자체를 찾을 수 없어 컴파일이 실패한다. authorizeRequests()나 antMatchers() 같은 메서드도 이미 deprecated되어 있거나 제거된 상태다.

SecurityFilterChain 방식으로 전환하기

새로운 방식의 핵심은 HttpSecurity를 파라미터로 받는 메서드에서 SecurityFilterChain을 반환하고 이를 @Bean으로 등록하는 것이다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    public SecurityConfig(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .permitAll()
            );

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

변경된 부분을 항목별로 짚어보면 다음과 같다.

  • extends WebSecurityConfigurerAdapter 제거, 일반 클래스로 선언
  • authorizeRequests() → authorizeHttpRequests()
  • antMatchers() → requestMatchers()
  • .csrf().disable() → .csrf(AbstractHttpConfigurer::disable) (람다 DSL 방식)
  • configure(AuthenticationManagerBuilder) 오버라이드 대신 AuthenticationManager 빈 직접 등록
  • 메서드 마지막에 http.build() 호출 후 반환

람다 DSL 방식으로 전환해야 하는 이유

Spring Security 6부터는 메서드 체이닝 방식의 and() 연결 대신 람다 DSL 방식이 권장된다. 기존의 .and() 체이닝 방식은 가독성이 떨어지고 설정 범위가 명확하지 않다는 피드백이 오랫동안 있었다.

람다 DSL 방식은 각 설정 블록의 시작과 끝이 명확해 실수를 줄이고 IDE의 자동완성 지원도 잘 된다. Spring Security 6.1 이후로는 기존 .and() 방식 자체가 deprecated 처리되어 있으므로, 이번 리팩토링 시점에 함께 전환해두는 것이 낫다.

UserDetailsService 연동 방식 변경

기존에는 configure(AuthenticationManagerBuilder auth) 메서드를 오버라이드해서 UserDetailsService와 PasswordEncoder를 연결했다. 새로운 방식에서는 UserDetailsService와 PasswordEncoder 빈이 스프링 컨텍스트에 등록되어 있으면 Spring Security가 자동으로 감지해 DaoAuthenticationProvider를 구성한다.

즉, 두 빈만 제대로 등록되어 있다면 별도로 AuthenticationManagerBuilder를 조작하지 않아도 된다. 다만 AuthenticationManager 자체를 다른 컴포넌트(예: JWT 필터)에서 주입받아 사용해야 한다면, 위 예시처럼 AuthenticationConfiguration으로부터 꺼내 빈으로 등록해야 한다.

@Bean
public AuthenticationManager authenticationManager(
        AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}

이렇게 하면 AuthenticationManager를 다른 빈에서 @Autowired나 생성자 주입으로 받아 쓸 수 있다.

경로별 다중 SecurityFilterChain 구성

새로운 빈 방식의 장점 중 하나는 경로별로 다른 보안 정책을 적용하는 다중 필터 체인 구성이 직관적이라는 점이다. API 엔드포인트와 웹 페이지 엔드포인트에 서로 다른 인증 방식을 적용하는 경우가 대표적이다.

@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .anyRequest().authenticated()
        );
    return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults());
    return http.build();
}

@Order로 우선순위를 지정하고, securityMatcher()로 해당 체인이 적용될 경로를 한정한다. /api/** 경로는 세션을 사용하지 않는 stateless 정책을 적용하고, 나머지 경로는 폼 로그인을 사용하는 구조다. 기존 방식에서는 이런 분리를 위해 별도의 설정 클래스를 여러 개 만들고 @Order를 붙이는 방식이었는데, 이제는 한 클래스 안에서 정리할 수 있다.

CORS 설정 변경 사항

Spring Boot 3.x에서 CORS 설정도 람다 DSL 방식으로 전환해야 한다.

.cors(cors -> cors
    .configurationSource(corsConfigurationSource())
)

CorsConfigurationSource 빈을 별도로 등록하고 위처럼 참조하는 방식이 권장된다. 기존의 .cors().and() 체이닝은 Spring Security 6.1부터 deprecated 처리되어 있다.

마이그레이션 시 자주 겪는 오류들

리팩토링 과정에서 반복적으로 마주치는 문제들을 정리했다.

antMatchers is not found

antMatchers()는 Spring Security 6에서 제거되었다. requestMatchers()로 교체하면 된다. Spring Security 6에서 requestMatchers()는 서블릿 환경에서는 AntPathRequestMatcher를 기본으로 사용하므로 동작 방식은 동일하다.

NoSuchBeanDefinitionException: AuthenticationManager

AuthenticationManager를 다른 컴포넌트에서 주입받으려 할 때 빈 등록 없이 사용하면 발생한다. 위에서 설명한 것처럼 AuthenticationConfiguration을 통해 명시적으로 빈 등록을 해줘야 한다.

PasswordEncoder 순환 참조

SecurityConfig 내부에서 UserDetailsService를 직접 생성하고 PasswordEncoder를 주입하는 과정에서 순환 참조가 발생하는 경우가 있다. UserDetailsService 구현체를 별도 클래스로 분리하고 SecurityConfig에는 생성자 주입만 받는 구조로 정리하면 해결된다.

H2 Console 접근 불가

Spring Boot 3.x에서 H2 콘솔을 사용할 때 requestMatchers("/h2-console/**").permitAll() 외에도 headers 설정에서 frameOptions를 허용해야 한다.

.headers(headers -> headers
    .frameOptions(frameOptions -> frameOptions.sameOrigin())
)

리팩토링 체크리스트

버전 업그레이드 후 보안 설정 리팩토링을 완료하기 위한 확인 항목을 정리하면 다음과 같다.

  • WebSecurityConfigurerAdapter 상속 제거 및 일반 클래스로 전환 완료
  • authorizeRequests() → authorizeHttpRequests() 교체
  • antMatchers() → requestMatchers() 교체
  • and() 체이닝 → 람다 DSL 방식으로 전환
  • configure(AuthenticationManagerBuilder) 오버라이드 제거, 필요 시 AuthenticationManager 빈 등록
  • UserDetailsService, PasswordEncoder 빈 별도 클래스 또는 @Bean 메서드로 정리
  • CSRF, CORS 설정 람다 DSL 방식으로 전환
  • 다중 필터 체인 사용 시 @Order와 securityMatcher() 적용 확인
  • 테스트 코드의 @WithMockUser, mockMvc 보안 설정도 함께 점검

버전 업그레이드는 단순히 의존성 버전만 올리는 작업이 아니다. Spring Security처럼 핵심 인프라 영역의 설계 방향이 바뀐 경우라면, 변경된 의도를 이해하고 코드 구조 자체를 맞춰 가는 것이 장기적으로 유지보수를 훨씬 수월하게 만든다. 이번 SecurityFilterChain 전환은 그 대표적인 사례로, 한 번 제대로 정리해두면 이후 추가 기능을 붙이는 과정도 훨씬 깔끔해진다.