Files
project-auth-server/docs/examples/web/api-versioning.md
T

4.5 KiB

API Versioning 예시

좋은 예시

예시 1. path major versioning으로 계약을 명시한다

@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을 제한적으로 활용한다

@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 공지를 준비한다

@RestController
@RequestMapping("/api/v1/sessions")
public class SessionV1Controller {
    // 구버전 유지
}

운영 정책 예:

  • 문서에 v1 deprecation 공지
  • 릴리스 노트에 종료 일정 공지
  • 응답 헤더에 deprecation/sunset/link 추가

좋은 이유:

  • 버전 종료가 갑작스럽지 않다
  • 클라이언트가 마이그레이션할 시간을 가진다
  • Spring도 deprecation 관련 응답 헤더 전송을 지원한다.

나쁜 예시

예시 1. 같은 API 군에서 path와 header versioning을 섞는다

@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을 올리지 않는다

public record UserResponse(
        String userId,
        String email,
        String displayName,
        String role
) {
}

기존에 email만 응답하던 endpoint가 같은 /api/v1/users/{id} 에서

  • 기존 필드 삭제
  • 필수 필드 의미 변경
  • 구조 변경

을 해 버리는 경우

나쁜 이유:

  • 기존 client를 조용히 깨뜨린다
  • versioning 목적 자체를 무력화한다

예시 3. minor/patch를 path에 과하게 노출한다

@RequestMapping("/api/v1.0.3/users")
public class UserController {
}

나쁜 이유:

  • 공개 URL이 불필요하게 복잡해진다
  • minor/patch 수준 변화까지 client 계약에 노출된다
  • 프로젝트의 major-only path 전략과 맞지 않는다

예시 4. 버전 누락 시 최신 버전으로 암묵 fallback한다

@GetMapping("/api/users/{userId}")
public ApiResult<UserResponse> getUser(@PathVariable String userId) {
    // 내부적으로 최신 버전 계약으로 응답
}

나쁜 이유:

  • client가 어떤 계약을 호출하는지 불명확하다
  • 시간이 지나며 응답 의미가 조용히 바뀔 수 있다
  • 명시적 계약 원칙과 맞지 않는다