<?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>Adapter Pattern on jyukki's Blog</title><link>https://jyukki.com/tags/adapter-pattern/</link><description>Recent content in Adapter Pattern on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Thu, 16 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/adapter-pattern/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Outbound API Adapter, 외부 API 의존성을 제품 장애로 번지지 않게 막는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-outbound-api-adapter-dependency-isolation-playbook/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-outbound-api-adapter-dependency-isolation-playbook/</guid><description>결제, 인증, 메일, 지도, AI 추론처럼 외부 API에 기대는 기능을 어댑터 경계, timeout budget, 재시도, 에러 변환, 관측 지표로 안전하게 운영하는 기준을 정리합니다.</description><content:encoded><![CDATA[<p>백엔드 서비스는 생각보다 많은 외부 API 위에서 돌아갑니다. 결제 승인, OAuth 로그인, 메일 발송, SMS, 주소 정규화, 지도 거리 계산, 파일 바이러스 검사, AI 추론, 세금 계산, 배송 조회가 모두 그렇습니다. 처음에는 SDK 하나를 넣고 <code>client.send()</code>를 호출하면 끝나는 것처럼 보입니다. 하지만 운영에 들어가면 질문이 달라집니다. provider가 2초 느려지면 우리 API는 몇 초까지 기다릴 것인가. 쓰기 요청이 timeout 났는데 실제로는 처리됐을 수 있다면 사용자는 어떤 상태를 보게 할 것인가. provider가 필드 이름이나 에러 코드를 바꾸면 어느 테스트가 먼저 깨질 것인가.</p>
<p>외부 API 호출은 코드 한 줄이 아니라 <strong>제품 의존성</strong>입니다. 그래서 controller나 domain service 안에서 provider SDK를 직접 호출하는 구조는 편해 보여도 오래 버티기 어렵습니다. 호출 정책, 장애 처리, 비용, 감사 로그, 데이터 경계가 각 호출부에 흩어지고, 나중에 provider를 교체하거나 장애 모드를 만들 때 모든 비즈니스 로직을 다시 열어야 합니다. 이 글은 <a href="/learning/deep-dive/deep-dive-api-resource-budgeting/">API Resource Budgeting</a>, <a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout, Retry, Backoff</a>, <a href="/learning/deep-dive/deep-dive-api-error-semantics-retryability-contract/">API Error Semantics</a>, <a href="/learning/deep-dive/deep-dive-resilience4j-circuit-breaker/">Resilience4j Circuit Breaker</a>와 이어집니다. 핵심은 &ldquo;HTTP client를 감싸자&quot;가 아니라, <strong>우리 시스템이 감당할 수 있는 외부 의존성 계약을 만들자</strong>입니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>외부 API 호출을 단순 네트워크 호출이 아니라 가용성, 정합성, 비용, 보안 경계로 이해합니다.</li>
<li>provider SDK와 도메인 코드를 분리하는 outbound adapter의 역할을 설명할 수 있습니다.</li>
<li>timeout, retry, idempotency, circuit breaker, fallback을 호출 유형별로 숫자 기준과 함께 잡을 수 있습니다.</li>
<li>provider 변경, 장애, brownout, quota 초과를 대비하는 dependency register와 contract fixture 운영법을 가져갑니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-외부-api는-우리-slo-안으로-들어온다">1) 외부 API는 우리 SLO 안으로 들어온다</h3>
<p>외부 API가 느리거나 실패하면 사용자는 provider 이름을 보지 않습니다. 그냥 우리 서비스가 느리거나 실패했다고 느낍니다. 특히 로그인, 결제, 파일 업로드, AI 응답 생성처럼 사용자 흐름 한가운데 있는 호출은 provider SLO가 곧 우리 SLO 일부가 됩니다.</p>
<p>초기 분류는 아래처럼 할 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>호출 유형</th>
          <th>예시</th>
          <th>기본 목표</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>핵심 읽기</td>
          <td>사용자 권한 조회, 배송 상태 확인</td>
          <td>p95 500~1500ms, 짧은 retry 가능</td>
      </tr>
      <tr>
          <td>핵심 쓰기</td>
          <td>결제 승인, 구독 변경, 외부 티켓 생성</td>
          <td>idempotency key 필수, retry 0~1회</td>
      </tr>
      <tr>
          <td>비핵심 enrich</td>
          <td>추천, 주소 보정, 마케팅 태그</td>
          <td>300~800ms timeout, 실패 시 생략</td>
      </tr>
      <tr>
          <td>비동기 작업</td>
          <td>메일 발송, 리포트 전송, 정산 sync</td>
          <td>queue 기반, 재시도와 dead letter</td>
      </tr>
      <tr>
          <td>검증 작업</td>
          <td>바이러스 검사, 정책 확인</td>
          <td>결과 불명확 상태와 quarantine</td>
      </tr>
  </tbody>
</table>
<p>여기서 중요한 기준은 전체 요청 deadline입니다. 사용자 요청 p95 목표가 2초라면 외부 API 하나가 2초를 모두 써서는 안 됩니다. 동기 경로에서는 외부 API 대기 시간이 전체 deadline의 **30~40%**를 넘지 않게 시작하는 편이 좋습니다. 여러 provider를 연쇄 호출한다면 더 보수적으로 잡아야 합니다.</p>
<h3 id="2-adapter는-provider-sdk-래퍼가-아니라-내부-계약이다">2) Adapter는 provider SDK 래퍼가 아니라 내부 계약이다</h3>
<p>좋은 outbound adapter는 SDK 메서드를 그대로 노출하지 않습니다. 내부 도메인이 이해하는 입력과 결과를 제공합니다. 예를 들어 결제 provider SDK가 <code>ChargeResponse</code>, <code>status_code</code>, <code>decline_reason</code>, <code>raw_error</code>를 준다고 해서 주문 도메인이 그 값을 모두 알아야 하는 것은 아닙니다.</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-java" data-lang="java"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">public</span> <span style="color:#8be9fd;font-style:italic">interface</span> <span style="color:#50fa7b">PaymentGateway</span> {
</span></span><span style="display:flex;"><span>    PaymentAttemptResult <span style="color:#50fa7b">authorize</span>(PaymentAuthorizeCommand command);
</span></span><span style="display:flex;"><span>    PaymentCaptureResult <span style="color:#50fa7b">capture</span>(PaymentCaptureCommand command);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">public</span> <span style="color:#8be9fd;font-style:italic">record</span> <span style="color:#50fa7b">PaymentAttemptResult</span>(
</span></span><span style="display:flex;"><span>    String attemptId,
</span></span><span style="display:flex;"><span>    ExternalDecision decision,
</span></span><span style="display:flex;"><span>    Retryability retryability,
</span></span><span style="display:flex;"><span>    String providerRequestId,
</span></span><span style="display:flex;"><span>    <span style="color:#8be9fd">boolean</span> reconciliationRequired
</span></span><span style="display:flex;"><span>) {}
</span></span></code></pre></div><p>도메인 입장에서 중요한 것은 provider의 원문 필드가 아니라 &ldquo;승인됐는가&rdquo;, &ldquo;거절됐는가&rdquo;, &ldquo;다시 시도해도 되는가&rdquo;, &ldquo;결과 확인 작업이 필요한가&quot;입니다. provider별 세부 계약은 adapter 내부에 둡니다. 이렇게 해야 provider A의 <code>PENDING</code>, provider B의 <code>PROCESSING</code>, provider C의 timeout을 내부 상태 전이표로 안정적으로 매핑할 수 있습니다.</p>
<p>Adapter 경계에는 최소 네 가지가 들어갑니다.</p>
<ul>
<li>내부 command/result 모델</li>
<li>provider별 request/response 변환</li>
<li>timeout, retry, idempotency, rate limit 정책</li>
<li>metric, trace, log, provider request id 기록</li>
</ul>
<p>이 네 가지가 없으면 adapter는 이름만 있는 wrapper가 됩니다.</p>
<h3 id="3-재시도는-성공률을-높이지만-중복-부작용도-만든다">3) 재시도는 성공률을 높이지만 중복 부작용도 만든다</h3>
<p>읽기 호출은 짧은 재시도가 유용할 때가 많습니다. DNS 흔들림, 일시적인 502, connection reset은 1~2회 재시도로 회복될 수 있습니다. 반대로 쓰기 호출은 조심해야 합니다. 결제 승인, 메일 발송, 외부 티켓 생성 같은 작업을 아무 생각 없이 재시도하면 중복 결제, 중복 발송, 중복 케이스가 생깁니다.</p>
<p>초기 기준은 아래 정도가 현실적입니다.</p>
<table>
  <thead>
      <tr>
          <th>호출</th>
          <th>timeout</th>
          <th>retry</th>
          <th>필수 조건</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>읽기 API</td>
          <td>500~1500ms</td>
          <td>1~2회, jitter 포함</td>
          <td>전체 deadline 안에서만</td>
      </tr>
      <tr>
          <td>사용자 요청 중 쓰기</td>
          <td>2~3초</td>
          <td>0~1회</td>
          <td>idempotency key, payload hash</td>
      </tr>
      <tr>
          <td>비동기 쓰기</td>
          <td>provider SLA 기준</td>
          <td>제한적 backoff</td>
          <td>job id, dead letter, max attempt</td>
      </tr>
      <tr>
          <td>대량 sync</td>
          <td>batch 단위 timeout</td>
          <td>rate limit 기반</td>
          <td>checkpoint, resume token</td>
      </tr>
  </tbody>
</table>
<p>재시도는 반드시 <a href="/learning/deep-dive/deep-dive-timeout-retry-backoff/">Timeout, Retry, Backoff</a>의 deadline 안에 있어야 합니다. 상위 요청 deadline이 2초인데 하위 adapter가 1초 timeout을 3회 반복하면 이미 정책이 깨진 것입니다. 또한 retryable 판단은 HTTP status만으로 하지 않는 편이 좋습니다. <code>429</code>, <code>503</code>, connection timeout은 retry 후보일 수 있지만, <code>400</code>, 권한 오류, payload validation 실패는 대부분 재시도해도 해결되지 않습니다.</p>
<h3 id="4-에러-변환은-사용자-경험과-운영-대응을-분리한다">4) 에러 변환은 사용자 경험과 운영 대응을 분리한다</h3>
<p>Provider 에러를 그대로 사용자에게 노출하면 두 가지 문제가 생깁니다. 첫째, 사용자는 provider 내부 코드나 영어 메시지를 이해하지 못합니다. 둘째, 내부 서비스명, 계정 구조, quota 이름 같은 민감 정보가 노출될 수 있습니다.</p>
<p>Adapter는 외부 에러를 내부 에러 의미로 변환해야 합니다.</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">provider_error_mapping</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">timeout</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">user_message</span>: <span style="color:#f1fa8c">&#34;요청 처리 상태를 확인 중입니다.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">retryability</span>: <span style="color:#f1fa8c">&#34;unknown_result_check_later&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">operation</span>: <span style="color:#f1fa8c">&#34;enqueue_reconciliation&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">rate_limited</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">user_message</span>: <span style="color:#f1fa8c">&#34;잠시 후 다시 시도해 주세요.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">retryability</span>: <span style="color:#f1fa8c">&#34;retry_after&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">operation</span>: <span style="color:#f1fa8c">&#34;open_circuit_if_sustained&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">invalid_payload</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">user_message</span>: <span style="color:#f1fa8c">&#34;요청 정보를 다시 확인해 주세요.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">retryability</span>: <span style="color:#f1fa8c">&#34;not_retryable&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">operation</span>: <span style="color:#f1fa8c">&#34;developer_alert_if_new_schema&#34;</span>
</span></span></code></pre></div><p>핵심은 실패를 <code>success/fail</code> 두 값으로만 보지 않는 것입니다. 외부 쓰기 호출은 결과가 불명확할 수 있습니다. timeout이 났지만 provider 쪽에서는 성공했을 수 있습니다. 이런 상태는 사용자에게 무작정 실패라고 말하기보다 <code>PENDING_CONFIRMATION</code> 같은 내부 상태로 두고, provider status 조회나 webhook으로 확인하는 편이 안전합니다. 이 관점은 <a href="/learning/deep-dive/deep-dive-api-error-semantics-retryability-contract/">API Error Semantics</a>와 <a href="/learning/deep-dive/deep-dive-inbound-webhook-receiver-playbook/">Inbound Webhook Receiver</a>에서도 중요합니다.</p>
<h3 id="5-관측-지표는-provider별-기능별로-쪼개야-한다">5) 관측 지표는 provider별, 기능별로 쪼개야 한다</h3>
<p>외부 API 장애는 전체 error rate만 보면 늦게 보입니다. 특정 provider의 특정 endpoint만 느려질 수 있고, 특정 tenant의 quota만 막힐 수 있으며, 특정 지역에서 DNS나 TLS handshake가 흔들릴 수도 있습니다. Adapter에서 지표를 남기면 이런 차이를 빠르게 볼 수 있습니다.</p>
<p>최소 지표:</p>
<ul>
<li><code>external_api.request.count{provider, operation, result}</code></li>
<li><code>external_api.latency.ms{provider, operation}</code></li>
<li><code>external_api.timeout.count{provider, operation}</code></li>
<li><code>external_api.retry.count{provider, operation, reason}</code></li>
<li><code>external_api.circuit.state{provider, operation}</code></li>
<li><code>external_api.unknown_result.count{provider, operation}</code></li>
<li><code>external_api.quota.remaining{provider, account}</code></li>
</ul>
<p>알림 기준은 처음부터 완벽할 필요는 없습니다. 시작 기준으로는 timeout 또는 5xx가 <strong>5분 동안 10% 초과</strong>, p95 latency가 기준선 대비 <strong>2배 이상</strong>, unknown result가 평소 대비 <strong>3배 이상</strong>, quota remaining이 <strong>20% 이하</strong>일 때 알림을 걸 수 있습니다. 이후 실제 트래픽과 장애 이력을 보며 조정하면 됩니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-dependency-register부터-만든다">1) Dependency register부터 만든다</h3>
<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">provider</span>: <span style="color:#f1fa8c">&#34;AcmePay&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">owner</span>: <span style="color:#f1fa8c">&#34;payments-platform&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">business_flow</span>: [<span style="color:#f1fa8c">&#34;checkout&#34;</span>, <span style="color:#f1fa8c">&#34;subscription_renewal&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">criticality</span>: <span style="color:#f1fa8c">&#34;tier-0&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">sync_path</span>: <span style="color:#ff79c6">true</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">request_deadline_budget_ms</span>: <span style="color:#bd93f9">2500</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">adapter_timeout_ms</span>: <span style="color:#bd93f9">1200</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">retry_policy</span>: <span style="color:#f1fa8c">&#34;write_once_with_idempotency&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">idempotency_key</span>: <span style="color:#f1fa8c">&#34;order_id + payment_attempt_id&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">fallback</span>: <span style="color:#f1fa8c">&#34;pending_confirmation&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">reconciliation</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">method</span>: <span style="color:#f1fa8c">&#34;provider_status_api + webhook&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">max_delay_minutes</span>: <span style="color:#bd93f9">10</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">dashboard</span>: <span style="color:#f1fa8c">&#34;external-api/acmepay&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">alert</span>: <span style="color:#f1fa8c">&#34;timeout_rate_10pct_5m&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">exit_plan</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">alternative_provider</span>: <span style="color:#f1fa8c">&#34;BetaPay&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">contract_fixture_count</span>: <span style="color:#bd93f9">80</span>
</span></span></code></pre></div><p>이 문서가 있으면 장애 때 &ldquo;어디까지 기다릴지&rdquo;, &ldquo;누가 owner인지&rdquo;, &ldquo;무엇을 낮춰 제공할지&quot;를 바로 확인할 수 있습니다. 반대로 이 목록이 없으면 외부 API 장애는 매번 코드 검색부터 시작합니다.</p>
<h3 id="2-패키지-구조는-도메인과-provider를-분리한다">2) 패키지 구조는 도메인과 provider를 분리한다</h3>
<p>Spring 기준으로는 아래처럼 둘 수 있습니다.</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-text" data-lang="text"><span style="display:flex;"><span>payment/
</span></span><span style="display:flex;"><span>  application/
</span></span><span style="display:flex;"><span>    PaymentService.java
</span></span><span style="display:flex;"><span>  domain/
</span></span><span style="display:flex;"><span>    PaymentAttempt.java
</span></span><span style="display:flex;"><span>    PaymentGateway.java
</span></span><span style="display:flex;"><span>  infrastructure/
</span></span><span style="display:flex;"><span>    acmepay/
</span></span><span style="display:flex;"><span>      AcmePayAdapter.java
</span></span><span style="display:flex;"><span>      AcmePayClient.java
</span></span><span style="display:flex;"><span>      AcmePayErrorMapper.java
</span></span><span style="display:flex;"><span>      AcmePayProperties.java
</span></span><span style="display:flex;"><span>      AcmePayContractFixturesTest.java
</span></span></code></pre></div><p><code>PaymentService</code>는 <code>AcmePayClient</code>를 몰라야 합니다. 도메인은 <code>PaymentGateway</code>만 알고, provider 전용 request, response, signature, endpoint, raw error는 infrastructure 아래에 둡니다. 이 경계가 있어야 테스트도 쉬워집니다. 도메인 테스트는 fake gateway로 상태 전이를 확인하고, adapter 테스트는 provider fixture와 mock server로 계약 변환을 확인합니다.</p>
<h3 id="3-contract-fixture를-저장한다">3) Contract fixture를 저장한다</h3>
<p>Provider API는 문서만 믿기 어렵습니다. 실제 sandbox 응답, webhook payload, error response, rate limit header를 fixture로 저장해 회귀 테스트에 넣는 편이 좋습니다.</p>
<p>검증 항목:</p>
<ul>
<li>정상 응답이 내부 result로 매핑되는가</li>
<li>provider enum이 새 값으로 늘어났을 때 unknown으로 안전하게 떨어지는가</li>
<li>timeout, 429, 5xx, validation error가 retryability로 분리되는가</li>
<li>민감 필드가 로그에 남지 않는가</li>
<li>provider request id와 trace id가 함께 남는가</li>
</ul>
<p>Provider가 API version을 올리거나 SDK를 업데이트할 때는 fixture 테스트를 먼저 돌립니다. 외부 API 변경은 런타임에서 처음 발견하면 늦습니다.</p>
<h3 id="4-circuit-breaker와-bulkhead를-adapter-단위로-둔다">4) Circuit breaker와 bulkhead를 adapter 단위로 둔다</h3>
<p>외부 API가 느려졌을 때 가장 나쁜 구조는 모든 웹 요청 thread가 같은 provider 응답을 기다리는 것입니다. 이때는 우리 DB가 멀쩡해도 서비스 전체가 느려집니다. Adapter별로 bulkhead를 두고, 실패율이 일정 기준을 넘으면 circuit을 열어 호출을 잠시 멈춥니다.</p>
<p>권장 시작값:</p>
<ul>
<li>외부 provider별 최대 동시 호출 수를 전체 worker thread의 10~20% 이하로 제한</li>
<li>timeout/5xx 비율 10% 초과가 5분 지속되면 open</li>
<li>half-open은 5~20개 probe 요청으로 제한</li>
<li>핵심 write path는 breaker open 시 <code>pending</code> 또는 명확한 사용자 메시지로 전환</li>
<li>비핵심 path는 빈 추천, cached value, &ldquo;나중에 갱신&quot;으로 degraded response 제공</li>
</ul>
<p>Circuit breaker는 실패를 숨기는 장치가 아닙니다. 실패가 전체 시스템으로 번지지 않게 경계를 만드는 장치입니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, adapter를 너무 일반화하면 오히려 망가집니다. 모든 provider를 같은 인터페이스에 억지로 맞추면 결제, 메일, AI 추론, 지도 호출의 중요한 차이가 사라집니다. 공통화할 것은 관측, timeout, error mapping 같은 운영 골격이고, 도메인 의미는 각 adapter에 남겨야 합니다.</p>
<p>둘째, provider 고유 기능을 숨기면 제품 속도가 느려질 수 있습니다. 이때는 내부 계약에 필요한 기능을 명시적으로 올리는 편이 낫습니다. &ldquo;나중에 바꿀 수 있게 아무것도 쓰지 말자&quot;가 아니라 &ldquo;우리가 의식적으로 선택한 provider 기능을 문서화하자&quot;가 맞습니다.</p>
<p>셋째, fallback은 언제나 가능한 것이 아닙니다. 결제 승인, 권한 변경, 보안 검증처럼 되돌리기 어려운 작업은 가짜 성공으로 넘기면 더 큰 사고가 됩니다. 이런 경로에서는 축소 제공보다 pending 상태, 수동 확인, 재처리 큐가 안전합니다.</p>
<p>넷째, 외부 API를 캐시하면 비용과 지연은 줄지만 신선도 문제가 생깁니다. 권한, 가격, 재고, 환율처럼 최신성이 중요한 값은 TTL과 stale 허용 범위를 명확히 해야 합니다.</p>
<p>다섯째, provider 상태 페이지를 맹신하면 안 됩니다. 상태 페이지가 정상이어도 특정 region, account, endpoint, quota만 실패할 수 있습니다. 우리 synthetic probe와 실제 사용자 지표가 더 빠른 신호일 때가 많습니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 외부 API별 owner, criticality, timeout, retry, quota, fallback이 dependency register에 있다.</li>
<li><input disabled="" type="checkbox"> 도메인 코드는 provider SDK 타입과 raw error를 직접 참조하지 않는다.</li>
<li><input disabled="" type="checkbox"> 쓰기 호출에는 idempotency key, payload hash, provider request id가 남는다.</li>
<li><input disabled="" type="checkbox"> timeout과 retry는 상위 request deadline 안에서 계산된다.</li>
<li><input disabled="" type="checkbox"> provider timeout, 5xx, 429, unknown result, quota remaining 지표가 adapter에서 나온다.</li>
<li><input disabled="" type="checkbox"> provider fixture와 error mapping 회귀 테스트가 있다.</li>
<li><input disabled="" type="checkbox"> 장애 시 degraded response, pending confirmation, reconciliation queue 중 하나로 흐름이 정리된다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>현재 서비스에서 외부 API 3개를 고르고 criticality를 <code>tier-0</code>, <code>tier-1</code>, <code>tier-2</code>로 나눠 보세요.</li>
<li>가장 위험한 쓰기 호출 하나를 골라 timeout 이후 실제 provider 처리 여부를 어떻게 확인하는지 적어 보세요.</li>
<li>provider SDK 타입이 도메인 service까지 새어 들어간 코드가 있다면 내부 result 타입으로 감싸는 adapter를 설계해 보세요.</li>
<li>provider 장애를 가정하고 5분 동안 timeout 20%가 발생했을 때 사용자 화면, 알림, queue, 재처리 흐름이 어떻게 움직이는지 런북으로 써 보세요.</li>
</ol>
]]></content:encoded></item></channel></rss>