<?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>Field Projection on jyukki's Blog</title><link>https://jyukki.com/tags/field-projection/</link><description>Recent content in Field Projection on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/field-projection/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Response Payload Budget, 큰 응답을 안전하게 내려주는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-response-payload-budget-field-projection-playbook/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-response-payload-budget-field-projection-playbook/</guid><description>목록·상세·대시보드 API에서 응답 크기, 필드 선택, 페이지네이션, export 전환 기준을 숫자 중심으로 정리합니다.</description><content:encoded><![CDATA[<p>큰 요청 본문을 제한하는 팀은 많지만, 큰 응답을 제한하는 팀은 의외로 적습니다. <code>GET /orders</code>가 처음에는 20개 주문의 요약만 내려주다가, 나중에는 결제 정보, 배송 상태, 쿠폰, 리뷰 가능 여부, 추천 액션까지 붙습니다. 화면은 편해지지만 응답은 조용히 커지고, 어느 날 모바일 앱에서 스크롤이 끊기고 API p95가 1초를 넘고 CDN 비용이 늘어납니다. 서버 로그에는 에러가 없습니다. 단지 <strong>너무 많이 내려주는 계약</strong>이 기본값이 된 것입니다.</p>
<p>응답 payload는 네트워크 바이트만의 문제가 아닙니다. DB에서 더 많은 row와 column을 읽고, 애플리케이션이 더 큰 객체 그래프를 만들고, JSON serializer가 CPU를 쓰고, 캐시는 더 큰 값을 저장하며, 클라이언트는 그 값을 다시 파싱하고 렌더링합니다. 특히 관리자 검색, 대시보드, 피드, 주문 목록처럼 자주 보는 API에서 응답 크기 관리를 놓치면 작은 기능 추가가 누적되어 성능 사고로 바뀝니다.</p>
<p>이 글은 <a href="/learning/deep-dive/deep-dive-api-resource-budgeting/">API Resource Budgeting</a>, <a href="/learning/deep-dive/deep-dive-cursor-pagination-consistency-playbook/">Cursor Pagination Consistency</a>, <a href="/learning/deep-dive/deep-dive-large-data-export-pipeline-playbook/">Large Data Export Pipeline</a>, <a href="/learning/deep-dive/deep-dive-http-caching-etag-revalidation-playbook/">HTTP Caching과 ETag</a>와 이어집니다. 핵심은 &ldquo;빠르게 직렬화하기&quot;가 아니라, <strong>어떤 필드를 지금 내려줄 자격이 있는지, 어디서부터 나눌지, 언제 파일 생성으로 넘길지</strong>를 계약으로 만드는 것입니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>응답 크기가 DB, 애플리케이션 CPU, 캐시, 네트워크, 클라이언트 렌더링에 어떤 비용을 만드는지 이해합니다.</li>
<li>목록·상세·대시보드 API에서 page size, field projection, partial response 기준을 숫자로 잡을 수 있습니다.</li>
<li>gzip, pagination, read model, async export를 어떤 순서로 검토할지 의사결정 기준을 가져갑니다.</li>
<li>큰 응답에서 관측해야 할 지표와 PR 리뷰 체크리스트를 정리합니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-큰-응답은-서버-바깥에서도-비용을-만든다">1) 큰 응답은 서버 바깥에서도 비용을 만든다</h3>
<p>응답 payload가 커지면 서버의 outbound traffic만 증가한다고 생각하기 쉽습니다. 실제 비용은 더 넓습니다.</p>
<table>
  <thead>
      <tr>
          <th>구간</th>
          <th>커지는 비용</th>
          <th>자주 보이는 증상</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB</td>
          <td>더 많은 row/column, join, sort</td>
          <td><code>rows_examined</code>, temp table 증가</td>
      </tr>
      <tr>
          <td>App</td>
          <td>객체 생성, 직렬화 CPU, heap</td>
          <td>GC 증가, p95 지연 상승</td>
      </tr>
      <tr>
          <td>Cache</td>
          <td>key별 value 크기, eviction</td>
          <td>hit ratio 하락, Redis memory 증가</td>
      </tr>
      <tr>
          <td>Network</td>
          <td>전송 시간, CDN 비용</td>
          <td>모바일 지연, client abort</td>
      </tr>
      <tr>
          <td>Client</td>
          <td>JSON parse, 렌더링, 메모리</td>
          <td>스크롤 끊김, 저사양 기기 crash</td>
      </tr>
  </tbody>
</table>
<p>그래서 응답 크기 예산은 API 성능 예산의 일부로 봐야 합니다. <a href="/learning/deep-dive/deep-dive-request-body-guardrail-streaming-playbook/">Request Body Guardrail</a>이 입구를 지키는 장치라면, Response Payload Budget은 출구를 지키는 장치입니다.</p>
<p>초기 기준은 아래처럼 둘 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>API 유형</th>
          <th>권장 시작 예산</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>모바일 목록</td>
          <td>page size 20~50, p95 payload 200KB 이하</td>
      </tr>
      <tr>
          <td>웹 목록</td>
          <td>page size 50~100, p95 payload 500KB 이하</td>
      </tr>
      <tr>
          <td>상세 화면</td>
          <td>p95 payload 300KB 이하, optional section lazy load</td>
      </tr>
      <tr>
          <td>관리자 검색</td>
          <td>page size 100 이하, 1,000행 초과는 export 유도</td>
      </tr>
      <tr>
          <td>대시보드</td>
          <td>카드별 부분 조회, 전체 payload 1MB 이하</td>
      </tr>
      <tr>
          <td>파일/리포트</td>
          <td>10MB 이상 또는 5초 이상이면 async export</td>
      </tr>
  </tbody>
</table>
<p>숫자는 서비스마다 달라질 수 있습니다. 중요한 것은 &ldquo;응답이 클 수도 있음&quot;을 방치하지 않고, endpoint마다 설명 가능한 예산을 둔다는 점입니다.</p>
<h3 id="2-필드-추가는-하위-호환처럼-보이지만-성능-호환성을-깨뜨릴-수-있다">2) 필드 추가는 하위 호환처럼 보이지만 성능 호환성을 깨뜨릴 수 있다</h3>
<p>REST나 JSON API에서 필드 추가는 보통 non-breaking change로 분류됩니다. 기존 클라이언트가 모르는 필드를 무시할 수 있기 때문입니다. 하지만 성능 관점에서는 이야기가 다릅니다. 새 필드 하나가 추가 join을 만들고, N+1 조회를 유발하고, 캐시 value를 2배 키우고, 모바일 앱의 파싱 시간을 늘릴 수 있습니다.</p>
<p>특히 위험한 필드는 아래입니다.</p>
<ul>
<li>이미지 목록, 큰 HTML/Markdown 본문, base64 인라인 데이터</li>
<li>권한 계산 결과처럼 사용자별로 달라져 캐시를 깨는 필드</li>
<li>외부 API 호출이나 별도 microservice fan-out이 필요한 필드</li>
<li>배열 안에 또 다른 배열을 넣는 nested collection</li>
<li>관리자 화면에서만 쓰는 진단 필드를 일반 사용자 API에 섞는 경우</li>
</ul>
<p>필드 추가 PR에서는 최소 세 가지를 확인해야 합니다. 첫째, 이 필드가 기본 응답에 꼭 필요한가. 둘째, 필드를 계산하는 비용이 p95에서 얼마인가. 셋째, 클라이언트가 lazy load나 별도 endpoint로 받을 수 있는가. 이 질문 없이 &ldquo;필드 추가는 안전하다&quot;고 보면 응답 계약이 계속 비대해집니다.</p>
<h3 id="3-field-projection은-만능이-아니라-계약-축소-도구다">3) Field projection은 만능이 아니라 계약 축소 도구다</h3>
<p><code>fields=id,title,status</code> 같은 field projection은 유용하지만 잘못 쓰면 캐시 키 폭발과 권한 버그를 만듭니다. 모든 필드를 마음대로 조합하게 하면 API 제공자는 조합 수만큼 테스트해야 하고, 캐시는 <code>fields</code> 조합마다 갈라집니다. 그래서 실무에서는 자유 조합보다 <strong>named projection</strong>이 더 안정적입니다.</p>
<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>GET /orders?view=summary
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">views</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">summary</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">fields</span>: [id, status, total_price, created_at]
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">max_payload_p95</span>: 200KB
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">detail</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">fields</span>: [id, status, items, payment, shipment, available_actions]
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">max_payload_p95</span>: 500KB
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">admin_audit</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">fields</span>: [id, status, payment, shipment, risk_flags, audit_refs]
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">auth</span>: order.audit.read
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">max_payload_p95</span>: 1MB
</span></span></code></pre></div><p>이렇게 하면 클라이언트는 필요한 형태를 고를 수 있고, 서버는 각 projection의 비용과 권한을 테스트할 수 있습니다. 필드 단위 자유 선택이 필요하다면 allowlist를 두고, nested collection과 민감 필드는 별도 projection으로 분리하는 편이 좋습니다.</p>
<h3 id="4-pagination은-성능-기능이-아니라-응답-계약이다">4) Pagination은 성능 기능이 아니라 응답 계약이다</h3>
<p>큰 목록에서 pagination을 나중에 붙이면 클라이언트 계약을 바꾸기 어렵습니다. 처음부터 목록 API에는 page size 상한, 정렬 기준, 다음 페이지 토큰, snapshot/live 기준이 들어가야 합니다.</p>
<p>운영 기준:</p>
<ul>
<li>기본 page size: 20~50</li>
<li>최대 page size: 일반 사용자 100, 관리자 200 이하부터 시작</li>
<li>offset 10,000 초과 접근: 차단 또는 export 유도</li>
<li>cursor token invalid rate: 1% 초과 시 클라이언트 사용 방식 점검</li>
<li>목록 응답 p95 payload: 500KB 초과 시 projection 또는 page size 축소 검토</li>
</ul>
<p>정렬이 흔들리는 목록에서는 <a href="/learning/deep-dive/deep-dive-cursor-pagination-consistency-playbook/">Cursor Pagination Consistency</a> 기준이 필요합니다. 단순히 <code>limit</code>만 붙인다고 해결되지 않습니다. stable sort, tie-breaker, cursor token, 삭제/삽입 중복 처리까지 API 계약에 포함해야 합니다.</p>
<h3 id="5-export는-큰-다운로드-버튼이-아니라-별도-파이프라인이다">5) Export는 &ldquo;큰 다운로드 버튼&quot;이 아니라 별도 파이프라인이다</h3>
<p>관리자나 파트너가 &ldquo;전체 데이터를 한 번에 받고 싶다&quot;고 요구하면 동기 API에 <code>size=100000</code>을 열어주기 쉽습니다. 이 방식은 위험합니다. 사용자는 기다리다 timeout을 만나고, 브라우저나 gateway가 재시도하고, 서버는 같은 대형 쿼리를 여러 번 실행합니다.</p>
<p>아래 조건 중 하나라도 맞으면 export job으로 분리하는 편이 안전합니다.</p>
<ul>
<li>결과가 10MB 이상이다.</li>
<li>생성 시간이 p95 5초를 넘는다.</li>
<li>행 수가 10,000개 이상이다.</li>
<li>파일 공유, 감사, 재다운로드, 만료가 필요하다.</li>
<li>DB snapshot 기준이나 권한 검증 기록이 필요하다.</li>
</ul>
<p>이때 응답은 <code>202 Accepted</code>와 <code>operation_id</code>를 주고, 상태 조회 API와 완료 artifact URL을 제공합니다. 자세한 흐름은 <a href="/learning/deep-dive/deep-dive-large-data-export-pipeline-playbook/">Large Data Export Pipeline</a>과 <a href="/learning/deep-dive/deep-dive-async-request-reply-operation-resource-playbook/">Async Request-Reply Operation Resource</a>를 따르면 됩니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-endpoint별-response-budget-표를-만든다">1) Endpoint별 response budget 표를 만든다</h3>
<p>먼저 모든 API를 완벽히 고치려 하지 말고, 트래픽 상위 20개 endpoint부터 표를 만듭니다.</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">endpoint</span>: GET /orders
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">default_view</span>: summary
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">page_size_default</span>: <span style="color:#bd93f9">30</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">page_size_max</span>: <span style="color:#bd93f9">100</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">payload_budget</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">p95_bytes</span>: 300KB
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">p99_bytes</span>: 700KB
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">latency_budget</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">p95</span>: 400ms
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">p99</span>: 1s
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">expensive_fields</span>:
</span></span><span style="display:flex;"><span>  - items
</span></span><span style="display:flex;"><span>  - shipment_tracking
</span></span><span style="display:flex;"><span>  - available_actions
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">fallback</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">over_budget</span>: reduce_optional_sections
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">export_threshold_rows</span>: <span style="color:#bd93f9">10000</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">observability</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">labels</span>: [endpoint, view, client_type]
</span></span></code></pre></div><p>이 표가 있으면 새 필드를 추가할 때 &ldquo;응답이 커질 것 같다&quot;가 아니라 &ldquo;summary p95 예산을 넘는가&quot;로 리뷰할 수 있습니다.</p>
<h3 id="2-계측은-byte-row-time을-같이-본다">2) 계측은 byte, row, time을 같이 본다</h3>
<p>큰 응답 관측에는 최소 아래 지표가 필요합니다.</p>
<ul>
<li><code>response_size_bytes</code> p50/p95/p99</li>
<li><code>row_count</code> 또는 item count</li>
<li><code>serialization_duration_ms</code></li>
<li><code>db_rows_read</code>와 <code>db_query_count</code></li>
<li><code>cache_value_size_bytes</code></li>
<li><code>client_abort_count</code></li>
<li><code>export_suggested_count</code></li>
<li><code>projection_name</code> 또는 <code>view</code></li>
</ul>
<p>응답 크기만 보면 원인을 놓칩니다. 800KB 응답이어도 DB는 가볍고 이미지 URL 배열만 큰 경우와, 200KB 응답인데 외부 API 6개 fan-out이 붙은 경우는 조치가 다릅니다. 전자는 projection과 CDN, 후자는 <a href="/learning/deep-dive/deep-dive-api-composition-aggregation-playbook/">API Composition/Aggregation</a>이나 deadline budget 쪽을 봐야 합니다.</p>
<h3 id="3-pr-리뷰-기준을-짧게-고정한다">3) PR 리뷰 기준을 짧게 고정한다</h3>
<p>응답 필드 추가 PR에서는 아래 질문을 기본 템플릿으로 둡니다.</p>
<ol>
<li>기본 응답에 포함되어야 하는 필드인가, optional view로 충분한가?</li>
<li>해당 필드가 추가 query, 외부 호출, 권한 계산, 큰 문자열/배열을 만들지 않는가?</li>
<li>p95 response size와 serialization time 변화가 측정되었는가?</li>
<li>모바일/저사양 클라이언트에서 parse와 렌더링 비용을 감당할 수 있는가?</li>
<li>캐시 key 또는 CDN cacheability를 깨지 않는가?</li>
<li>1,000행 이상 또는 10MB 이상 결과는 export job으로 유도되는가?</li>
</ol>
<p>이 기준은 기능팀을 귀찮게 하려는 절차가 아니라, API가 조용히 무거워지는 것을 막는 최소 안전장치입니다.</p>
<h3 id="4-단계적-개선-순서">4) 단계적 개선 순서</h3>
<p>1단계는 관측입니다. 상위 endpoint의 response size와 serialization time을 수집합니다.</p>
<p>2단계는 page size 상한입니다. 무제한 목록과 큰 offset 접근을 막습니다.</p>
<p>3단계는 projection입니다. <code>summary</code>, <code>detail</code>, <code>admin</code>처럼 named view를 나눕니다.</p>
<p>4단계는 async export입니다. 큰 결과는 동기 JSON에서 빼고 operation resource와 artifact로 전환합니다.</p>
<p>5단계는 read model과 캐시입니다. 반복적으로 같은 큰 조합을 만들면 API마다 join하지 말고 별도 read model 또는 materialized view를 검토합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, projection은 API 표면을 늘립니다. view가 2~3개일 때는 좋지만, 10개를 넘으면 사실상 새 API 버전 관리 문제가 됩니다. view owner와 deprecation 기준을 같이 둬야 합니다.</p>
<p>둘째, page size를 너무 작게 줄이면 클라이언트 왕복이 늘어납니다. 모바일 목록에서 10개씩만 내려주면 payload는 작아도 화면 전환이 자주 끊길 수 있습니다. page size는 response byte, user scroll pattern, cache hit ratio를 같이 보고 정해야 합니다.</p>
<p>셋째, gzip이나 Brotli는 전송 비용을 줄이지만 서버 내부 비용을 없애지 않습니다. 직렬화 전 객체 생성, DB 조회, 권한 계산은 그대로 남습니다. 압축률이 높다는 이유로 큰 응답 계약을 방치하면 나중에 export나 projection으로 빼기가 더 어려워집니다.</p>
<p>넷째, export job은 운영 복잡도를 추가합니다. 상태 저장, 만료, 재다운로드, 권한 재검증, 파일 삭제 정책이 필요합니다. 하지만 큰 결과를 동기 API에 계속 얹는 것보다 장애 반경이 작고 사용자 경험도 예측 가능합니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 트래픽 상위 20개 API의 response size p95/p99를 알고 있다.</li>
<li><input disabled="" type="checkbox"> 목록 API에 page size 기본값과 최대값이 문서화되어 있다.</li>
<li><input disabled="" type="checkbox"> 기본 응답 필드와 optional projection이 구분되어 있다.</li>
<li><input disabled="" type="checkbox"> 10MB 이상 또는 5초 이상 결과는 export job으로 전환하는 기준이 있다.</li>
<li><input disabled="" type="checkbox"> 새 필드 추가 PR에서 serialization time과 cache 영향이 확인된다.</li>
<li><input disabled="" type="checkbox"> client abort, cursor invalid, export suggested 지표를 운영 대시보드에서 본다.</li>
</ul>
<h3 id="연습-과제">연습 과제</h3>
<ol>
<li>현재 서비스의 목록 API 하나를 골라 p95 response size, item count, serialization time을 측정해 보세요. 예산을 넘는다면 page size 축소와 projection 중 무엇이 먼저인지 판단합니다.</li>
<li>주문 상세 API를 <code>summary</code>, <code>detail</code>, <code>admin_audit</code> 세 projection으로 나누고 각 projection의 필드, 권한, p95 payload 예산을 적어 보세요.</li>
<li>관리자 검색에서 10,000행 결과가 필요한 상황을 가정하고, 동기 API 유지와 export job 전환을 latency, DB 부하, 사용자 경험, 감사 가능성 기준으로 비교해 보세요.</li>
</ol>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-api-resource-budgeting/">API Resource Budgeting</a></li>
<li><a href="/learning/deep-dive/deep-dive-cursor-pagination-consistency-playbook/">Cursor Pagination Consistency</a></li>
<li><a href="/learning/deep-dive/deep-dive-large-data-export-pipeline-playbook/">Large Data Export Pipeline</a></li>
<li><a href="/learning/deep-dive/deep-dive-request-body-guardrail-streaming-playbook/">Request Body Guardrail</a></li>
<li><a href="/learning/deep-dive/deep-dive-http-caching-etag-revalidation-playbook/">HTTP Caching과 ETag</a></li>
</ul>
]]></content:encoded></item></channel></rss>