API 버저닝, 언제 올리고 언제 안 올리나

2026. 10. 21. 13:00·IT
728x90
반응형

API를 만들고 나면 언젠가 바꿔야 할 때가 온다. 필드 이름을 고치거나, 응답 구조를 바꾸거나, 없애야 할 엔드포인트가 생긴다. 그런데 이미 쓰고 있는 클라이언트가 있으면 마음대로 바꿀 수 없다. 이 글은 버저닝 방식의 선택지와, 그보다 먼저 고민해야 할 "버전을 올리지 않고 바꾸는 법"을 다룬다.

깨는 변경과 깨지 않는 변경

먼저 구분이 필요하다. 모든 변경이 새 버전을 요구하지는 않는다.

깨지 않는 변경(추가) 은 대체로 안전하다. 새 엔드포인트 추가, 응답에 새 필드 추가, 선택적 요청 파라미터 추가가 여기 속한다. 기존 클라이언트는 모르는 필드를 무시하면 되므로 영향이 없다.

깨는 변경은 이런 것들이다. 필드 삭제나 이름 변경, 타입 변경(문자열 → 숫자), 필수 파라미터 추가, 기본값 변경, 오류 코드 체계 변경, 엔드포인트 삭제.

실무에서는 대부분의 변경이 E 경로로 처리된다. 새 버전을 올리는 것은 유지 비용이 크기 때문에, 정말 구조를 바꿔야 할 때만 한다.

한 가지 주의할 점은 "모르는 필드를 무시한다"가 보장되지 않는 경우가 있다는 것이다. 엄격한 스키마 검증을 하는 클라이언트라면 새 필드만으로도 깨진다. 그래서 API 문서에 "응답에 필드가 추가될 수 있다"는 것을 명시해 두는 것이 좋다. 계약의 일부로 삼는 것이다.

버전을 어디에 표시할 것인가

새 버전이 필요하다면 위치를 정해야 한다. 세 가지가 흔하다.

방식 예 장점 단점
URL 경로 /v2/orders 눈에 보이고 캐시·라우팅이 쉽다 URL 이 리소스가 아니라 버전을 담는다
쿼리 파라미터 /orders?version=2 도입이 쉽다 빠뜨리기 쉽고 캐시 키가 지저분해진다
헤더 Accept: application/vnd.api.v2+json URL 이 깨끗하다 브라우저에서 테스트가 번거롭다

현실적으로는 URL 경로 방식이 가장 널리 쓰인다. 이론적으로는 헤더 방식이 더 옳다는 주장이 있지만, 로그에서 버전이 바로 보이고, 라우팅과 캐시 설정이 단순하고, 문서와 지원이 쉽다는 실용적 이점이 크다.

어느 방식이든 버전을 메이저 단위로만 올리는 것이 낫다. v1, v2면 충분하고 v1.2.3까지 가면 관리가 불가능해진다. 마이너한 변경은 버전 없이 처리한다.

여러 버전을 유지하는 비용

버전을 올리기 쉬워 보이지만, 실제 비용은 그 뒤에 온다. 두 버전을 동시에 유지해야 한다.

버그를 고치면 양쪽에 반영해야 한다. 보안 패치도 마찬가지다. 테스트도 두 벌이다. 데이터베이스 스키마를 바꾸면 두 버전 모두 동작해야 한다. 버전이 셋이 되면 이 부담이 더 커진다.

그래서 구조를 이렇게 잡는 것이 일반적이다. 핵심 로직은 하나만 두고, 버전별로 변환 계층만 둔다.

# 내부 도메인 모델은 하나
order = service.get_order(order_id)

# 버전별 직렬화만 분리
def serialize_v1(o):
    return {"id": o.id, "user": o.user_id, "price": o.amount}

def serialize_v2(o):
    return {"id": o.id, "userId": o.user_id,
            "amount": {"value": o.amount, "currency": o.currency}}

로직이 갈라지기 시작하면 감당이 안 된다. 변환만 갈라지게 유지하는 것이 핵심이다.

폐기에는 절차와 시간이 필요하다

옛 버전을 언제 없앨지도 미리 정해야 한다. 정하지 않으면 영원히 남는다.

절차를 정리하면 이렇다.

1단계: 공지. 폐기 예정일을 문서와 릴리스 노트에 밝힌다. 기간은 서비스 성격에 따라 다르지만, 외부 개발자가 쓰는 API라면 6개월에서 1년은 필요하다.

2단계: 응답에 경고를 싣는다. Deprecation과 Sunset 헤더로 알린다.

Deprecation: true
Sunset: Sat, 31 Oct 2026 23:59:59 GMT
Link: <https://docs.example.com/migrate-v2>; rel="deprecation"

3단계: 사용량을 관측한다. 버전별 호출 수와 호출자를 집계한다. 누가 아직 옛 버전을 쓰는지 모르면 끌 수 없다. 내부 서비스라면 직접 연락할 수 있다.

4단계: 예고된 중단(brownout). 폐기 전에 짧은 시간씩 일부러 응답을 막아본다. 아직 쓰고 있는 곳이 있으면 그때 드러난다. 실제 폐기일에 한꺼번에 터지는 것보다 낫다.

5단계: 제거. 예고한 날에 끈다. 미루기 시작하면 계속 미뤄진다.

버저닝보다 설계가 먼저다

마지막으로 관점을 하나 남기고 싶다. 버저닝은 잘못 설계한 API를 고치는 비용을 나중에 치르는 방법이다. 비용이 사라지는 것이 아니다.

그래서 처음 설계할 때 몇 가지를 지키면 버전을 올릴 일이 크게 준다.

응답을 객체로 감싼다. 배열을 최상위로 반환하면 나중에 메타데이터를 추가할 수 없다. {"items": [...]} 형태면 "next_cursor"나 "total"을 나중에 넣을 수 있다.

열거값은 확장을 전제로 한다. 클라이언트가 모르는 상태값을 받았을 때 어떻게 해야 하는지 문서에 적어둔다. 그러면 상태를 추가해도 깨지지 않는다.

내부 구조를 그대로 노출하지 않는다. 데이터베이스 컬럼을 그대로 응답 필드로 쓰면, 스키마를 바꿀 때마다 API가 깨진다. 중간에 변환 계층을 두면 내부를 자유롭게 바꿀 수 있다.

필드를 성급하게 추가하지 않는다. 한 번 내보낸 필드는 계약이 된다. 없애려면 폐기 절차를 거쳐야 한다. 필요한 것만 내보내는 편이 나중에 편하다.

이 네 가지만 지켜도 v2를 만들 일이 몇 년쯤 미뤄진다. 그 시간 동안 아낀 유지보수 비용이 설계에 들인 시간보다 훨씬 크다.

참고 자료

  • RFC 9745: The Deprecation HTTP Response Header Field — 폐기 알림 헤더 규격
  • RFC 8594: The Sunset HTTP Header Field — 서비스 종료 시점을 알리는 표준
  • Google Cloud API Design Guide: Versioning — 버전 표기와 호환성 정책 사례
728x90
반응형

'IT' 카테고리의 다른 글

메시지 큐를 언제 써야 하고 언제 쓰면 안 되나  (0) 2026.10.23
트랜잭션 격리 수준과 데드락 줄이기  (0) 2026.10.22
타임아웃과 재시도, 서킷 브레이커까지  (0) 2026.10.20
헬스체크와 무중단 배포, 프로브 제대로 쓰기  (0) 2026.10.19
페이지네이션, 오프셋 대신 커서를 쓸 때  (0) 2026.10.18
'IT' 카테고리의 다른 글
  • 메시지 큐를 언제 써야 하고 언제 쓰면 안 되나
  • 트랜잭션 격리 수준과 데드락 줄이기
  • 타임아웃과 재시도, 서킷 브레이커까지
  • 헬스체크와 무중단 배포, 프로브 제대로 쓰기
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (463)
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (66)
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    오류
    Java Array
    java casting
    자바 for문
    자바 객체 지향
    spring java
    svelte
    클론코딩
    nginx ssl 설정
    css transition
    css perspective
    AI 코딩 에이전트
    AI 반도체
    css float
    jsp parameter
    자바 exception
    css tb
    SSL 인증서
    API 설계
    React Compiler
    Extension Bisect
    제미나이
    자바 클래스
    jsp include
    vscode
    vue 설치
    lang rust
    servlet class
    자바 배열
    gitlab 설치
    프런트엔드
    ssl 인증서 발급
    Node
    리눅스 설치
    proxy pass
    ubuntu
    mysql db
    VS Code 팁
    티스토리 자동화
    docker
    에디터 팁
    자바 생성자
    Rust lang
    nginx
    css table
    브라우저 자동화
    rust linux
    러스트
    vue cli
    nginx ssl 적용
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
API 버저닝, 언제 올리고 언제 안 올리나
상단으로

티스토리툴바