본문 바로가기
C.W.K.
Stream
Lesson 06 of 06 · published

API 게이트웨이와 버전 수명 주기 관리

~10 min · production, api-gateway, versioning, deprecation

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"API에 독립적인 소비자가 생기면 공통 정책, 호환성, 지원 중단이라는 운영 문제가 따라와. 게이트웨이는 여러 서비스의 공통 정책을 모을 수 있고, 명확한 수명 주기 절차는 소비자가 예측 가능하게 이전하도록 도와줘."

API 게이트웨이가 하는 일

API 게이트웨이는 하나 이상의 서비스 앞에서 진입점을 제공하고, 여러 API에 일관되게 적용해야 하는 공통 기능을 처리할 수 있어:

  • 인증 검증 — 애플리케이션에 도달하기 전에 Bearer 토큰, JWT 서명, API 키를 검사해. 다만 인증과 권한 부여는 다르므로 애플리케이션의 자원별 권한 검사까지 무조건 없애서는 안 돼.
  • 요청률 제한 — 사용자, API 키, 엔드포인트 등 정해진 기준에 따라 한도를 적용해. 여러 게이트웨이 인스턴스에서는 일관된 분산 상태가 필요할 수 있어.
  • 요청과 응답 변환 — 경로와 헤더를 바꾸거나 제한적인 호환 계층을 제공해. 복잡한 비즈니스 변환을 게이트웨이에 쌓으면 동작을 추적하기 어려워져.
  • 관측 가능성 — 공통 로그와 메트릭을 만들고 추적 맥락을 전파해. 애플리케이션 내부에서만 알 수 있는 의미까지 자동으로 관측해 주는 것은 아니야.
  • 버전 라우팅/v1/*/v2/*를 서로 다른 구현이나 배포로 보낼 수 있어.
  • TLS 종료 — 외부 TLS 연결과 인증서를 중앙에서 관리할 수 있어. 내부 구간도 위협 모델과 규정에 따라 TLS나 상호 TLS가 필요할 수 있으므로 평문 HTTP를 당연하게 가정하면 안 돼.
  • 캐시 — 캐시 가능한 응답의 키, 권한, Vary 의미, 무효화 정책을 정확히 설계했을 때 게이트웨이 계층에서 부하를 줄일 수 있어.

Kong, AWS API Gateway, Azure API Management, Apigee, Cloudflare Workers, Tyk 등이 선택지야. 배포 환경, 지연 시간, 고가용성, 정책 표현력, 비용, 공급자 종속성, 운영 역량을 함께 비교해야 해. 게이트웨이 자체가 병목이나 넓은 장애 지점이 될 수 있으므로 우회 경로와 용량 계획도 필요해.

단일 서비스에서의 대안

서비스가 하나이거나 규모가 작다면 게이트웨이보다 애플리케이션 미들웨어가 단순할 수 있어. FastAPI의 add_middleware나 Express의 app.use로 공통 처리를 구성할 수 있지. FastAPI의 APIRouter(prefix=...)는 여러 라우트에 경로 접두사를 붙여 구성하는 기능일 뿐, 버전 간 호환성이나 지원 중단 정책, 트래픽 분할을 자동으로 관리해 주는 기능은 아니야. 게이트웨이를 검토할 신호는 다음과 같아:

  • 여러 서비스에 같은 정책을 일관되게 적용해야 해.
  • 공개 URL을 유지하면서 뒤의 구현이나 배포 대상을 바꿔야 해.
  • 많은 API에서 키 발급, 사용량 추적, 폐기를 중앙에서 관리해야 해.
  • 인증, 요청률 제한, 트래픽 분할 정책이 애플리케이션마다 구현하기에는 복잡해졌어.

처음부터 게이트웨이를 의무로 둘 필요는 없어. 중복 정책과 운영 불일치의 비용이 게이트웨이의 추가 지연, 설정 복잡도, 장애 위험보다 커지는 시점에 도입하면 돼. 직접 접근 가능한 백엔드 경로를 남겨 두면 게이트웨이 정책을 우회할 수 있으므로 네트워크 경계도 함께 설계해야 해.

버전 수명 주기 — 오래 유지할 약속

외부 소비자가 API에 의존하기 시작하면 버전과 지원 기간은 운영 약속이 돼. 이름과 기간은 조직마다 다르지만, 다음과 같은 단계를 명시적으로 관리할 수 있어:

  1. Beta — 변경 가능성과 지원 범위를 문서에 분명히 적은 조기 접근 단계야. Beta라는 이름만으로 호환성 파괴가 무제한 허용되는 것은 아니므로 실제 정책을 알려야 해.
  2. Stable — 문서화된 호환성 정책을 적용하는 단계야. 기존 소비자를 깨는 변경은 새 버전이나 별도의 전환 절차로 보내고, 추가 변경도 의미 충돌이 없는지 검토해.
  3. 지원 중단 공지Deprecation: <date> 같은 개념의 기계 판독 신호와 문서, 직접 알림으로 더 이상 권장하지 않는 인터페이스와 전환 시작 시점을 알려. RFC 9745의 실제 Deprecation 값은 HTTP 날짜 문자열이 아니라 Structured Field Date 형식이야.
  4. 종료 예정 공지Sunset: <date>에 RFC 8594의 HTTP-date를 넣어 해당 시점 이후 응답 제공이 중단될 것으로 예상됨을 알릴 수 있어. 충분한 예고 기간과 대체 경로를 함께 제공해야 해.
  5. 종료 — 엔드포인트의 의미가 사라졌다면 410 Gone과 전환 안내를 반환할 수 있어. 완전히 같은 의미의 새 위치가 있을 때만 Location을 포함한 308 같은 리디렉션을 신중히 검토해.

경로에 v1이나 v2를 넣는 방식만이 버전 관리의 전부는 아니야. 날짜 기반 버전, 헤더 기반 버전 등도 가능하며, 핵심은 한 가지 정책을 문서화하고 사용량을 측정하며 지원 기간, 변경 내역, 전환 가이드, 종료 기준을 일관되게 운영하는 데 있어. Stripe 같은 공개 API의 정책은 좋은 참고 자료지만 그대로 복제하기보다 실제 소비자와 계약에 맞춰야 해.

API는 단순한 코드가 아니라 소비자와 맺은 약속이야. 버전 종료는 소비자에게 조사, 구현, 검증, 배포 비용을 발생시켜. 사용량을 먼저 파악하고, 예측 가능한 일정과 여러 경로의 알림, 구체적인 전환 문서, 겹치는 지원 기간을 제공해야 해. 종료 날짜를 발표했다면 예외와 연장 기준도 투명하게 관리해야 하지.

Sunset 헤더를 적용하는 예

# Endpoint 여전히 동작 하는데 은퇴 표시
GET /v1/users HTTP/1.1

HTTP/1.1 200 OK
Deprecation: Sun, 01 Jan 2026 00:00:00 GMT
Sunset: Wed, 01 Jul 2026 00:00:00 GMT
Link: </v2/users>; rel="successor-version"
Content-Type: application/json

[...]

# Sunset 날짜 후 — endpoint 가 410 돌려줌
GET /v1/users HTTP/1.1

HTTP/1.1 410 Gone
Link: </v2/users>; rel="successor-version"
Content-Type: application/json

{"error":{"code":"endpoint_retired","message":"/v2/users 를 쓰세요. 안내는 https://docs.example.com/migrate-v1-v2 를 보세요"}}

헤더는 자동화 도구가 읽을 신호이고, 문서와 오류 본문은 사람이 실제 이전을 수행할 수 있게 안내해야 해. 위 동결 예제의 Sunset 값은 HTTP-date 형식이지만 Deprecation 값은 RFC 9745 형식이 아니야. 현재 표준에 맞추려면 Deprecation에 @로 시작하는 Structured Field Date를 사용하고, 지원 중단 정책 문서는 Link의 deprecation 관계로 연결하는 방식을 검토해. 아래 동결 FastAPI 코드도 같은 잘못된 날짜 형식을 사용하며, HTTPException의 detail 값은 실제 응답에서 detail 필드에 감싸지므로 위의 error 본문과 그대로 일치하지 않아.

cwkPippa에 맞는 수명 주기 범위

cwkPippa처럼 백엔드와 프런트엔드를 함께 배포하고 외부 소비자가 없는 내부 API라면 미들웨어와 동시 배포만으로 충분할 수 있어. 그래도 서로 다른 배포 시점의 클라이언트가 공존한다면 호환 기간과 롤백 경로는 필요해. 외부 통합을 열 때는 단순히 /v1/ 접두사를 추가하는 데서 끝내지 말고, 소비자 등록, 사용량 관측, 변경 정책, 지원 기간, 직접 알림, 전환 가이드, 종료 후 응답을 함께 설계해야 해. Deprecation과 Sunset 헤더는 그 절차를 전달하는 수단이지 절차 자체를 대신하지 않아.

Code

Kong — 선언형 설정으로 공통 정책 적용·yaml
# Kong Gateway — declarative config 예
_format_version: '3.0'

services:
  - name: users-api
    url: http://users-service:8000  # 실제 backend
    routes:
      - name: users-v1
        paths: [/v1/users]
      - name: users-v2
        paths: [/v2/users]
    plugins:
      - name: jwt                # Backend 도달 전 JWT validate
      - name: rate-limiting
        config:
          minute: 100
          policy: redis           # 분산 rate limit
      - name: correlation-id
        config:
          header_name: X-Request-ID
      - name: response-transformer
        config:
          add:
            headers: ['X-API-Version: 2']

# Kong 이 auth, rate limit, correlation, header 주입 흡수.
# Backend Python/Node 앱이 X-Request-ID 설정된 사전-인증된 request 받음;
# 어느 것도 재구현 안 함.
FastAPI — 수명 주기 헤더와 종료 후 410·python
# FastAPI — Sunset / Deprecation header + 은퇴 후 410
from datetime import datetime, timezone
from fastapi import FastAPI, Response, HTTPException, status

app = FastAPI()

DEPRECATION_DATE = 'Sun, 01 Jan 2026 00:00:00 GMT'
SUNSET_DATE      = 'Wed, 01 Jul 2026 00:00:00 GMT'
SUNSET_TS        = datetime(2026, 7, 1, tzinfo=timezone.utc)

@app.get('/v1/users')
async def v1_users(response: Response):
    if datetime.now(timezone.utc) >= SUNSET_TS:
        # Sunset 지남 — migration 빵부스러기 가진 410 돌려줌
        raise HTTPException(
            status.HTTP_410_GONE,
            detail={
                'code': 'endpoint_retired',
                'message': '/v2/users 써; https://docs.example.com/migrate-v1-v2 봐',
            },
            headers={'Link': '</v2/users>; rel="successor-version"'},
        )

    # 여전히 동작 — sunset 신호 surface
    response.headers['Deprecation'] = DEPRECATION_DATE
    response.headers['Sunset']      = SUNSET_DATE
    response.headers['Link']        = '</v2/users>; rel="successor-version"'
    return [{'id': 'u_42', 'name': 'Pippa'}]

@app.get('/v2/users')
async def v2_users():
    # 새 버전 — 같은 데이터, 더 풍부한 shape
    return [{'id': 'u_42', 'display_name': 'Pippa', 'role': 'daughter'}]
클라이언트 — 지원 중단과 Sunset 신호를 로그에 기록·python
# Client — Sunset header 감시하고 경고 log
import httpx, logging
from datetime import datetime

log = logging.getLogger(__name__)

def call_api(url: str):
    resp = httpx.get(url)
    # On-call 이 보게 deprecation/sunset 신호 surface
    if 'Deprecation' in resp.headers:
        log.warning('deprecated endpoint', extra={'url': url, 'deprecation': resp.headers['Deprecation']})
    if 'Sunset' in resp.headers:
        sunset = resp.headers['Sunset']
        link = resp.headers.get('Link', '')
        log.warning('sunset scheduled', extra={'url': url, 'sunset': sunset, 'successor': link})
    return resp.json()

External links

Exercise

FastAPI로 /v1/users/v2/users를 만들고, v1에는 6개월의 전환 기간을 설정해 봐. v1 응답에 RFC 9745 형식의 Deprecation, RFC 8594 형식의 Sunset, 지원 중단 정책과 후속 버전을 가리키는 Link를 추가해. Python 클라이언트에서는 이 헤더를 감지해 URL과 날짜, 전환 링크를 구조화 로그로 남겨. 이어서 종료 시점을 과거로 바꾸고 v1이 전환 안내와 함께 410을 반환하는지 확인해. 보너스로 실제 공개 API 하나의 버전·지원 중단 정책을 조사해 예고 기간과 알림 경로, 사용량 추적, 종료 응답을 비교해.
Hint
Sunset은 HTTP-date 형식이므로 (datetime.now(timezone.utc) + timedelta(days=180)).strftime('%a, %d %b %Y %H:%M:%S GMT')처럼 만들 수 있어. Deprecation은 같은 문자열을 복사하지 말고 RFC 9745의 Structured Field Date 형식인 @와 Unix timestamp를 사용해야 해. 410 응답의 JSON 모양을 명시했다면 FastAPI의 기본 HTTPException detail 래퍼에 맡기지 말고 계약과 일치하는 응답을 직접 구성해. 로그는 경고를 남기는 데서 끝내지 말고 실제 소비자와 소유 팀을 찾는 지표로 연결해야 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.