Spring Security 7 마이그레이션 — lambda DSL 필수화와 SecurityFilterChain 설정 변경 정리
Spring Security 6에서 7로 올리면서 만난 deprecated API 제거, lambda DSL 필수화, OAuth2 Resource Server 변경점을 before/after 비교와 함께 정리했습니다.

Spring Boot 버전을 올리면서 Spring Security도 7로 함께 올라갔습니다.
빌드를 돌리니 SecurityFilterChain 설정 코드에서 deprecated 경고가 쏟아졌고, 일부는 아예 컴파일이 되지 않았습니다.
처음에는 "어차피 lambda DSL은 이미 쓰고 있으니 금방 끝나겠지"라고 생각했습니다. 하지만 막상 열어보니 lambda DSL 필수화 말고도, 알게 모르게 의존하던 deprecated API가 곳곳에 숨어 있었습니다. 이 글은 그 과정에서 하나씩 잡아낸 변경점들을 정리한 기록입니다.
왜 이렇게 많이 바뀐 걸까
Spring Security는 5.x 시절부터 "설정 방식을 현대화하겠다"는 방향을 꾸준히 밀어왔습니다.
WebSecurityConfigurerAdapter를 버리고 SecurityFilterChain 빈으로 전환한 것이 대표적이었고, 6.x에서는 lambda DSL을 권장하면서 기존 체이닝 방식에 deprecated를 붙였습니다.
7.0에서는 그 deprecated들이 실제로 제거되면서, "권장"이 "필수"로 바뀌었습니다. 변화의 방향 자체는 일관적이지만, 한 번에 여러 API가 사라지니 마이그레이션 범위가 예상보다 넓었습니다.
lambda DSL 필수화 — 가장 넓은 영향 범위
변경 전: 체이닝 방식
Spring Security 5.x 스타일 코드를 쓰고 있었다면, 아마 이런 모양이었을 겁니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}.and()로 설정 블록을 이어 붙이는 방식입니다.
코드가 길어지면 "이 .and()가 어디로 돌아가는 거지?" 하고 헷갈리는 경우가 많았습니다.
변경 후: lambda DSL
Spring Security 7에서는 .csrf(), .sessionManagement(), .authorizeHttpRequests() 같은 메서드가 Customizer<T>를 인자로 받는 lambda 형태만 남았습니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}.and()가 사라지니 각 설정 블록의 범위가 lambda 중괄호로 명확해졌습니다.
사실 코드량 자체는 비슷하지만, 중첩 깊이와 가독성 면에서는 확실히 나아졌습니다.
비활성화 설정도 바뀌었다
기존에 http.csrf().disable() 처럼 쓰던 패턴은 Customizer.withDefaults()나 lambda 내부에서 처리하게 됩니다.
// 변경 전
http.cors().disable();
http.httpBasic().disable();
// 변경 후
http.cors(cors -> cors.disable());
http.httpBasic(httpBasic -> httpBasic.disable());한 줄짜리 설정도 lambda로 감싸야 해서 처음에는 약간 번거로웠지만, 금세 익숙해졌습니다.
deprecated API 제거 — 컴파일이 안 되는 것들
lambda DSL 전환만으로 끝나면 좋겠지만, 7.0에서는 그 외에도 꽤 많은 API가 제거되었습니다. 실제로 저희 프로젝트에서 잡아야 했던 항목들을 정리해 보겠습니다.
requestMatchers 관련 변경
// 변경 전 — antMatchers, mvcMatchers 사용
http.authorizeHttpRequests()
.antMatchers("/api/public/**").permitAll()
.mvcMatchers("/api/admin/**").hasRole("ADMIN");
// 변경 후 — requestMatchers로 통합
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
);antMatchers()와 mvcMatchers()는 6.x에서 deprecated 되었고 7.0에서 완전히 제거되었습니다.
requestMatchers()가 내부적으로 MVC가 있으면 MVC matcher를, 없으면 Ant matcher를 자동 선택하기 때문에, 대부분의 경우 그냥 이름만 바꾸면 됐습니다.
authorizeRequests에서 authorizeHttpRequests로
// 변경 전
http.authorizeRequests()
.antMatchers("/api/**").authenticated();
// 변경 후
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated()
);authorizeRequests()는 내부적으로 SecurityExpressionHandler를 사용하는 방식이었고, authorizeHttpRequests()는 AuthorizationManager 기반으로 동작합니다.
단순한 이름 변경이 아니라 인가 처리 메커니즘 자체가 바뀐 것이어서, SpEL 표현식을 쓰고 있었다면 주의가 필요합니다.
전체 변경 비교표
| 항목 | 변경 전 (Security 5~6) | 변경 후 (Security 7) |
|---|---|---|
| 설정 방식 | 체이닝 + .and() | lambda DSL (필수) |
| CSRF 비활성화 | .csrf().disable() | .csrf(csrf -> csrf.disable()) |
| 경로 매칭 | antMatchers(), mvcMatchers() | requestMatchers() |
| 인가 설정 | authorizeRequests() | authorizeHttpRequests() |
| 세션 정책 | .sessionManagement().sessionCreationPolicy(...) | .sessionManagement(s -> s.sessionCreationPolicy(...)) |
| CORS 비활성화 | .cors().disable() | .cors(cors -> cors.disable()) |
| HTTP Basic 비활성화 | .httpBasic().disable() | .httpBasic(httpBasic -> httpBasic.disable()) |
OAuth2 Resource Server 설정 변경
저희 프로젝트에서는 JWT 기반 인증을 직접 구현해서 쓰고 있었지만, 일부 내부 API는 OAuth2 Resource Server 설정으로 JWT 검증을 위임하고 있었습니다. 이 부분에서도 변경이 있었습니다.
JWT 디코더 설정
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthConverter());
return http.build();
}@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthConverter())
)
);
return http.build();
}구조적으로는 동일하지만, lambda 중첩으로 변경됩니다.
한 가지 주의할 점은 oauth2ResourceServer() 내부에서 .jwt()를 호출하는 인자 없는 메서드가 제거되었기 때문에, 반드시 jwt(Customizer<...>) 형태로 써야 한다는 것이었습니다.
Opaque Token도 동일한 패턴
JWT 대신 Opaque Token을 쓰는 경우에도 같은 변경이 적용됩니다.
// 변경 전
http.oauth2ResourceServer()
.opaqueToken()
.introspectionUri("https://auth.example.com/introspect")
.introspectionClientCredentials("client-id", "client-secret");
// 변경 후
http.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspectionUri("https://auth.example.com/introspect")
.introspectionClientCredentials("client-id", "client-secret")
)
);마이그레이션하면서 실수한 것들
코드를 바꾸는 것 자체는 기계적인 작업에 가까웠지만, 몇 가지 함정이 있었습니다.
첫 번째, requestMatchers 순서를 잘못 배치해서 특정 API가 인증 없이 열렸던 적이 있습니다.
lambda DSL로 전환하면서 경로 규칙 순서를 재배치했는데, permitAll()이 앞에 오면서 더 구체적인 경로의 인가 규칙이 무시되었습니다.
이전에 JWT 테스트 환경 구축기에서 만들어 둔 통합 테스트가 이 문제를 잡아줬습니다.
// 이렇게 하면 /api/admin도 permitAll에 걸림
auth.requestMatchers("/api/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN");
// 좁은 범위를 먼저 선언해야 함
auth.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated();두 번째, @EnableWebSecurity를 빼먹은 설정 클래스가 있었습니다.
Spring Boot 자동 설정이 어느 정도 커버해주기 때문에 없어도 동작하는 경우가 있었는데, Security 7에서는 명시하지 않으면 일부 기본 필터 체인 구성이 달라지는 경우가 생겼습니다.
명시적으로 붙여두는 편이 안전했습니다.
세 번째, 커스텀 필터에서 OncePerRequestFilter를 상속할 때 shouldNotFilter 오버라이드를 빼먹었습니다.
이전에 JWT Refresh Token 전략에서 구현한 JWT 인증 필터가 로그인 경로까지 타면서 불필요한 토큰 파싱 에러가 발생했습니다.
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/api/auth/");
}이건 Security 7의 변경은 아니지만, 마이그레이션 과정에서 설정을 통째로 정리하다 보니 발견된 문제였습니다.
마이그레이션 체크리스트
실제로 작업하면서 정리한 체크리스트입니다. 비슷한 작업을 하시는 분께 도움이 되면 좋겠습니다.
| 순서 | 항목 | 확인 |
|---|---|---|
| 1 | .and() 체이닝을 lambda DSL로 전환 | |
| 2 | antMatchers() / mvcMatchers() → requestMatchers() | |
| 3 | authorizeRequests() → authorizeHttpRequests() | |
| 4 | oauth2ResourceServer().jwt() → lambda 형태 전환 | |
| 5 | csrf(), cors(), httpBasic() 등 비활성화 코드 lambda 전환 | |
| 6 | @EnableWebSecurity 명시 확인 | |
| 7 | requestMatchers 경로 순서 검증 (좁은 범위 먼저) | |
| 8 | 커스텀 필터의 shouldNotFilter 경로 확인 | |
| 9 | 기존 통합 테스트 전체 실행하여 인가 규칙 검증 |
마무리
Spring Security 7 마이그레이션은 "deprecated 경고를 지우는 작업" 정도로 시작했지만, 막상 해보니 설정 전체를 다시 읽어보는 기회가 되었습니다.
lambda DSL 필수화 자체는 방향이 맞다고 느꼈습니다. .and()로 설정 블록을 이어 붙이던 방식은 코드가 길어질수록 흐름을 따라가기 어려웠고, lambda로 바꾸니 각 설정의 범위가 중괄호로 명확해졌습니다.
다만 deprecated API가 한꺼번에 제거되면서, 변경이 필요한 지점이 SecurityConfig 한 곳이 아니라 커스텀 필터, 테스트 코드, OAuth2 설정 등 여러 곳에 흩어져 있다는 것을 간과하기 쉬웠습니다.
결국 가장 든든했던 건 이전에 작성해 둔 통합 테스트였습니다. 설정을 바꾸고 테스트를 돌렸을 때 실패하는 케이스가 바로 나와줬기 때문에, "이 경로는 열려야 하고 저 경로는 막혀야 한다"는 인가 규칙을 안심하고 검증할 수 있었습니다.
마이그레이션은 늘 귀찮은 작업이지만, 미루면 미룰수록 더 귀찮아진다는 걸 이번에도 확인했습니다.