포스트

API를 바꿔야 할 때 망가뜨리지 않는 법

이미 운영 중인 API를 수정할 때 클라이언트를 깨뜨리지 않는 법. 안전한 변경과 파괴적 변경의 경계, URI·헤더 버저닝, 관용적 파서 원칙을 정리한다.

API를 바꿔야 할 때 망가뜨리지 않는 법

이미 여러 클라이언트가 붙어 있는 API의 응답 형식을 바꿔야 하는 순간이 온다. 필드를 추가하거나, 이름을 고치거나, 의미를 바꾼다. 이때 가장 위험한 착각은 “내 코드만 고치면 된다”는 생각이다. API는 코드가 아니라 계약(contract) 이다. 한쪽이 계약을 어기면 상대는 말없이 깨진다.

핵심 개념: 무엇이 파괴적 변경인가

호환성의 기준은 단순하다. 기존 클라이언트가 코드를 고치지 않고도 계속 동작하는가. 이 기준으로 변경을 둘로 가른다.

안전한(하위호환) 변경

  • 응답에 새 필드 추가 — 옛 클라이언트는 모르는 필드를 무시한다.
  • 선택적(optional) 요청 파라미터 추가 — 안 보내면 기본값.
  • 새 엔드포인트 추가.

파괴적(breaking) 변경

  • 필드 삭제나 이름 변경.
  • 필드 타입/의미 변경 (예: status를 숫자→문자열, 또는 같은 값의 뜻을 바꿈).
  • 요청 파라미터를 필수로 만들기.
  • 에러 코드/HTTP 상태의 의미 변경.

핵심 직관: 추가는 대체로 안전하고, 삭제·변경은 대체로 파괴적이다. 이유는 호환성이 “양쪽이 같은 필드를 같은 의미로 안다”는 합의에서 나오기 때문이다. 추가는 합의를 깨지 않지만, 삭제·변경은 한쪽의 기대를 무너뜨린다.

버저닝 전략

파괴적 변경이 불가피하면 버전을 나눠 옛 클라이언트와 새 클라이언트를 공존시킨다.

URI 버저닝 — /v1/orders, /v2/orders. 가장 직관적이고, 캐시·로깅·라우팅에서 버전이 눈에 보인다. 가장 널리 쓰인다.

헤더 버저닝 — Accept: application/vnd.myapp.v2+json. URL은 자원의 정체성만 담아야 한다는 REST 순수주의에 가깝지만, 디버깅·캐시 키 관리가 번거롭다.

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/v1/orders/{id}")
public OrderV1 getV1(@PathVariable long id) {
    Order o = orderService.find(id);
    return OrderV1.from(o);   // 옛 스키마로 변환
}

@GetMapping("/v2/orders/{id}")
public OrderV2 getV2(@PathVariable long id) {
    Order o = orderService.find(id);
    return OrderV2.from(o);   // 새 스키마. 도메인은 하나, 표현만 둘
}

도메인 모델은 하나로 두고, 표현(DTO)만 버전별로 둔다. 비즈니스 로직을 버전마다 복제하면 유지보수가 폭발한다.

운영 함정

1) 깨고 나서야 안다. 응답 필드 하나를 지웠는데 그게 어느 클라이언트가 쓰는지 모른다. 그래서 로버스트니스 원칙(보낼 땐 엄격하게, 받을 땐 관대하게)을 적용한다. 파서는 모르는 필드에 예외를 던지지 말고 무시해야 하고, 필드 삭제는 곧바로 지우지 말고 deprecation 기간을 둔 뒤 사용량이 0인 걸 확인하고 제거한다.

2) JSON 역직렬화의 엄격 모드. 클라이언트 측 파서가 “알 수 없는 필드 → 실패”로 설정돼 있으면, 안전해야 할 필드 추가조차 파괴적이 된다. 서버가 호환을 지켜도 클라이언트가 깐깐하면 깨진다. 그래서 양쪽 모두 unknown 필드를 무시하도록 합의해 둔다.

핵심 요약

  • API는 계약이다. 변경의 안전성은 “옛 클라이언트가 그대로 도느냐” 하나로 판단한다.
  • 추가는 안전, 삭제·이름변경·의미변경은 파괴적.
  • 파괴적 변경은 버전 분리(URI 권장)로 공존시키고, 표현만 버전화하고 도메인은 단일화한다.
  • 받는 쪽은 모르는 필드를 무시(관용적 파서)하도록 만들어 둔다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.