<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Schema Evolution on jyukki's Blog</title><link>https://jyukki.com/tags/schema-evolution/</link><description>Recent content in Schema Evolution on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Tue, 28 Jul 2026 11:30:00 +0900</lastBuildDate><atom:link href="https://jyukki.com/tags/schema-evolution/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: API Response Compatibility Contract, 응답 스키마 변경을 사고 없이 배포하는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-api-response-compatibility-contract-playbook/</link><pubDate>Tue, 28 Jul 2026 10:06:00 +0900</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-api-response-compatibility-contract-playbook/</guid><description>API 응답 필드 추가·삭제·이름 변경·enum 확장·nullable 변경이 클라이언트 호환성과 성능에 어떤 영향을 주는지, 배포 전 어떤 숫자와 기준으로 판단해야 하는지 정리합니다.</description><content:encoded><![CDATA[<p>API 응답은 서버가 마음대로 바꿀 수 있는 JSON 덩어리가 아닙니다. 모바일 앱, 웹 프론트엔드, 파트너 연동, 배치 수집기, 사내 어드민, 다른 microservice가 같은 응답을 각자의 속도로 읽습니다. 서버는 오늘 배포할 수 있어도 모바일 앱은 사용자 업데이트가 필요하고, 파트너는 한 달 뒤에야 반영할 수 있으며, 오래 떠 있는 배치 작업은 예전 SDK를 그대로 쓸 수 있습니다. 그래서 응답 스키마 변경은 코드 변경이 아니라 <strong>클라이언트 생태계와 맺은 계약 변경</strong>으로 봐야 합니다.</p>
<p>실무에서 사고는 대개 큰 버전 변경보다 작은 필드 변경에서 납니다. &ldquo;필드 하나 추가는 하위 호환이니까 괜찮다&quot;며 user profile 응답에 계산 필드를 붙였는데 DB join이 늘어 p95가 300ms에서 900ms로 튀거나, enum 값을 하나 추가했더니 오래된 앱의 strict parser가 예외를 내는 식입니다. 반대로 &ldquo;안 쓰는 필드&quot;라고 삭제했는데 파트너 정산 배치가 그 필드를 key로 쓰고 있던 경우도 흔합니다.</p>
<p>이 글은 <a href="/learning/deep-dive/deep-dive-api-versioning/">API Versioning</a>, <a href="/learning/deep-dive/deep-dive-consumer-driven-contract-testing/">Consumer Driven Contract Testing</a>, <a href="/learning/deep-dive/deep-dive-response-payload-budget-field-projection-playbook/">Response Payload Budget</a>, <a href="/learning/deep-dive/deep-dive-api-deprecation-sunset-playbook/">API Deprecation/Sunset</a>과 이어집니다. 목표는 &ldquo;버전을 어떻게 붙일까&quot;보다 한 단계 구체적입니다. <strong>필드 단위 변경을 어떤 숫자와 조건으로 허용하고, 어떤 변경은 이행 기간 없이는 막을 것인가</strong>를 정합니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>응답 필드 추가·삭제·이름 변경·nullable 변경·enum 확장이 각각 어떤 호환성 위험을 만드는지 구분합니다.</li>
<li>OpenAPI diff, consumer contract test, production usage telemetry, payload budget을 배포 전 gate로 묶는 방법을 배웁니다.</li>
<li>public/mobile/partner/internal API별로 deprecation window와 승인 기준을 다르게 잡을 수 있습니다.</li>
<li>&ldquo;하위 호환&quot;처럼 보이는 변경도 성능 호환성과 운영 관측성까지 확인하는 습관을 가져갑니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-응답-스키마-호환성은-문법-호환성과-의미-호환성으로-나뉜다">1) 응답 스키마 호환성은 문법 호환성과 의미 호환성으로 나뉜다</h3>
<p>JSON 문법만 보면 필드 추가는 보통 안전합니다. 오래된 클라이언트는 모르는 필드를 무시하면 됩니다. 하지만 실제 호환성은 더 넓습니다.</p>
<table>
  <thead>
      <tr>
          <th>변경 유형</th>
          <th>문법 호환성</th>
          <th>의미 호환성</th>
          <th>주의점</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>optional 필드 추가</td>
          <td>대체로 안전</td>
          <td>조건부 안전</td>
          <td>계산 비용과 payload 증가 확인</td>
      </tr>
      <tr>
          <td>required 필드 추가</td>
          <td>클라이언트 입력에는 위험, 응답에는 조건부</td>
          <td>조건부</td>
          <td>SDK model 생성 방식에 따라 깨질 수 있음</td>
      </tr>
      <tr>
          <td>필드 삭제</td>
          <td>위험</td>
          <td>위험</td>
          <td>미사용 증거와 deprecation 필요</td>
      </tr>
      <tr>
          <td>필드 이름 변경</td>
          <td>위험</td>
          <td>위험</td>
          <td>새 필드 추가 후 dual-read 기간 필요</td>
      </tr>
      <tr>
          <td>nullable -&gt; non-null</td>
          <td>조건부</td>
          <td>위험 가능</td>
          <td>null 처리하던 client 로직 영향</td>
      </tr>
      <tr>
          <td>non-null -&gt; nullable</td>
          <td>문법상 가능</td>
          <td>위험</td>
          <td>기존 client가 null을 예상하지 못할 수 있음</td>
      </tr>
      <tr>
          <td>enum 값 추가</td>
          <td>조건부</td>
          <td>위험 가능</td>
          <td>strict switch/case에서 예외 가능</td>
      </tr>
      <tr>
          <td>필드 의미 변경</td>
          <td>문법상 안전</td>
          <td>매우 위험</td>
          <td>가장 탐지하기 어려운 breaking change</td>
      </tr>
  </tbody>
</table>
<p>가장 무서운 변경은 문법상 안전하지만 의미가 바뀌는 경우입니다. 예를 들어 <code>status: &quot;READY&quot;</code>가 예전에는 &ldquo;다운로드 가능&quot;이었는데 새 버전에서 &ldquo;생성 완료, 보안 스캔 대기&quot;라는 뜻으로 바뀌면 JSON schema는 그대로입니다. 하지만 클라이언트 행동은 깨집니다. 따라서 response compatibility contract에는 타입뿐 아니라 의미, 단위, 시간 기준, 정렬 기준, 상태 전이 규칙도 포함해야 합니다.</p>
<h3 id="2-필드-추가도-성능-호환성을-깨뜨릴-수-있다">2) 필드 추가도 성능 호환성을 깨뜨릴 수 있다</h3>
<p>응답 필드 추가는 가장 흔한 변경입니다. 하지만 새 필드가 어디서 오는지 확인하지 않으면 성능 사고가 됩니다.</p>
<p>예를 들어 <code>GET /orders/{id}</code>에 <code>latest_payment_attempt</code>를 추가한다고 합시다. 구현이 단순 조회라면 괜찮습니다. 그런데 payment table join, provider 상태 조회, JSON aggregation, 권한 필터, 캐시 miss가 붙으면 기존 API의 성격이 바뀝니다. 사용자는 &ldquo;주문 상세&quot;를 요청했을 뿐인데 서버는 결제 시도 전체를 뒤지고 있을 수 있습니다.</p>
<p>초기 예산 기준은 아래처럼 둘 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>권장 gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>응답 payload 증가</td>
          <td>기존 p50 size 대비 20% 이하, public/mobile API는 10% 이하</td>
      </tr>
      <tr>
          <td>API p95 latency delta</td>
          <td>10% 또는 50ms 중 작은 값 이하</td>
      </tr>
      <tr>
          <td>DB query count</td>
          <td>endpoint당 추가 쿼리 1개 이하, N+1 금지</td>
      </tr>
      <tr>
          <td>캐시 value 증가</td>
          <td>30% 초과 시 별도 cache key 또는 projection 검토</td>
      </tr>
      <tr>
          <td>모바일 파싱 시간</td>
          <td>저사양 기준 16ms frame budget 영향 여부 확인</td>
      </tr>
      <tr>
          <td>새 필드 계산 실패</td>
          <td>전체 응답 실패로 전파하지 않고 field unavailable 정책 검토</td>
      </tr>
  </tbody>
</table>
<p>이 기준은 <a href="/learning/deep-dive/deep-dive-response-payload-budget-field-projection-playbook/">Response Payload Budget</a>과 연결됩니다. 필드 하나가 비싸면 API 버전을 올리기보다 projection, expansion parameter, 별도 endpoint, 비동기 operation resource를 검토하는 편이 낫습니다.</p>
<h3 id="3-enum-추가는-생각보다-자주-깨진다">3) enum 추가는 생각보다 자주 깨진다</h3>
<p>서버 개발자는 enum 추가를 하위 호환으로 보는 경우가 많습니다. 기존 값은 그대로 있고 새 값만 생기기 때문입니다. 하지만 클라이언트가 아래처럼 작성되어 있으면 이야기가 달라집니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#ff79c6">when</span> (order.status) {
</span></span><span style="display:flex;"><span>    <span style="color:#f1fa8c">&#34;PENDING&#34;</span> <span style="color:#ff79c6">-&gt;</span> showPending()
</span></span><span style="display:flex;"><span>    <span style="color:#f1fa8c">&#34;PAID&#34;</span> <span style="color:#ff79c6">-&gt;</span> showPaid()
</span></span><span style="display:flex;"><span>    <span style="color:#f1fa8c">&#34;CANCELLED&#34;</span> <span style="color:#ff79c6">-&gt;</span> showCancelled()
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">else</span> <span style="color:#ff79c6">-&gt;</span> <span style="color:#ff79c6">throw</span> IllegalStateException(<span style="color:#f1fa8c">&#34;unknown status&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>새로운 <code>REFUND_PENDING</code>이 내려오면 앱이 크래시할 수 있습니다. TypeScript, Kotlin, Swift, Java SDK가 OpenAPI enum을 sealed type처럼 생성하는 경우도 있습니다. 이때 새 enum 값은 응답 스키마에서 &ldquo;추가&quot;지만 실제 런타임에서는 breaking change입니다.</p>
<p>권장 기준:</p>
<ul>
<li>public API enum에는 <code>UNKNOWN</code> 또는 fallback 처리 가이드를 문서화한다.</li>
<li>새 enum 값은 최소 1개 릴리스 전에 문서와 SDK에 먼저 노출한다.</li>
<li>모바일 앱은 새 enum을 서버에서 바로 내려주기 전에 minimum supported version을 확인한다.</li>
<li>상태 전이 enum은 <a href="/learning/deep-dive/deep-dive-operational-state-machine-design/">운영용 상태 머신 설계</a>처럼 허용 전이를 함께 문서화한다.</li>
<li>새 enum이 사용자 행동을 바꾸면 단순 스키마 변경이 아니라 product behavior 변경으로 리뷰한다.</li>
</ul>
<h3 id="4-필드-삭제는-로그에-안-보인다만으로-결정하면-안-된다">4) 필드 삭제는 &ldquo;로그에 안 보인다&quot;만으로 결정하면 안 된다</h3>
<p>서버 access log만 보고 필드 사용 여부를 알기는 어렵습니다. HTTP 요청에는 클라이언트가 어떤 응답 필드를 읽었는지가 보통 남지 않습니다. GraphQL처럼 selection set이 있거나 field-level telemetry를 심은 경우가 아니라면 서버는 응답을 보냈다는 사실만 알 뿐, client가 어느 필드를 사용했는지 모릅니다.</p>
<p>삭제 판단에는 여러 증거가 필요합니다.</p>
<ul>
<li>SDK와 주요 client repo 검색</li>
<li>OpenAPI generated model usage 검색</li>
<li>partner integration 문서 확인</li>
<li>response field-level logging 또는 structured client telemetry</li>
<li>deprecated field를 제거한 canary 응답에서 client error 증가 여부</li>
<li>고객 지원 ticket, export/report 스크립트, BI 수집기 확인</li>
</ul>
<p>필드 삭제 gate는 보수적으로 잡는 편이 좋습니다.</p>
<table>
  <thead>
      <tr>
          <th>API 유형</th>
          <th>삭제 전 최소 조건</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>내부 실험 API</td>
          <td>owner 승인, contract test 통과, 7일 usage 확인</td>
      </tr>
      <tr>
          <td>사내 서비스 API</td>
          <td>consumer owner 승인, 14일 deprecation 공지</td>
      </tr>
      <tr>
          <td>모바일 API</td>
          <td>최소 2개 앱 릴리스 또는 30~60일 window</td>
      </tr>
      <tr>
          <td>파트너/public API</td>
          <td>90일 이상 window, migration guide, sunset header</td>
      </tr>
      <tr>
          <td>결제/정산/인증 API</td>
          <td>explicit consumer sign-off, rollback plan 필수</td>
      </tr>
  </tbody>
</table>
<p>이 기준은 <a href="/learning/deep-dive/deep-dive-api-deprecation-sunset-playbook/">API Deprecation/Sunset</a>의 핵심과 같습니다. 삭제는 코드 정리가 아니라 운영 전환입니다.</p>
<h3 id="5-openapi는-source-of-truth가-될-수-있지만-production-truth는-아니다">5) OpenAPI는 source of truth가 될 수 있지만 production truth는 아니다</h3>
<p>OpenAPI 문서가 최신이면 큰 도움이 됩니다. schema diff로 breaking change를 탐지하고, SDK를 생성하고, mock server와 contract test를 붙일 수 있습니다. 하지만 OpenAPI가 항상 production truth를 보장하지는 않습니다.</p>
<p>실무에서 흔한 차이:</p>
<ul>
<li>문서에는 optional인데 실제 응답에서는 항상 내려온다.</li>
<li>문서에는 string인데 일부 데이터에서 number-like string을 내려준다.</li>
<li>enum 문서는 업데이트됐지만 구버전 서버가 섞여 있다.</li>
<li>nullable 문서와 실제 null 빈도가 다르다.</li>
<li>feature flag에 따라 필드가 조건부로 내려온다.</li>
<li>에러 응답 schema가 성공 응답보다 덜 관리된다.</li>
</ul>
<p>따라서 스키마 관리는 세 층으로 봐야 합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">response_contract_sources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">design_contract</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">source</span>: <span style="color:#f1fa8c">&#34;OpenAPI&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">role</span>: <span style="color:#f1fa8c">&#34;의도한 공개 계약&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">consumer_contract</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">source</span>: <span style="color:#f1fa8c">&#34;Pact 또는 consumer test&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">role</span>: <span style="color:#f1fa8c">&#34;실제 consumer가 의존하는 행동&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">production_observation</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">source</span>: <span style="color:#f1fa8c">&#34;field telemetry, sampled response validation&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">role</span>: <span style="color:#f1fa8c">&#34;운영 데이터에서 실제로 나가는 값&#34;</span>
</span></span></code></pre></div><p>OpenAPI diff만 통과했다고 끝내면 부족합니다. consumer contract test와 production sampled response validation을 같이 둬야 합니다. <a href="/learning/deep-dive/deep-dive-consumer-driven-contract-testing/">Consumer Driven Contract Testing</a>은 이 간극을 줄이는 실전 도구입니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-필드-단위-response-catalog를-만든다">1) 필드 단위 response catalog를 만든다</h3>
<p>모든 API를 한 번에 정리하려 하지 말고, 변경 비용이 큰 API부터 시작합니다. 결제, 주문, 인증, 파일 다운로드, 파트너 정산, 모바일 홈 화면 API가 우선입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">response_field_catalog</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">endpoint</span>: <span style="color:#f1fa8c">&#34;GET /v1/orders/{orderId}&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">field</span>: <span style="color:#f1fa8c">&#34;status&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">type</span>: <span style="color:#f1fa8c">&#34;enum&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">stability</span>: <span style="color:#f1fa8c">&#34;stable&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">nullable</span>: <span style="color:#ff79c6">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">owner</span>: <span style="color:#f1fa8c">&#34;commerce-platform&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">consumers</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;ios-app&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;android-app&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;partner-settlement-batch&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">enum_values</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;PENDING&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;PAID&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;CANCELLED&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">deprecation_status</span>: <span style="color:#f1fa8c">&#34;active&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">compatibility_notes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;새 enum 값은 앱 minimum version 확인 후 노출&#34;</span>
</span></span></code></pre></div><p>catalog의 목적은 문서 장식이 아닙니다. PR에서 응답 변경이 들어올 때 &ldquo;이 필드는 stable인가&rdquo;, &ldquo;누가 읽고 있나&rdquo;, &ldquo;삭제하려면 어떤 window가 필요한가&quot;를 바로 판단하기 위한 운영 테이블입니다.</p>
<h3 id="2-pr에-response-schema-diff-gate를-붙인다">2) PR에 response schema diff gate를 붙인다</h3>
<p>응답 DTO, OpenAPI spec, serialization 설정이 바뀌면 diff를 생성합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">api_schema_change_gate</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">detect</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;openapi diff&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;dto public field diff&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;enum value diff&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;nullable annotation diff&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">block_if</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;field_removed_without_deprecation&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;enum_removed_or_renamed&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;required_field_added_to_public_contract&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;nullable_changed_without_consumer_test&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">warn_if</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;optional_field_added&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;enum_value_added&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f1fa8c">&#34;payload_size_budget_unknown&#34;</span>
</span></span></code></pre></div><p>block과 warn을 나누는 것이 중요합니다. 모든 변경을 막으면 팀은 gate를 우회합니다. optional 필드 추가는 경고와 성능 예산 확인으로 충분할 수 있습니다. 반면 필드 삭제와 이름 변경은 deprecation 증거가 없으면 막아야 합니다.</p>
<h3 id="3-pr-설명에는-변경-유형과-증거를-같이-남긴다">3) PR 설명에는 변경 유형과 증거를 같이 남긴다</h3>
<p>호환성 사고는 코드 diff만 봐서는 잘 보이지 않습니다. DTO에 필드 하나가 추가됐는지보다 그 필드가 어떤 consumer에게 어떤 비용을 만드는지가 중요합니다. 그래서 response contract가 바뀌는 PR에는 아래처럼 짧은 변경 기록을 붙이는 편이 좋습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span><span style="font-weight:bold">## Response Contract Change
</span></span></span><span style="display:flex;"><span><span style="font-weight:bold"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Endpoint: <span style="color:#f1fa8c">`GET /v1/orders/{orderId}`</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Change type: optional field added
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Field: <span style="color:#f1fa8c">`latest_payment_attempt`</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Consumers checked: web-order-detail, ios-app, partner-settlement-batch
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Compatibility verdict: non-breaking with performance gate
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Payload delta: p50 +3.8%, p95 +4.6%
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Latency delta: p95 +18ms, p99 +31ms
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Query delta: +0 on cache hit, +1 on cache miss
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Rollout: feature flag <span style="color:#f1fa8c">`orders.latest_payment_attempt_response`</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Rollback: disable flag, keep field absent
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Follow-up: add field-level usage telemetry for 14 days
</span></span></code></pre></div><p>반대로 필드 삭제나 이름 변경이라면 기록이 더 길어져야 합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span><span style="font-weight:bold">## Response Contract Change
</span></span></span><span style="display:flex;"><span><span style="font-weight:bold"></span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Endpoint: <span style="color:#f1fa8c">`GET /v1/users/{userId}`</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Change type: field rename with dual-field window
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Old field: <span style="color:#f1fa8c">`created_at`</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> New field: <span style="color:#f1fa8c">`createdAt`</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Deprecated date: 2026-07-28
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Sunset target: 2026-09-30
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Known consumers: web-profile, android-app, crm-export
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Migration evidence: web-profile merged, android min version pending, crm-export owner approved
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Block removal until: 30-day old-field usage is 0 and android min version reaches 9.4.0
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">-</span> Rollback: continue emitting both fields
</span></span></code></pre></div><p>이 템플릿의 목적은 문서 양식을 늘리는 것이 아닙니다. 리뷰어가 &ldquo;이 변경이 깨지는가&quot;를 한눈에 판단하게 만드는 것입니다. 특히 payload delta, latency delta, consumer checked, rollback이 빠져 있으면 optional 필드 추가도 안전하다고 보기 어렵습니다.</p>
<p>운영팀이 보는 release note도 같은 언어를 써야 합니다.</p>
<table>
  <thead>
      <tr>
          <th>릴리스 노트 항목</th>
          <th>이유</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>endpoint와 field</td>
          <td>장애가 났을 때 영향 범위를 바로 좁힌다</td>
      </tr>
      <tr>
          <td>change type</td>
          <td>추가, 삭제, 이름 변경, 의미 변경을 혼동하지 않는다</td>
      </tr>
      <tr>
          <td>consumer impact</td>
          <td>모바일, 파트너, 배치처럼 느린 consumer를 놓치지 않는다</td>
      </tr>
      <tr>
          <td>observability</td>
          <td>어떤 metric으로 성공/실패를 볼지 정한다</td>
      </tr>
      <tr>
          <td>rollback</td>
          <td>새 필드 미노출, dual-field 유지, route-back 중 어떤 방식인지 고정한다</td>
      </tr>
  </tbody>
</table>
<p>작은 팀이라면 처음부터 자동화가 없어도 됩니다. PR 템플릿 한 블록으로 시작하고, 반복되는 항목만 나중에 OpenAPI diff bot이나 CI gate로 옮기면 됩니다.</p>
<h3 id="4-consumer별-호환성-등급을-둔다">4) consumer별 호환성 등급을 둔다</h3>
<p>모든 client를 같은 수준으로 관리하면 현실성이 떨어집니다. consumer 통제권에 따라 기준을 다르게 둡니다.</p>
<table>
  <thead>
      <tr>
          <th>Consumer</th>
          <th>통제권</th>
          <th>변경 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>같은 repo 내부 client</td>
          <td>높음</td>
          <td>같은 PR에서 수정 가능</td>
      </tr>
      <tr>
          <td>사내 microservice</td>
          <td>중간</td>
          <td>owner 승인과 contract test</td>
      </tr>
      <tr>
          <td>웹 프론트엔드</td>
          <td>중간</td>
          <td>배포 순서와 rollback 확인</td>
      </tr>
      <tr>
          <td>모바일 앱</td>
          <td>낮음</td>
          <td>minimum version과 앱 릴리스 window</td>
      </tr>
      <tr>
          <td>파트너 API</td>
          <td>매우 낮음</td>
          <td>사전 공지, migration guide, sandbox</td>
      </tr>
      <tr>
          <td>BI/export 수집기</td>
          <td>낮음</td>
          <td>field usage 확인과 샘플 데이터 검증</td>
      </tr>
  </tbody>
</table>
<p>이 분류가 있으면 의사결정이 빨라집니다. 내부 API에서 7일 window면 충분한 변경도 public API에서는 90일이 필요할 수 있습니다.</p>
<h3 id="5-dual-field-전략으로-이름-변경을-처리한다">5) dual-field 전략으로 이름 변경을 처리한다</h3>
<p>필드 이름 변경은 삭제와 추가를 동시에 하는 변경입니다. 바로 바꾸지 말고 dual-field 기간을 둡니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;created_at&#34;</span>: <span style="color:#f1fa8c">&#34;2026-07-28T10:06:00+09:00&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;createdAt&#34;</span>: <span style="color:#f1fa8c">&#34;2026-07-28T10:06:00+09:00&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>운영 순서:</p>
<ol>
<li>새 필드를 추가하고 기존 필드를 유지한다.</li>
<li>문서와 SDK에는 새 필드를 권장으로 표시한다.</li>
<li>기존 필드에 <code>deprecated: true</code>와 sunset date를 붙인다.</li>
<li>field usage telemetry 또는 consumer sign-off를 수집한다.</li>
<li>window가 끝난 뒤 기존 필드를 제거한다.</li>
</ol>
<p>dual-field는 payload를 잠시 늘립니다. 그래서 response size budget이 필요합니다. 이름 변경이 많으면 새 API version이 더 낫습니다. 작은 이름 정리 때문에 여러 달 동안 응답이 지저분해지는 비용도 계산해야 합니다.</p>
<h3 id="6-production-sampled-validation을-돌린다">6) production sampled validation을 돌린다</h3>
<p>테스트 환경에서는 놓치는 데이터가 많습니다. 운영 데이터에서 실제 응답 샘플을 schema에 검증하는 방식이 도움이 됩니다.</p>
<p>권장 기준:</p>
<ul>
<li>핵심 endpoint는 0.1~1% 샘플링으로 response schema validation</li>
<li>validation 실패는 사용자 응답 실패로 만들지 말고 별도 metric으로 기록</li>
<li>필드별 null 빈도, enum unknown 빈도, payload size p95/p99를 수집</li>
<li>새 필드 rollout은 1%, 10%, 50%, 100% 단계로 관측</li>
<li>p95 latency 10% 이상 악화 또는 schema violation 0.1% 초과 시 rollout 중단</li>
</ul>
<p>이 방식은 <a href="/learning/deep-dive/deep-dive-shadow-traffic-dark-launch-playbook/">Shadow Traffic/Dark Launch</a>와도 잘 맞습니다. 실제 트래픽 분포에서 새 응답 모양이 안전한지 먼저 볼 수 있습니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, 호환성 gate는 개발 속도를 늦출 수 있습니다. 특히 내부 API가 빠르게 변하는 초기 제품에서는 과한 gate가 부담입니다. 그래서 public/mobile/partner/critical API부터 강하게 적용하고, 내부 실험 API는 경량 diff와 owner 승인 정도로 시작하는 편이 좋습니다.</p>
<p>둘째, 스키마 호환성과 성능 호환성은 다릅니다. optional 필드 추가는 schema 관점에서 안전해도 latency, DB 부하, cache hit rate를 망칠 수 있습니다. API 변경 리뷰에는 schema diff와 함께 p95 latency, query count, payload size delta가 있어야 합니다.</p>
<p>셋째, consumer contract test는 모든 consumer 행동을 잡지 못합니다. 테스트에 없는 필드 의존, BI 수집기, 파트너의 임시 스크립트는 여전히 빠질 수 있습니다. 그래서 contract test와 usage telemetry, deprecation 공지를 같이 써야 합니다.</p>
<p>넷째, field-level telemetry는 개인정보와 비용 이슈를 만듭니다. &ldquo;누가 어떤 필드를 읽었는가&quot;를 과하게 수집하면 민감할 수 있습니다. 원문 값보다 field name, client id, version, presence 여부 중심으로 최소 수집하는 편이 안전합니다.</p>
<p>다섯째, 버전 추가는 만능 해결책이 아닙니다. <code>/v2</code>를 열면 기존 <code>/v1</code>도 운영해야 합니다. 문서, SDK, 모니터링, 알림, 보안 패치가 두 배가 됩니다. 작은 변경은 dual-field와 deprecation으로 처리하고, 의미가 크게 바뀌거나 consumer 행동이 달라질 때만 새 버전을 검토합니다.</p>
<p>의사결정 우선순위는 <strong>데이터/금전/권한 영향 &gt; consumer 통제권 &gt; 배포 주기 &gt; 성능 예산 &gt; 코드 정리 욕구</strong>입니다. 오래된 필드를 지우고 싶은 마음보다, 그 필드를 읽는 쪽의 실패 비용이 먼저입니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> public/mobile/partner API의 응답 필드 catalog가 있다.</li>
<li><input disabled="" type="checkbox"> OpenAPI diff에서 필드 삭제, required 추가, enum 삭제, nullable 변경을 탐지한다.</li>
<li><input disabled="" type="checkbox"> optional 필드 추가에도 payload size와 p95 latency delta를 본다.</li>
<li><input disabled="" type="checkbox"> enum 값 추가 전 strict parser를 가진 consumer가 있는지 확인한다.</li>
<li><input disabled="" type="checkbox"> 필드 삭제 전 deprecation window, usage 증거, consumer 승인, rollback plan이 있다.</li>
<li><input disabled="" type="checkbox"> 이름 변경은 dual-field 기간과 sunset date를 가진다.</li>
<li><input disabled="" type="checkbox"> production sampled response validation으로 실제 null/enum/payload 분포를 본다.</li>
<li><input disabled="" type="checkbox"> schema 변경은 <a href="/learning/deep-dive/deep-dive-consumer-driven-contract-testing/">Consumer Driven Contract Testing</a>과 연결돼 있다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>현재 운영 중인 API 하나를 고르고, 응답 필드 10개에 대해 <code>stable</code>, <code>experimental</code>, <code>deprecated</code> 중 하나를 붙여보세요.</li>
<li>최근 30일 동안 추가된 응답 필드가 payload size와 latency에 어떤 영향을 줬는지 p50/p95로 비교해보세요.</li>
<li>enum 필드 하나를 골라 새 값이 추가됐을 때 각 client가 어떻게 동작하는지 테스트를 작성해보세요.</li>
<li>삭제하고 싶은 필드 하나를 정하고, 30일 deprecation plan과 consumer 공지 문구를 만들어보세요.</li>
</ol>
<h2 id="다음에-같이-보면-좋은-글">다음에 같이 보면 좋은 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-api-versioning/">API Versioning</a></li>
<li><a href="/learning/deep-dive/deep-dive-consumer-driven-contract-testing/">Consumer Driven Contract Testing</a></li>
<li><a href="/learning/deep-dive/deep-dive-response-payload-budget-field-projection-playbook/">Response Payload Budget과 Field Projection</a></li>
<li><a href="/learning/deep-dive/deep-dive-api-deprecation-sunset-playbook/">API Deprecation/Sunset 운영 플레이북</a></li>
<li><a href="/learning/deep-dive/deep-dive-api-error-semantics-retryability-contract/">API Error Semantics</a></li>
</ul>
]]></content:encoded></item></channel></rss>