<?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>Resource Budget on jyukki's Blog</title><link>https://jyukki.com/tags/resource-budget/</link><description>Recent content in Resource Budget on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Fri, 03 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/resource-budget/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: API Resource Budgeting, 요청 하나의 CPU·DB·외부 호출 비용을 설계하는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-api-resource-budgeting/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-api-resource-budgeting/</guid><description>API 요청을 단순 QPS가 아니라 CPU, DB 커넥션, 외부 API, 큐, 캐시 비용을 소비하는 작업 단위로 보고 예산을 설계하는 실무 기준을 정리합니다.</description><content:encoded><![CDATA[<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>API를 &ldquo;엔드포인트별 기능&quot;이 아니라 <strong>리소스를 소비하는 작업 단위</strong>로 바라보는 기준을 잡을 수 있습니다.</li>
<li>QPS, latency, DB query count, 외부 호출 수, response size를 묶어 요청 비용을 산정하는 방법을 배웁니다.</li>
<li>무거운 요청을 막을지, 줄일지, 비동기로 보낼지 결정하는 실무 기준을 가져갈 수 있습니다.</li>
<li>tenant별 공정성, SLO, 인프라 비용을 같은 표에서 의사결정하는 방식을 익힙니다.</li>
</ul>
<p>이 글은 <a href="/learning/deep-dive/deep-dive-capacity-planning-littles-law-saturation/">Capacity Planning과 Little&rsquo;s Law</a>, <a href="/learning/deep-dive/deep-dive-admission-control-concurrency-limits/">Admission Control과 Concurrency Limits</a>, <a href="/learning/deep-dive/deep-dive-api-rate-limit-backpressure/">API 레이트 리밋과 백프레셔</a>, <a href="/learning/deep-dive/deep-dive-tail-latency-engineering-playbook/">Tail Latency 엔지니어링 플레이북</a>과 함께 보면 좋습니다. 공통 질문은 하나입니다. <strong>서버가 할 수 있는 일을 어떤 요청에 먼저 배정할 것인가?</strong></p>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-qps만-보면-무거운-요청을-놓친다">1) QPS만 보면 무거운 요청을 놓친다</h3>
<p>운영에서 자주 나오는 착각은 &ldquo;초당 요청 수가 낮으니 괜찮다&quot;입니다. 하지만 백엔드 부하는 요청 수만으로 결정되지 않습니다. 같은 1건이라도 아래처럼 비용이 다릅니다.</p>
<table>
  <thead>
      <tr>
          <th>요청</th>
          <th style="text-align: right">겉보기 QPS</th>
          <th>실제 비용</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>GET /users/me</code></td>
          <td style="text-align: right">높음</td>
          <td>캐시 hit, DB 0~1회, 응답 작음</td>
      </tr>
      <tr>
          <td><code>GET /reports/monthly</code></td>
          <td style="text-align: right">낮음</td>
          <td>DB aggregation 6개, 외부 API 2회, 응답 큼</td>
      </tr>
      <tr>
          <td><code>POST /orders/import</code></td>
          <td style="text-align: right">낮음</td>
          <td>파일 파싱, row validation, 큐 적재, 중복 검사</td>
      </tr>
      <tr>
          <td><code>GET /search</code></td>
          <td style="text-align: right">중간</td>
          <td>검색 엔진 fan-out, highlight, pagination 비용</td>
      </tr>
  </tbody>
</table>
<p>QPS 기반 rate limit만 있으면 <code>GET /users/me</code>와 <code>GET /reports/monthly</code>를 같은 1건으로 봅니다. 그러면 낮은 QPS의 무거운 요청이 DB pool을 잠식하고, 평범한 조회 API의 p99까지 같이 끌어내립니다. 그래서 실무에서는 endpoint별 <strong>request cost unit</strong>을 둬야 합니다.</p>
<p>초기 기준은 단순해도 됩니다.</p>
<table>
  <thead>
      <tr>
          <th>cost unit</th>
          <th>기준 예시</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU unit</td>
          <td>p95 CPU time 10ms를 1점</td>
      </tr>
      <tr>
          <td>DB unit</td>
          <td>단순 indexed query 1회를 1점, full scan 또는 aggregation은 5~20점</td>
      </tr>
      <tr>
          <td>External unit</td>
          <td>외부 API 1회를 10점, 결제/배송처럼 느린 의존성은 20점</td>
      </tr>
      <tr>
          <td>Payload unit</td>
          <td>응답 100KB를 1점, upload 1MB를 2~5점</td>
      </tr>
      <tr>
          <td>Queue unit</td>
          <td>메시지 1개 enqueue를 1점, fan-out 메시지는 개수만큼 가산</td>
      </tr>
  </tbody>
</table>
<p>정밀한 원가 계산이 목표가 아닙니다. &ldquo;이 요청은 평균 요청보다 20배 비싸다&quot;를 팀이 같은 언어로 말하게 만드는 것이 먼저입니다.</p>
<h3 id="2-resource-budget은-timeout과-다르다">2) Resource Budget은 timeout과 다르다</h3>
<p>timeout은 시간 제한입니다. resource budget은 시간뿐 아니라 리소스 사용량 제한입니다.</p>
<ul>
<li><code>remaining_time_ms</code>: 이 요청이 더 쓸 수 있는 시간</li>
<li><code>remaining_db_queries</code>: 더 실행할 수 있는 DB 쿼리 수</li>
<li><code>remaining_external_calls</code>: 더 호출할 수 있는 외부 API 수</li>
<li><code>remaining_payload_bytes</code>: 더 내려줄 수 있는 응답 크기</li>
<li><code>remaining_retry_count</code>: 더 허용되는 재시도 횟수</li>
<li><code>remaining_cost_units</code>: 위 항목을 합산한 요청 비용 점수</li>
</ul>
<p><a href="/learning/deep-dive/deep-dive-end-to-end-deadline-cancellation-playbook/">종단간 Deadline Budget과 Cancellation Propagation</a>이 &ldquo;언제까지 끝낼 것인가&quot;를 다룬다면, resource budget은 &ldquo;그 시간 안에 무엇을 얼마나 써도 되는가&quot;를 다룹니다. 둘은 같이 가야 합니다. 시간이 남았더라도 DB 쿼리 예산을 다 썼으면 추가 fan-out을 막아야 하고, DB 예산이 남았더라도 deadline이 50ms밖에 남지 않았다면 degraded response로 빠지는 편이 낫습니다.</p>
<h3 id="3-예산은-endpoint-tenant-request-class-세-축으로-잡는다">3) 예산은 endpoint, tenant, request class 세 축으로 잡는다</h3>
<p>모든 요청에 같은 예산을 주면 공정하지 않습니다. 실무에서는 세 축을 같이 봅니다.</p>
<ol>
<li><strong>Endpoint budget</strong>: API 특성별 기본 예산</li>
<li><strong>Tenant budget</strong>: 고객 또는 조직별 공정성 예산</li>
<li><strong>Request class budget</strong>: interactive, background, admin, batch 같은 목적별 예산</li>
</ol>
<p>예를 들어 같은 검색 API라도 interactive 검색은 p95 500ms 안에 partial result라도 주는 것이 중요하고, 관리자 bulk export는 10분이 걸려도 정확성과 재시작 가능성이 더 중요합니다. 두 요청을 같은 <code>GET /search</code> 계열로만 보면 의사결정이 흐려집니다.</p>
<p>출발점은 아래처럼 둘 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>class</th>
          <th style="text-align: right">latency 목표</th>
          <th style="text-align: right">cost unit</th>
          <th>초과 시 정책</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>interactive read</td>
          <td style="text-align: right">p95 500ms</td>
          <td style="text-align: right">50</td>
          <td>partial response, cache fallback</td>
      </tr>
      <tr>
          <td>interactive write</td>
          <td style="text-align: right">p95 800ms</td>
          <td style="text-align: right">80</td>
          <td>idempotency key 확인 후 빠른 실패</td>
      </tr>
      <tr>
          <td>admin report</td>
          <td style="text-align: right">p95 3s</td>
          <td style="text-align: right">200</td>
          <td>async job 전환</td>
      </tr>
      <tr>
          <td>batch import</td>
          <td style="text-align: right">분 단위</td>
          <td style="text-align: right">1000</td>
          <td>queue, chunk, retry budget</td>
      </tr>
      <tr>
          <td>internal health</td>
          <td style="text-align: right">p95 100ms</td>
          <td style="text-align: right">5</td>
          <td>항상 lightweight 유지</td>
      </tr>
  </tbody>
</table>
<p>중요한 우선순위는 <strong>사용자-facing interactive 요청 보호 &gt; 데이터 정합성 유지 &gt; batch 처리량 &gt; 편의성 기능</strong>입니다. 이 기준이 없으면 장애 때 가장 큰 요청이 가장 많은 자원을 계속 가져갑니다.</p>
<h3 id="4-예산-초과는-실패가-아니라-분기-조건이다">4) 예산 초과는 실패가 아니라 분기 조건이다</h3>
<p>budget을 만들면 처음에는 차단 규칙처럼 보입니다. 하지만 좋은 resource budgeting은 무조건 거절보다 먼저 <strong>대체 경로</strong>를 설계합니다.</p>
<ul>
<li>남은 시간이 부족하면 full aggregation 대신 cached summary를 반환</li>
<li>DB query 예산이 부족하면 N+1 상세 정보를 생략하고 <code>has_more_details=true</code> 표시</li>
<li>외부 API 예산이 부족하면 실시간 조회 대신 마지막 동기화 시각을 함께 반환</li>
<li>payload 예산이 부족하면 cursor pagination을 강제</li>
<li>tenant 예산이 부족하면 429와 <code>Retry-After</code>를 명확히 반환</li>
<li>시스템 전체 포화면 503과 load shedding으로 빠르게 실패</li>
</ul>
<p>이 분기는 <a href="/learning/deep-dive/deep-dive-graceful-degradation-brownout-playbook/">Graceful Degradation과 Brownout</a>, <a href="/learning/deep-dive/deep-dive-cursor-pagination-consistency-playbook/">Cursor Pagination Consistency</a>, <a href="/learning/deep-dive/deep-dive-cache-consistency-invalidation-playbook/">Cache Consistency와 Invalidation</a>과도 연결됩니다. 예산은 사용자 경험을 망치기 위한 규칙이 아니라, 전체 시스템이 무너지는 것을 막으면서 덜 중요한 부분을 줄이는 기준입니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-핵심-api-10개의-cost-profile부터-만든다">1) 핵심 API 10개의 cost profile부터 만든다</h3>
<p>처음부터 모든 API를 분석할 필요는 없습니다. 트래픽 상위 5개와 장애 때 자주 등장하는 무거운 API 5개를 고릅니다. 각 API에 대해 최근 14일 기준으로 아래 값을 기록합니다.</p>
<table>
  <thead>
      <tr>
          <th>지표</th>
          <th>권장 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>p50/p95/p99 latency</td>
          <td>p95는 SLO, p99는 포화 신호</td>
      </tr>
      <tr>
          <td>DB query count</td>
          <td>평균보다 p95 query count가 중요</td>
      </tr>
      <tr>
          <td>DB pool wait</td>
          <td>p95 50ms 초과면 포화 후보</td>
      </tr>
      <tr>
          <td>external call count</td>
          <td>2개 이상이면 fan-out 관리 필요</td>
      </tr>
      <tr>
          <td>response size</td>
          <td>p95 500KB 초과면 pagination 검토</td>
      </tr>
      <tr>
          <td>retry count</td>
          <td>요청당 1회 초과면 retry storm 후보</td>
      </tr>
      <tr>
          <td>cache hit ratio</td>
          <td>80% 미만이면 fallback 품질 확인</td>
      </tr>
      <tr>
          <td>tenant concentration</td>
          <td>상위 tenant 1곳이 30% 초과면 공정성 검토</td>
      </tr>
  </tbody>
</table>
<p>이 표를 만들면 &ldquo;느린 API&quot;보다 더 유용한 결론이 나옵니다. 예를 들어 latency는 비슷해도 DB query count가 4배인 API, 외부 호출이 많아 timeout 전파가 중요한 API, 응답 크기 때문에 네트워크 비용이 큰 API를 나눌 수 있습니다.</p>
<h3 id="2-request-cost-unit을-로그와-trace에-남긴다">2) request cost unit을 로그와 trace에 남긴다</h3>
<p>budget은 코드 안 상수로만 있으면 운영에 쓰기 어렵습니다. 최소한 아래 필드는 structured log 또는 trace attribute에 남깁니다.</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_resource_budget</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">endpoint</span>: <span style="color:#f1fa8c">&#34;GET /reports/monthly&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">request_class</span>: <span style="color:#f1fa8c">&#34;interactive_read&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">tenant_id_hash</span>: <span style="color:#f1fa8c">&#34;t_91ab&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">budget_units</span>: <span style="color:#bd93f9">120</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">used_units</span>: <span style="color:#bd93f9">146</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">db_query_count</span>: <span style="color:#bd93f9">12</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">external_call_count</span>: <span style="color:#bd93f9">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">response_bytes</span>: <span style="color:#bd93f9">842000</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">budget_decision</span>: <span style="color:#f1fa8c">&#34;degraded_partial_response&#34;</span>
</span></span></code></pre></div><p>원문 고객 식별자나 민감 데이터는 남기지 않습니다. 대신 tenant hash, endpoint, class, budget decision을 남기면 운영 판단에 충분합니다.</p>
<h3 id="3-차단-기준은-429와-503을-분리한다">3) 차단 기준은 429와 503을 분리한다</h3>
<p>예산 초과 응답은 원인을 구분해야 합니다.</p>
<ul>
<li>특정 사용자, API key, tenant가 자기 예산을 넘김: <strong>429</strong></li>
<li>시스템 전체가 포화되어 정상 요청도 보호해야 함: <strong>503</strong></li>
<li>요청 자체가 너무 큰 형태로 들어옴: <strong>413</strong> 또는 <strong>422</strong></li>
<li>deadline이 이미 지나 의미 없는 요청: <strong>408/499 계열 관측 + 빠른 중단</strong></li>
</ul>
<p>429에는 <code>Retry-After</code>, 남은 quota 또는 다음 window를 알려주는 헤더가 필요합니다. 503에는 클라이언트가 짧은 즉시 재시도를 반복하지 않도록 backoff 힌트가 필요합니다. 둘을 섞으면 클라이언트가 잘못 재시도하고 서버 부하가 커집니다.</p>
<h3 id="4-도입-순서는-측정-경고-제한-자동-조정이다">4) 도입 순서는 측정, 경고, 제한, 자동 조정이다</h3>
<p>권장 rollout은 4단계입니다.</p>
<ol>
<li><strong>측정 전용 2주</strong>: cost unit을 계산하되 차단하지 않습니다.</li>
<li><strong>경고 2주</strong>: budget 120% 초과 요청을 로그와 대시보드에 표시합니다.</li>
<li><strong>부분 제한 2주</strong>: payload, pagination, 외부 호출 같은 안전한 항목부터 제한합니다.</li>
<li><strong>자동 조정</strong>: tenant별 예산, degraded path, admission control을 traffic pattern에 맞춰 조정합니다.</li>
</ol>
<p>처음부터 강하게 막으면 실제 사용자 흐름을 깨기 쉽습니다. 반대로 측정만 오래 하면 정책이 장식이 됩니다. 6주 안에 최소 하나의 실제 제한 정책까지 가는 것이 좋습니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, cost unit은 완벽한 원가 모델이 아닙니다. CPU, DB, 네트워크, 외부 API 비용을 하나의 점수로 합치면 단순화가 생깁니다. 그래도 아무 기준 없이 QPS만 보는 것보다는 낫습니다.</p>
<p>둘째, 예산이 너무 낮으면 제품 기능이 빈약해집니다. 특히 검색, 추천, 리포트처럼 결과 품질이 중요한 API는 partial response가 사용자 신뢰를 떨어뜨릴 수 있습니다. 이런 API는 degraded 응답에 <code>generated_at</code>, <code>partial</code>, <code>omitted_fields</code> 같은 표시를 넣어야 합니다.</p>
<p>셋째, tenant budget은 영업 정책과 충돌할 수 있습니다. 대형 고객이 더 많은 자원을 쓰는 것은 자연스러울 수 있습니다. 그래서 technical budget과 contract tier를 연결해야 합니다. 무료/기본/엔터프라이즈 tier별로 burst, sustained budget, batch window를 다르게 두는 편이 현실적입니다.</p>
<p>넷째, budget 초과를 모두 에러로 보면 개발자가 우회합니다. 좋은 운영은 차단 수보다 <strong>degraded 성공률</strong>, <strong>budget 초과 후 재시도율</strong>, <strong>tenant별 공정성 개선</strong>을 봅니다.</p>
<p>다섯째, resource budget은 코드만으로 끝나지 않습니다. 제품 요구, SLO, 인프라 비용, 고객 등급, 장애 대응 기준이 같이 들어갑니다. 소유자가 불명확하면 금방 오래된 숫자가 됩니다.</p>
<p>의사결정 우선순위는 <strong>시스템 생존 &gt; interactive 사용자 경험 &gt; 데이터 정합성 &gt; tenant 공정성 &gt; 인프라 비용 &gt; 부가 기능 완성도</strong>입니다. 장애 상황에서는 이 순서가 특히 중요합니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 핵심 API 10개의 p95 latency, DB query count, external call count, response size를 알고 있다.</li>
<li><input disabled="" type="checkbox"> endpoint별 request cost unit 기준이 문서화돼 있다.</li>
<li><input disabled="" type="checkbox"> tenant별 budget과 system-wide admission control을 구분한다.</li>
<li><input disabled="" type="checkbox"> budget 초과 시 429, 503, partial response, async 전환 기준이 분리돼 있다.</li>
<li><input disabled="" type="checkbox"> budget decision이 structured log 또는 trace에 남는다.</li>
<li><input disabled="" type="checkbox"> retry 정책이 request budget을 초과하지 않도록 제한돼 있다.</li>
<li><input disabled="" type="checkbox"> budget 초과 알림은 에러율뿐 아니라 degraded path 사용률과 같이 본다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>최근 장애 또는 latency 이슈가 있었던 API 1개를 골라 <code>DB query count</code>, <code>external call count</code>, <code>response_bytes</code>, <code>retry_count</code>의 p95를 계산해 보세요.</li>
<li>그 API의 기본 budget을 100점으로 두고 DB, 외부 호출, payload, retry가 각각 몇 점을 쓰는지 가중치를 만들어 보세요.</li>
<li>budget 120% 초과 시 &ldquo;무조건 실패&quot;가 아니라 &ldquo;어떤 필드를 생략할지&rdquo;, &ldquo;어떤 호출을 캐시로 대체할지&rdquo;, &ldquo;언제 async job으로 보낼지&quot;를 표로 정리해 보세요.</li>
<li>상위 tenant 5곳의 1시간 단위 cost unit 사용량을 비교해, 한 tenant가 전체의 30%를 넘는 구간이 있는지 확인해 보세요.</li>
</ol>
<p>API Resource Budgeting의 목적은 개발자를 괴롭히는 제한표를 만드는 것이 아닙니다. 요청 하나가 실제로 무엇을 소비하는지 드러내고, 시스템이 바쁠 때 어떤 일을 먼저 살릴지 합의하는 것입니다. QPS와 평균 latency만 보는 팀은 장애를 늦게 발견합니다. 요청 비용을 보는 팀은 장애가 오기 전에 무거운 흐름을 줄일 수 있습니다.</p>
<h2 id="관련-글">관련 글</h2>
<ul>
<li><a href="/learning/deep-dive/deep-dive-capacity-planning-littles-law-saturation/">Capacity Planning과 Little&rsquo;s Law</a></li>
<li><a href="/learning/deep-dive/deep-dive-admission-control-concurrency-limits/">Admission Control과 Concurrency Limits</a></li>
<li><a href="/learning/deep-dive/deep-dive-api-rate-limit-backpressure/">API 레이트 리밋과 백프레셔</a></li>
<li><a href="/learning/deep-dive/deep-dive-end-to-end-deadline-cancellation-playbook/">종단간 Deadline Budget과 Cancellation Propagation</a></li>
<li><a href="/learning/deep-dive/deep-dive-tail-latency-engineering-playbook/">Tail Latency 엔지니어링 플레이북</a></li>
</ul>
]]></content:encoded></item></channel></rss>