이번 포스팅에서는 Spring Boot 4 API Versioning에 대해서 정리하고자 한다. Spring Boot 4.0과 Spring Framework 7.0이 출시되면서 그동안 커스텀 어노테이션이나 외부 라이브러리에 의존해야 했던 API 버전 관리가 프레임워크의 일급 시민(first-class citizen)으로 승격되었다. @GetMapping의 version 속성 하나로 버전별 엔드포인트를 분리할 수 있고, application.properties 몇 줄이면 자동 구성이 완료된다. 이 글에서는 Path Segment, Header, Query Parameter, Media Type 4가지 전략을 실제 프로젝트 코드로 구현하면서 각 전략의 장단점과 실무 선택 기준까지 다룬다.
그동안 API 버전 관리가 얼마나 번거로웠는지
Spring Boot 3.x까지 API 버전 관리를 구현하려면 선택지가 제한적이었다. URL에 /v1/, /v2/를 수동으로 붙이거나, 커스텀 HandlerMapping을 작성하거나, spring-api-versioning 같은 서드파티 라이브러리를 사용해야 했다. 이런 방식은 프로젝트마다 구현이 달라 일관성을 유지하기 어려웠고, 버전 폐기(deprecation) 알림이나 클라이언트 측 버전 지정 같은 기능은 별도로 구현해야 했다.
Spring Framework 7.0은 이 문제를 정면으로 다룬다. @RequestMapping 계열 어노테이션에 version 속성이 추가되었고, 버전 해석(resolve)부터 파싱(parse), 검증(validate), 폐기 알림(deprecation)까지 ApiVersionStrategy 하나로 처리한다. Spring Boot 4.0은 여기에 자동 구성을 얹었다. spring.mvc.apiversion.* 프로퍼티 몇 줄이면 바로 동작한다.

내부적으로 동작하는 구성 요소는 다음과 같다.
ApiVersionResolver는 요청에서 버전 정보를 추출한다. Header, Path, Query Parameter, Media Type 중 하나를 선택하게 된다.ApiVersionParser는 추출된 문자열을 비교 가능한 버전 객체로 변환한다. 기본 구현체인SemanticApiVersionParser가major.minor.patch형식을 처리한다.ApiVersionDeprecationHandler는 폐기된 버전 요청 시 RFC 9745(Deprecation), RFC 8594(Sunset) 표준 헤더를 응답에 넣어 준다.
프로젝트 설정부터 시작하기
별도 라이브러리가 필요 없다. Spring Boot 4.0 이상이면 spring-boot-starter-web 하나로 Spring Boot 4 API Versioning을 바로 쓸 수 있다.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.4</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>XML위 Maven 설정에서 spring-boot-starter-parent 4.0.4를 지정하면 Spring Framework 7.0.x가 자동으로 포함된다. API Versioning은 spring-webmvc 모듈에 내장되어 있으므로 spring-boot-starter-web 하나면 충분하다. Gradle을 사용한다면 아래와 같이 설정한다.
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.4'
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}GroovyGradle 설정에서도 별도 버전 관리 라이브러리를 추가할 필요가 없다. spring-boot-starter-web이 Spring Framework 7.0의 API Versioning 인프라를 모두 포함한다.
버전별 응답 DTO 설계, record로 가볍게

실습에 사용할 도메인 모델과 DTO를 먼저 준비한다. v1은 name 필드 하나로 이름을 관리하고, v2에서 firstName과 lastName으로 분리하는 시나리오이다.
// User 엔티티
public record User(
Integer id,
String name, // "홍 길동" 형태의 전체 이름
String email
) {}
// v1 응답 — 이름을 단일 필드로 반환
public record UserDTOv1(
Integer id,
String name,
String email
) {}
// v2 응답 — 이름을 성/이름으로 분리하여 반환
public record UserDTOv2(
Integer id,
String firstName,
String lastName,
String email
) {}JavaJava 21의 record를 사용하면 불변 DTO를 간결하게 정의할 수 있다. v1에서 v2로 전환할 때 name → firstName + lastName 분리는 실무에서 자주 발생하는 breaking change 시나리오이다. 이런 변경이 발생했을 때 기존 클라이언트를 보호하면서 새 버전을 제공하는 것이 Spring Boot 4 API Versioning의 핵심 목적이다.
변환 로직을 담당하는 Mapper도 함께 정의한다.
import org.springframework.stereotype.Component;
@Component
public class UserMapper {
public UserDTOv1 toV1(User user) {
return new UserDTOv1(user.id(), user.name(), user.email());
}
public UserDTOv2 toV2(User user) {
// 전체 이름에서 마지막 공백을 기준으로 성과 이름을 분리한다
String[] parts = splitName(user.name());
return new UserDTOv2(user.id(), parts[0], parts[1], user.email());
}
private String[] splitName(String fullName) {
if (fullName == null || fullName.isBlank()) {
return new String[]{"", ""};
}
int lastSpace = fullName.trim().lastIndexOf(' ');
if (lastSpace == -1) {
return new String[]{fullName.trim(), ""};
}
return new String[]{
fullName.substring(0, lastSpace).trim(),
fullName.substring(lastSpace + 1).trim()
};
}
}JavasplitName 메서드는 마지막 공백을 기준으로 이름을 분리한다. 한국어 이름(“홍 길동”)이든 영어 이름(“John Doe”)이든 동일한 로직으로 처리할 수 있다. @Component로 등록하여 Controller에서 주입받아 사용한다.
4가지 버전 관리 전략을 코드로 비교한다
Spring Boot 4 API Versioning은 Path Segment, Request Header, Query Parameter, Media Type Parameter 4가지 전략을 기본 제공한다. 각 전략은 @GetMapping의 version 속성과 조합하여 동작한다.
Path Segment: URL에 버전을 명시하는 가장 직관적인 방식
REST API에서 가장 널리 사용되는 방식이다. /api/v1/users, /api/v2/users처럼 URL 경로에 버전 번호가 포함된다.
# application.yml
spring:
mvc:
apiversion:
use:
path-segment: 1YAML위 설정에서 path-segment: 1은 URL 경로의 두 번째 세그먼트(0-based index)를 버전으로 해석하겠다는 의미이다. /api/v1/users에서 v1이 index 1에 해당한다.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api")
public class UserController {
private final UserMapper userMapper;
private final UserRepository userRepository;
public UserController(UserMapper userMapper, UserRepository userRepository) {
this.userMapper = userMapper;
this.userRepository = userRepository;
}
// v1 — name 필드를 단일 문자열로 반환
@GetMapping(value = "/{version}/users", version = "1.0")
public List<UserDTOv1> getUsersV1() {
return userRepository.findAll().stream()
.map(userMapper::toV1)
.toList();
}
// v2 — name을 firstName, lastName으로 분리하여 반환
@GetMapping(value = "/{version}/users", version = "2.0")
public List<UserDTOv2> getUsersV2() {
return userRepository.findAll().stream()
.map(userMapper::toV2)
.toList();
}
}Java@GetMapping의 version 속성에 "1.0", "2.0"을 지정하는 것만으로 버전별 라우팅이 완성된다. Spring Framework 7.0이 내부적으로 ApiVersionResolver를 통해 URL 경로에서 버전을 추출하고, SemanticApiVersionParser로 파싱한 뒤 매칭되는 핸들러 메서드를 호출한다. 기존처럼 /v1/users와 /v2/users를 별도 Controller로 분리할 필요가 없다.
# v1 호출 — name이 단일 필드로 반환된다
curl http://localhost:8080/api/v1/users
# [{"id":1,"name":"홍 길동","email":"hong@example.com"}]
# v2 호출 — firstName, lastName으로 분리되어 반환된다
curl http://localhost:8080/api/v2/users
# [{"id":1,"firstName":"홍","lastName":"길동","email":"hong@example.com"}]ShellScriptPath Segment 방식은 URL만 보고 어떤 버전을 호출하는지 즉시 파악할 수 있어 디버깅과 문서화에 유리하다. CDN이나 API Gateway에서 URL 패턴 기반 캐싱과 라우팅을 적용하기도 쉽다.
Request Header: URL을 깔끔하게 유지하는 방식
내부 마이크로서비스 간 통신이나 URL 변경을 최소화해야 하는 경우에 적합하다. 클라이언트가 HTTP 헤더에 버전을 명시한다.
spring:
mvc:
apiversion:
use:
header: X-API-VersionYAMLheader: X-API-Version은 요청의 X-API-Version 헤더 값을 버전으로 사용하겠다는 설정이다. 헤더 이름은 자유롭게 지정할 수 있다.
@RestController
@RequestMapping("/api/users")
public class UserHeaderController {
private final UserMapper userMapper;
private final UserRepository userRepository;
public UserHeaderController(UserMapper userMapper, UserRepository userRepository) {
this.userMapper = userMapper;
this.userRepository = userRepository;
}
// 동일한 URL에서 헤더 값으로 버전을 구분한다
@GetMapping(version = "1.0")
public List<UserDTOv1> getUsersV1() {
return userRepository.findAll().stream()
.map(userMapper::toV1)
.toList();
}
@GetMapping(version = "2.0")
public List<UserDTOv2> getUsersV2() {
return userRepository.findAll().stream()
.map(userMapper::toV2)
.toList();
}
}JavaPath Segment 방식과 달리 URL에 {version} 경로 변수가 없다. /api/users라는 단일 URL로 모든 버전 요청을 처리하고, 헤더 값에 따라 내부적으로 핸들러가 분기된다. URL 구조를 변경하지 않고 버전 관리를 도입할 수 있어 기존 API와의 하위 호환성을 유지하기 좋다.
# v1 호출
curl -H "X-API-Version: 1.0" http://localhost:8080/api/users
# v2 호출
curl -H "X-API-Version: 2.0" http://localhost:8080/api/usersShellScriptQuery Parameter: 브라우저에서 바로 테스트하기 좋은 방식
개발 단계에서 빠르게 버전을 전환하며 테스트할 때 편리하다. URL 뒤에 ?version=1.0을 붙이면 된다.
spring:
mvc:
apiversion:
use:
query-parameter: versionYAML@RestController
@RequestMapping("/api/users")
public class UserQueryController {
private final UserMapper userMapper;
private final UserRepository userRepository;
public UserQueryController(UserMapper userMapper, UserRepository userRepository) {
this.userMapper = userMapper;
this.userRepository = userRepository;
}
@GetMapping(value = "/list", version = "1.0")
public List<UserDTOv1> listUsersV1() {
return userRepository.findAll().stream()
.map(userMapper::toV1)
.toList();
}
@GetMapping(value = "/list", version = "2.0")
public List<UserDTOv2> listUsersV2() {
return userRepository.findAll().stream()
.map(userMapper::toV2)
.toList();
}
}JavaQuery Parameter 방식은 version 파라미터를 프레임워크가 자동으로 해석한다. Controller 메서드에 @RequestParam을 별도로 선언할 필요가 없다. Spring Boot 4 API Versioning이 내부적으로 쿼리 스트링을 파싱하여 version 속성과 매칭한다.
# 브라우저에서도 URL만 바꿔가며 테스트할 수 있다
curl http://localhost:8080/api/users/list?version=1.0
curl http://localhost:8080/api/users/list?version=2.0ShellScriptMedia Type Parameter: Content Negotiation 기반의 표준 준수 방식
Accept 헤더의 미디어 타입 파라미터로 버전을 전달하는 방식이다. GitHub API가 application/vnd.github.v3+json 형식으로 사용하는 것과 유사한 접근이다.
spring:
mvc:
apiversion:
use:
media-type-parameter:
application/json: versionYAML@RestController
@RequestMapping("/api/users")
public class UserMediaTypeController {
private final UserMapper userMapper;
private final UserRepository userRepository;
public UserMediaTypeController(UserMapper userMapper, UserRepository userRepository) {
this.userMapper = userMapper;
this.userRepository = userRepository;
}
// Accept 헤더의 version 파라미터로 분기한다
@GetMapping(value = "/media", version = "1.0", produces = "application/json")
public List<UserDTOv1> getUsersMediaV1() {
return userRepository.findAll().stream()
.map(userMapper::toV1)
.toList();
}
@GetMapping(value = "/media", version = "2.0", produces = "application/json")
public List<UserDTOv2> getUsersMediaV2() {
return userRepository.findAll().stream()
.map(userMapper::toV2)
.toList();
}
}JavaMedia Type 방식은 HTTP 표준인 Content Negotiation 메커니즘을 활용한다. Accept: application/json;version=2.0처럼 미디어 타입에 버전 파라미터를 포함시키는 방식이다. REST 원칙에 가장 충실하지만, 클라이언트 구현이 다소 복잡해질 수 있다.
# Accept 헤더에 미디어 타입 파라미터로 버전을 지정한다
curl -H "Accept: application/json;version=1.0" http://localhost:8080/api/users/media
curl -H "Accept: application/json;version=2.0" http://localhost:8080/api/users/mediaShellScriptApiVersionConfigurer로 Java 코드에서 직접 설정하기
application.properties만으로 부족한 경우 WebMvcConfigurer의 configureApiVersioning 메서드를 오버라이드하여 Java 코드로 세밀하게 설정할 수 있다.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer
// 헤더 기반 버전 해석을 사용한다
.useRequestHeader("X-API-Version")
// 지원하는 버전 목록을 명시한다
.addSupportedVersions("1.0", "1.1", "2.0")
// 버전 헤더가 없을 때 사용할 기본 버전을 지정한다
.setDefaultVersion("1.0")
// 버전 필수 여부 — false면 헤더 없이도 요청 가능
.setVersionRequired(false);
}
}JavaconfigureApiVersioning 메서드는 Spring Framework 7.0에서 WebMvcConfigurer에 새로 추가된 메서드이다. addSupportedVersions로 명시한 버전 외의 요청이 들어오면 InvalidApiVersionException이 발생하며 400 응답을 반환한다. setDefaultVersion("1.0")을 지정하면 버전 정보가 없는 요청은 자동으로 v1.0으로 라우팅된다.
Baseline 버전: “2.0+” 문법으로 하위 호환 처리
Spring Boot 4 API Versioning의 SemanticApiVersionParser는 baseline 버전 표기를 지원한다. version = "2.0+"로 설정하면 2.0 이상의 모든 버전 요청이 해당 핸들러로 라우팅된다.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
// v1.0 전용 핸들러
@GetMapping(value = "/{id}", version = "1.0")
public OrderDTOv1 getOrderV1(@PathVariable Long id) {
return orderService.findByIdV1(id);
}
// v1.1 이상 모든 요청을 처리하는 baseline 핸들러
@GetMapping(value = "/{id}", version = "1.1+")
public OrderDTOv2 getOrderV2(@PathVariable Long id) {
return orderService.findByIdV2(id);
}
}Java위 코드에서 v1.0 요청은 getOrderV1으로, v1.1, v1.2, v2.0 등 1.1 이상의 모든 요청은 getOrderV2로 라우팅된다. 새로운 마이너 버전을 추가할 때마다 핸들러를 만들 필요 없이 baseline 하나로 처리할 수 있어 유지보수 부담이 줄어든다.
Deprecation 처리, RFC 표준 헤더로 클라이언트에 알린다

API 버전을 폐기할 때 클라이언트에게 적절한 사전 안내가 필요하다. Spring Boot 4 API Versioning은 StandardApiVersionDeprecationHandler를 통해 RFC 표준 헤더를 자동으로 응답에 추가한다.
import org.springframework.web.accept.ApiVersionDeprecationHandler;
import org.springframework.web.accept.StandardApiVersionDeprecationHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DeprecationConfig {
@Bean
public ApiVersionDeprecationHandler deprecationHandler() {
// StandardApiVersionDeprecationHandler를 빈으로 등록하면
// deprecated 버전 요청 시 RFC 표준 헤더가 자동으로 응답에 추가된다
return new StandardApiVersionDeprecationHandler();
}
}JavaStandardApiVersionDeprecationHandler는 아래 세 가지 RFC 표준 헤더를 응답에 추가한다.
| 헤더 | RFC | 역할 |
|---|---|---|
Deprecation | RFC 9745 | 해당 API 버전이 폐기 예정임을 알린다 |
Sunset | RFC 8594 | API 버전이 완전히 제거되는 날짜를 명시한다 |
Link | — | 새로운 버전의 API 문서 위치를 안내한다 |
클라이언트는 이 헤더를 파싱해서 버전 업그레이드 알림을 자동으로 띄울 수 있고, 모니터링 시스템에서 폐기 예정 API 호출량을 추적하기에도 좋다. 예전에는 HttpServletResponse에 직접 헤더를 넣는 코드를 매번 작성해야 했는데, 그 수고가 사라진다.
잘못된 버전 요청이 들어오면 어떻게 되나

Spring Boot 4 API Versioning은 두 가지 예외를 자동으로 처리한다.
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.accept.InvalidApiVersionException;
import org.springframework.web.accept.MissingApiVersionException;
@RestControllerAdvice
public class ApiVersionExceptionHandler {
// 지원하지 않는 버전 요청 시 (예: v3.0 요청이 들어왔지만 1.0, 2.0만 지원)
@ExceptionHandler(InvalidApiVersionException.class)
public ProblemDetail handleInvalidVersion(InvalidApiVersionException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setTitle("Unsupported API Version");
problem.setDetail("요청한 API 버전 '" + ex.getMessage()
+ "'은(는) 지원하지 않는다. 지원 버전: 1.0, 2.0");
return problem;
}
// 버전 필수인데 헤더/파라미터가 누락된 경우
@ExceptionHandler(MissingApiVersionException.class)
public ProblemDetail handleMissingVersion(MissingApiVersionException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setTitle("Missing API Version");
problem.setDetail("API 버전 정보가 누락되었다. "
+ "X-API-Version 헤더 또는 version 파라미터를 포함해야 한다.");
return problem;
}
}JavaInvalidApiVersionException은 addSupportedVersions로 등록하지 않은 버전 요청 시 발생한다. MissingApiVersionException은 setVersionRequired(true) 상태에서 버전 정보 없이 요청이 들어올 때 발생한다. 두 예외 모두 기본적으로 400 Bad Request를 반환하지만, 위처럼 @ExceptionHandler로 커스텀 응답을 구성하면 클라이언트에게 더 명확한 에러 메시지를 전달할 수 있다.
테스트 코드로 Spring Boot 4 API Versioning 검증하기
Spring Boot 4는 MockMvc와 WebTestClient 모두에서 API 버전을 지정할 수 있는 기능을 제공한다.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void v1_요청시_name_단일_필드를_반환한다() throws Exception {
mockMvc.perform(get("/api/users")
.header("X-API-Version", "1.0"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").exists())
.andExpect(jsonPath("$[0].firstName").doesNotExist());
}
@Test
void v2_요청시_firstName_lastName_분리_필드를_반환한다() throws Exception {
mockMvc.perform(get("/api/users")
.header("X-API-Version", "2.0"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].firstName").exists())
.andExpect(jsonPath("$[0].lastName").exists())
.andExpect(jsonPath("$[0].name").doesNotExist());
}
@Test
void 지원하지_않는_버전_요청시_400을_반환한다() throws Exception {
mockMvc.perform(get("/api/users")
.header("X-API-Version", "99.0"))
.andExpect(status().isBadRequest());
}
@Test
void 버전_헤더_누락시_기본_버전으로_라우팅된다() throws Exception {
// setDefaultVersion("1.0"), setVersionRequired(false) 설정 시
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].name").exists());
}
}Java테스트 코드에서 .header("X-API-Version", "1.0")으로 버전을 지정하는 것만으로 해당 버전의 핸들러가 호출된다. 지원하지 않는 버전에 대한 400 응답, 기본 버전 폴백(fallback) 동작까지 테스트로 검증할 수 있다. RestTestClient를 사용하는 통합 테스트에서도 동일한 방식으로 버전을 지정할 수 있다.
실무에서 어떤 전략을 선택할 것인가

4가지 전략 중 하나를 선택해야 할 때 아래 기준을 참고한다. Spring Boot 4 API Versioning은 전략 하나만 활성화하는 것을 권장한다. Path Segment와 다른 전략을 동시에 사용하면 Path Segment가 우선 적용되어 혼란이 발생할 수 있다.
| 기준 | Path Segment | Header | Query Param | Media Type |
|---|---|---|---|---|
| URL 가시성 | /api/v1/users | /api/users | /api/users?v=1 | /api/users |
| 캐싱 친화도 | CDN/프록시 캐싱 용이 | Vary 헤더 필요 | 캐싱 키에 포함 | Vary 헤더 필요 |
| 브라우저 테스트 | URL만 변경 | DevTools 필요 | URL만 변경 | DevTools 필요 |
| 적합한 상황 | 공개 REST API | 내부 마이크로서비스 | 개발/테스트 환경 | 엔터프라이즈 표준 준수 |
| API Gateway 연동 | URL 패턴 라우팅 | 헤더 기반 라우팅 | 파라미터 라우팅 | Content-Type 라우팅 |
공개 API를 운영한다면 Path Segment 방식이 가장 무난하다. URL만 보고 버전을 파악할 수 있어 문서화와 디버깅이 쉽고, CDN 캐싱도 별도 설정 없이 동작한다.
내부 마이크로서비스 간 통신에서는 Header 방식이 적합하다. 서비스 간 URL 계약을 변경하지 않고 버전을 관리할 수 있어 배포 유연성이 높다.
Query Parameter 방식은 개발 단계에서 브라우저로 빠르게 테스트할 때 유용하지만, 프로덕션 API로는 권장하지 않는다. 쿼리 스트링이 길어지고 캐싱 전략이 복잡해질 수 있다.
Media Type 방식은 HTTP 표준에 가장 충실하지만 클라이언트 구현 복잡도가 높아 도입 전에 팀 합의가 필요하다.
커스텀 버전 파서로 “v1” 형식 지원하기

기본 SemanticApiVersionParser는 1.0, 2.1.3 같은 숫자 형식만 허용한다. v1, V2처럼 접두사가 붙은 형식을 사용하고 싶다면 커스텀 파서를 등록한다.
import org.springframework.web.accept.ApiVersionParser;
import org.springframework.web.accept.SemanticApiVersion;
public class PrefixAwareApiVersionParser implements ApiVersionParser<SemanticApiVersion> {
private final ApiVersionParser<SemanticApiVersion> delegate =
SemanticApiVersion.parser();
@Override
public SemanticApiVersion parseVersion(String version) {
// "v" 또는 "V" 접두사를 제거한 뒤 기본 파서에 위임한다
String normalized = version.startsWith("v") || version.startsWith("V")
? version.substring(1)
: version;
return delegate.parseVersion(normalized);
}
}Java위 파서는 v1, V2.1, 1.0 어떤 형식이든 SemanticApiVersion 객체로 변환한다. 기존 SemanticApiVersion.parser()에 전처리만 추가하는 방식이라 버전 비교 로직은 그대로 유지된다.
@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer
.usePathSegment(1)
.setVersionParser(new PrefixAwareApiVersionParser());
}
}JavasetVersionParser로 커스텀 파서를 등록하면 /api/v1/users와 /api/1.0/users 모두 동일한 핸들러로 라우팅된다.
클라이언트 측에서 버전을 지정하는 방법
Spring Boot 4 API Versioning은 서버 측뿐 아니라 클라이언트 측에서도 버전을 지정할 수 있다. RestClient와 WebClient 모두 지원한다.
import org.springframework.web.client.RestClient;
@Configuration
public class ClientConfig {
@Bean
public RestClient userServiceClient(RestClient.Builder builder) {
return builder
.baseUrl("http://user-service:8080/api")
// 모든 요청에 X-API-Version 헤더를 자동 추가한다
.defaultHeader("X-API-Version", "2.0")
.build();
}
}JavadefaultHeader로 버전 헤더를 설정하면 해당 RestClient 인스턴스의 모든 요청에 자동으로 버전이 포함된다. 마이크로서비스 간 통신에서 각 서비스가 사용하는 API 버전을 중앙에서 관리할 수 있다.

지금까지 Spring Boot 4 API Versioning에 대해서 정리해 보았다. 솔직히 이전까지 커스텀 HandlerMapping을 만들거나 서드파티 라이브러리를 끌어다 쓰던 것과 비교하면, version 속성 하나와 프로퍼티 몇 줄로 끝나는 지금 방식이 훨씬 깔끔하다. Deprecation 헤더까지 RFC 표준으로 자동 처리해 주니, 직접 구현할 때 빠뜨리기 쉬웠던 부분도 커버된다. 각 전략의 세부 옵션은 Spring Framework 공식 문서에서, 전체 변경 사항은 Spring Boot 4.0 Release Notes에서 확인할 수 있다.
