infra
Platform

모듈 맵

[SW Eng] API와 계약 — REST·GraphQL·스키마로 서비스가 대화하는 법

0 / 38 완료

펼치기
0 / 38 완료0%

PM·SRE를 위한 소프트웨어 엔지니어링 · 19 / 38

[SW Eng] API와 계약 — REST·GraphQL·스키마로 서비스가 대화하는 법

REST/GraphQL·엔드포인트·요청/응답·직렬화·OpenAPI 계약·버전 관리를 PM·인프라 관점에서 정리하고, 계약이 흔들릴 때의 협업 비용을 다룹니다

🚨INCIDENT ALERT
HIGH

프론트 개발자가 화났습니다. "어제까지 응답에 있던 userName 필드가 오늘 사라졌어요. 화면이 다 깨졌습니다." 백엔드는 "아, name으로 바꿨는데요"라고 답합니다. 공지도, 버전도 없었습니다. 또 다른 회의에선 "그 API 응답이 어떻게 생겼죠?"를 두고 FE·BE가 서로 다른 그림을 그리고 있습니다. API는 서비스들이 대화하는 '계약'입니다. 이 계약이 명확하고 안정적이면 팀이 병렬로 빠르게 달리고, 흔들리면 서로의 작업이 끊임없이 깨집니다.

이번 챕터에서 배울 것
  • 1REST의 자원(경로)·행위(HTTP 메서드) 모델을 설명할 수 있다
  • 2API 계약(요청/응답 스키마)이 FE↔BE 병렬 개발을 가능케 함을 설명할 수 있다
  • 3REST와 GraphQL의 차이를 "고정 응답 vs 필드 선택"으로 구분할 수 있다
  • 4API breaking change를 버전·deprecated·공지로 안전하게 내는 법을 설명할 수 있다

REST — 자원과 행위

💡개념

경로는 '무엇을', 메서드는 '어떻게'

REST API는 자원(경로)행위(HTTP 메서드) 의 조합으로 표현합니다.

TEXT
GET    /orders        주문 목록 조회      (안전·멱등: 여러 번 호출해도 같음)
POST   /orders        새 주문 생성        (호출마다 새로 생김)
GET    /orders/42     42번 주문 조회
PATCH  /orders/42     42번 주문 일부 수정
DELETE /orders/42     42번 주문 삭제      (멱등: 여러 번 = 결과 같음)

요청/응답은 보통 JSON:
  요청 Body:  {"productId": 7, "qty": 2}
  응답 Body:  {"orderId": 42, "status": "created"}

핵심 규약: GET은 데이터를 바꾸지 않는다(안전), 같은 요청을 여러 번 해도 결과가 같아야 하는 것이 멱등성(요구사항 정의의 중복 처리와 연결). HTTP 상태코드(200/201/400/404/500…)로 결과를 알립니다 — 깊은 코드 해석은 용어사전·networking 트랙에서.

API 계약 — 클라이언트와 서버가 약속으로 병렬 개발확대 위 그림처럼 API 계약을 먼저 정하면 프론트엔드와 백엔드가 서로 기다리지 않고 동시에 개발할 수 있습니다.

계약 — 먼저 합의하면 병렬로 달린다

💡개념

API 계약: FE와 BE가 동시에 일하게 하는 약속

개발 조직의 역할에서 본 'FE↔BE 인터페이스'가 바로 API 계약입니다. 계약은 다음을 정합니다.

TEXT
계약(예: OpenAPI 명세):
  엔드포인트:  POST /orders
  요청 스키마:  { productId: number(필수), qty: number(필수, ≥1) }
  응답 스키마:  { orderId: number, status: "created"|"failed" }
  에러 형식:   { code: string, message: string }  (예: 400, 409)

계약을 먼저 합의하면:

  • FE는 그 계약으로 목(mock) 서버를 띄워 화면을 병렬 개발
  • BE는 그 계약에 맞춰 구현
  • 둘이 만나는 지점에서 어긋남이 최소화

도구: OpenAPI(Swagger) 명세로 계약을 코드처럼 관리하고, 스키마 검증(JSON Schema) 으로 요청/응답이 계약을 지키는지 자동 확인합니다. 계약이 문서로 고정되면 "그 API 어떻게 생겼죠?"라는 소모적 질문이 사라집니다.

OpenAPI 스펙 — 계약 한 파일로 문서·Mock·테스트·SDK 자동 생성확대 위 그림처럼 openapi.yaml 한 파일이 Swagger UI 문서, Mock 서버, 계약 테스트, SDK 자동생성을 모두 이끌어냅니다.

REST vs GraphQL — 언제 무엇을
표준적 CRUD, 캐싱·단순함 중시REST엔드포인트별 고정 응답, HTTP 캐시 친화
화면마다 필요한 데이터 조합이 크게 다름(모바일·대시보드)GraphQL클라이언트가 필드 선택, over/under-fetch 완화
외부 공개 API·단순 연동REST생태계·도구 풍부, 진입장벽 낮음
실시간 양방향(채팅·알림)WebSocket/SSE 별도REST/GraphQL과 병행

계약 준수·변경 점검 — 직접 확인

1API 응답이 계약(스키마)과 일치하는지 확인

연동 문제나 화면 깨짐이 의심되면, 실제 응답을 계약과 대조합니다. 코드를 못 짜도 할 수 있는 점검입니다.

로컬 또는 서버
# 실제 응답 구조 확인
curl -s https://api.example.com/orders/42 | jq .

# 계약(OpenAPI)에 명시된 필드와 실제 응답 필드 대조
curl -s https://api.example.com/orders/42 | jq 'keys'
# 기대: ["orderId","status","items","createdAt"]
OUTPUT
$ curl -s .../orders/42 | jq 'keys'
["orderId", "name", "items"]      ← 계약엔 "userName"인데 실제는 "name"!
                                    + "status","createdAt" 누락
→ 계약과 응답 불일치. FE 화면 깨짐의 원인. 누가 언제 바꿨나 추적([[git-fundamentals]])
curl -s https://api.example.com/orders/42 | jq .
🔍실행 후 확인할 것
  • jq keys로 응답 필드 목록을 계약과 대조 — 계약에 있는데 응답에 없으면(필드 삭제/이름변경) breaking change. FE 깨짐의 직접 원인
  • HTTP 상태코드를 먼저 본다: 2xx면 성공인데 형식이 다른 것, 4xx면 요청 잘못, 5xx면 서버 장애 — 방향이 완전히 다르다(용어사전)
  • 같은 요청을 두 번 보냈을 때 POST가 주문을 2개 만들면 멱등성 미보장 → 중복 결제 위험. Idempotency-Key 적용 여부 확인
  • 응답 필드가 바뀌었으면 git에서 "언제 누가" 바꿨는지 추적해, 버전/공지 없이 낸 breaking change인지 확인(시맨틱 버저닝)

상황: 백엔드가 정리 차원에서 응답 필드 userNamename으로 바꿨는데, 공지·버전 없이 배포돼 프론트엔드와 외부 연동사의 코드가 일제히 깨집니다.

원인: 계약을 깨는 변경(breaking change)을 통보·버전 없이 냈습니다. API 응답 형식은 클라이언트와의 계약인데, 한쪽이 임의로 바꾸면 그 형식에 의존하던 모든 곳이 무너집니다(시맨틱 버저닝).

진단:

로컬 터미널
# 계약(OpenAPI) diff 또는 응답 필드 변화 확인
git log -p -- openapi.yaml | grep -A2 -B2 "userName\|name"

해결: (1) 즉시 호환 복구 — 옛 필드(userName)를 유지하면서 새 필드(name)를 함께 제공(둘 다 응답). (2) 변경은 버전 분리(/v2) 또는 deprecated 표기 + 유예 기간 + 사전 공지로. (3) CI에 계약 호환성 검사(OpenAPI diff)를 넣어 breaking change가 게이트에서 잡히게(CI/CD 파이프라인). API는 '내 코드'가 아니라 '여러 팀·외부와의 계약'이라는 인식이 핵심입니다.

REST vs GraphQL — 데이터 요청 왕복 횟수 비교확대 위 그림처럼 REST는 데이터 종류마다 별도 요청이 필요하지만, GraphQL은 단일 쿼리로 필요한 필드만 정확하게 가져옵니다.

심화 — '하위 호환'의 회색지대

💡개념

심화: 추가는 안전하다는 착각 — 호환성은 문법이 아니라 소비자가 정한다

"필드 삭제는 breaking, 추가는 안전" — 계약 관리의 첫 규칙이지만, 실무의 사고는 이 규칙의 회색지대에서 납니다.

  • 추가가 안전한 건 소비자가 관대할 때뿐입니다: 모르는 필드를 무시하는 클라이언트(tolerant reader)에겐 필드 추가가 무해하지만, 알 수 없는 필드를 만나면 역직렬화를 거부하도록 엄격하게 설정된 클라이언트는 '안전한 추가'에도 깨집니다. 계약에는 스키마만이 아니라 "모르는 필드는 무시한다" 같은 소비 규칙까지 합의돼야 합니다.
  • enum 확장은 사실상 breaking입니다: 응답의 상태값 목록에 새 값을 추가하면, 그 값을 모르는 클라이언트의 분기(switch)가 미정의 경로로 빠집니다. 필드를 지우지 않았어도 클라이언트가 죽습니다. enum이 있는 계약엔 "알 수 없는 값이 오면 이렇게 처리한다"는 폴백 조항이 필요합니다.
  • 스키마 diff는 문법만 봅니다: CI의 OpenAPI 호환성 검사는 필드 유무·타입 변화는 잡지만, amount의 단위가 원에서 센트로 바뀌는 것 같은 의미 변화는 통과시킵니다. 이를 보완하는 것이 소비자 주도 계약 테스트(consumer-driven contract, Pact류)입니다 — 각 소비자가 "내가 실제로 쓰는 필드와 기대값"을 계약으로 등록하고, 공급자 CI가 그 기대를 검증합니다. 부수 효과로 '아무도 안 쓰는 필드'가 드러나 자유롭게 정리할 수 있게 됩니다.
  • /v2의 숨은 비용은 v1입니다: 버전을 올리는 건 쉽지만, 옛 버전이 살아 있는 한 그것은 별도의 제품입니다 — 보안 패치·모니터링·문서·테스트가 전부 이중화됩니다. 버전별 호출량 계측(누가 아직 v1을 쓰나) 없이 버전을 늘리면, 몇 년 뒤 '끌 수 없는 v1'이 유지보수의 절반을 먹습니다.

상황: 부분 환불 기능을 출시하며 주문 상태 enum에 PARTIALLY_REFUNDED를 추가했습니다. 스키마상 필드 변화가 없어 호환성 검사(OpenAPI diff)도 통과했고 웹은 멀쩡한데, 배포 몇 시간 뒤 구버전 모바일 앱의 주문내역 화면 크래시가 폭증합니다.

원인: 구버전 앱은 상태값을 enum으로 강타입 파싱하고 있었고, 알 수 없는 값에 대한 폴백이 없었습니다. enum 확장은 스키마 diff가 못 잡는 breaking change입니다. 웹은 즉시 배포로 새 값을 알고 있었지만, 모바일은 사용자가 업데이트하기 전까지 옛 코드가 남는 롱테일이 있어 피해가 길게 이어집니다.

진단:

TEXT
□ 크래시가 특정 앱 버전 이하에서만 나나? (구버전 롱테일 확정)
□ 크래시 로그가 응답 파싱(역직렬화) 단계를 가리키나?
□ 최근 배포에서 응답의 '값 목록'이 바뀐 게 있나? (필드가 아니라 값)

해결: (1) 급한 불은 서버에서 끕니다 — 구버전 클라이언트에는 새 상태를 기존 값(예: REFUNDED)으로 매핑해 내려주는 호환 계층을 둡니다. 앱 심사·업데이트는 몇 주가 걸리므로 서버가 맞춰야 합니다. (2) 계약에 "enum은 확장될 수 있으며, 알 수 없는 값은 지정된 기본 동작으로 처리한다"는 조항을 넣고 클라이언트 구현 가이드에 폴백을 의무화합니다. (3) CI 호환성 검사가 enum 값 추가를 경고로 잡도록 규칙을 보강하고, 모바일처럼 강제 업데이트가 불가능한 소비자가 있는 API는 값 추가도 버전 정책(시맨틱 버저닝)으로 다룹니다.

💼
실무 맥락
현업 패턴

인프라/플랫폼 관점에서 API 계약은 트래픽 제어와 이벤트 기반 연계 구조·게이트웨이 설정, 버전 라우팅(/v1, /v2), 스키마 검증·레이트리밋의 기준이 됩니다. CI에 OpenAPI 호환성 검사를 넣어 breaking change를 머지 게이트에서 막는 것도 플랫폼팀의 역할입니다. PM은 API 계약을 'FE·BE·외부 연동사가 공유하는 합의서'로 관리해, 변경 시 영향 범위(누가 이 API를 쓰나)와 마이그레이션 일정을 조율합니다. 특히 외부에 공개된 API의 breaking change는 연동사 신뢰와 직결되므로, deprecated 정책과 공지 채널을 미리 갖춰야 합니다.

다음 모듈에서는 이런 서비스들이 환경에 흔들리지 않고 동작하도록 만드는 설계 원칙 — 12-Factor App을 다룹니다.

실전 랩으로 손에 익히기: API 계약 설계 실습 — 리소스·상태코드·에러형식·버저닝·호환성을 계약으로 확정합니다.

지식 확인

퀴즈 — 8문제

Q1

REST API에서 'POST /orders'와 'GET /orders/42'의 의미 차이는?

Q2

API 계약(contract)이 'FE↔BE 합의'로 중요한 이유는?

Q3

GraphQL이 REST와 다른 핵심 특징은?

Q4

API에 'breaking change'를 낼 때 권장되는 방식은?

Q5

REST API에서 200·201·400·404·500 같은 상태코드를 올바르게 쓰면 좋은 점은?

Q6

API 계약을 먼저 합의하면 프론트·백엔드가 '동시에' 일할 수 있는 이유는?

Q7

[심화] API에 breaking change가 필요해 /v2로 버전을 올리기로 했다. 이 선택의 '숨은 비용'을 가장 정확히 설명한 것은?

Q8

[심화] 주문 상태 enum에 PARTIALLY_REFUNDED를 추가하자 스키마 호환성 검사(OpenAPI diff)는 통과했고 웹은 멀쩡한데, 구버전 모바일 앱만 주문내역 화면 크래시가 폭증한다. 원인과 급한 처방으로 옳은 것은?

0 / 8 답변

🧪 실습으로 확인하기

API 계약 설계 — 리소스·상태코드·버저닝·에러 형식

중급

REST API의 리소스·메서드·상태코드를 설계하고, 일관된 에러 응답·페이지네이션·버저닝 규칙을 세운 뒤, 계약 우선(contract-first)으로 프론트·백이 합의하고 호환성 깨는 변경을 판단한다.

55📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요