182 lines
4.5 KiB
Markdown
182 lines
4.5 KiB
Markdown
# API Versioning 예시
|
|
|
|
## 좋은 예시
|
|
|
|
### 예시 1. path major versioning으로 계약을 명시한다
|
|
|
|
```java
|
|
@RestController
|
|
@RequiredArgsConstructor
|
|
@RequestMapping("/api/v1/users")
|
|
public class UserQueryV1Controller {
|
|
|
|
private final UserQueryUseCase userQueryUseCase;
|
|
|
|
@GetMapping("/{userId}")
|
|
public ApiResult<UserV1Response> getUser(@PathVariable String userId) {
|
|
UserResult result = userQueryUseCase.getUser(userId);
|
|
return ApiResult.success(new UserV1Response(
|
|
result.userId(),
|
|
result.email()
|
|
));
|
|
}
|
|
}
|
|
|
|
@RestController
|
|
@RequiredArgsConstructor
|
|
@RequestMapping("/api/v2/users")
|
|
public class UserQueryV2Controller {
|
|
|
|
private final UserQueryUseCase userQueryUseCase;
|
|
|
|
@GetMapping("/{userId}")
|
|
public ApiResult<UserV2Response> getUser(@PathVariable String userId) {
|
|
UserResult result = userQueryUseCase.getUser(userId);
|
|
return ApiResult.success(new UserV2Response(
|
|
result.userId(),
|
|
result.email(),
|
|
result.displayName()
|
|
));
|
|
}
|
|
}
|
|
```
|
|
|
|
**좋은 이유:**
|
|
|
|
- URL만 보고 major version이 드러난다
|
|
- 버전별 계약 차이가 controller와 DTO에서 명확하다
|
|
- 내부 use case는 공유하면서 외부 계약은 분리할 수 있다
|
|
|
|
### 예시 2. Spring 7+ native version mapping을 제한적으로 활용한다
|
|
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/accounts/{id}")
|
|
public class AccountController {
|
|
|
|
@GetMapping
|
|
public ApiResult<AccountLegacyResponse> getDefault(@PathVariable String id) {
|
|
return ApiResult.success(...);
|
|
}
|
|
|
|
@GetMapping(version = "1.1")
|
|
public ApiResult<AccountV11Response> getV11(@PathVariable String id) {
|
|
return ApiResult.success(...);
|
|
}
|
|
|
|
@GetMapping(version = "1.2+")
|
|
public ApiResult<AccountV12Response> getV12Plus(@PathVariable String id) {
|
|
return ApiResult.success(...);
|
|
}
|
|
}
|
|
```
|
|
|
|
**좋은 이유:**
|
|
|
|
- Spring이 공식 지원하는 version mapping 규칙을 따른다
|
|
- fixed version과 baseline version의 의미가 분명하다
|
|
- 단, 이 방식은 Spring 7+에 맞는 선택지다.
|
|
|
|
### 예시 3. deprecated version에 sunset 공지를 준비한다
|
|
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/v1/sessions")
|
|
public class SessionV1Controller {
|
|
// 구버전 유지
|
|
}
|
|
```
|
|
|
|
운영 정책 예:
|
|
|
|
- 문서에 v1 deprecation 공지
|
|
- 릴리스 노트에 종료 일정 공지
|
|
- 응답 헤더에 deprecation/sunset/link 추가
|
|
|
|
**좋은 이유:**
|
|
|
|
- 버전 종료가 갑작스럽지 않다
|
|
- 클라이언트가 마이그레이션할 시간을 가진다
|
|
- Spring도 deprecation 관련 응답 헤더 전송을 지원한다.
|
|
|
|
## 나쁜 예시
|
|
|
|
### 예시 1. 같은 API 군에서 path와 header versioning을 섞는다
|
|
|
|
```java
|
|
@RestController
|
|
@RequestMapping("/api/v1/users")
|
|
public class MixedVersionController {
|
|
|
|
@GetMapping
|
|
public ApiResult<List<UserResponse>> getUsers() {
|
|
return ApiResult.success(...);
|
|
}
|
|
|
|
@GetMapping(headers = "API-Version=2")
|
|
public ApiResult<List<UserResponse>> getUsersV2() {
|
|
return ApiResult.success(...);
|
|
}
|
|
}
|
|
```
|
|
|
|
**나쁜 이유:**
|
|
|
|
- 버전 협상 위치가 두 군데다
|
|
- client, gateway, 문서, 테스트가 모두 복잡해진다
|
|
- 한 API product 안의 일관성을 깨뜨린다
|
|
|
|
### 예시 2. breaking change인데 version을 올리지 않는다
|
|
|
|
```java
|
|
public record UserResponse(
|
|
String userId,
|
|
String email,
|
|
String displayName,
|
|
String role
|
|
) {
|
|
}
|
|
```
|
|
|
|
기존에 email만 응답하던 endpoint가 같은 /api/v1/users/{id} 에서
|
|
|
|
- 기존 필드 삭제
|
|
- 필수 필드 의미 변경
|
|
- 구조 변경
|
|
|
|
을 해 버리는 경우
|
|
|
|
**나쁜 이유:**
|
|
|
|
- 기존 client를 조용히 깨뜨린다
|
|
- versioning 목적 자체를 무력화한다
|
|
|
|
### 예시 3. minor/patch를 path에 과하게 노출한다
|
|
|
|
```java
|
|
@RequestMapping("/api/v1.0.3/users")
|
|
public class UserController {
|
|
}
|
|
```
|
|
|
|
**나쁜 이유:**
|
|
|
|
- 공개 URL이 불필요하게 복잡해진다
|
|
- minor/patch 수준 변화까지 client 계약에 노출된다
|
|
- 프로젝트의 major-only path 전략과 맞지 않는다
|
|
|
|
### 예시 4. 버전 누락 시 최신 버전으로 암묵 fallback한다
|
|
|
|
```java
|
|
@GetMapping("/api/users/{userId}")
|
|
public ApiResult<UserResponse> getUser(@PathVariable String userId) {
|
|
// 내부적으로 최신 버전 계약으로 응답
|
|
}
|
|
```
|
|
|
|
**나쁜 이유:**
|
|
|
|
- client가 어떤 계약을 호출하는지 불명확하다
|
|
- 시간이 지나며 응답 의미가 조용히 바뀔 수 있다
|
|
- 명시적 계약 원칙과 맞지 않는다
|