--- kind: CONCEPT slug: adapter-outbound-httpclient-c05 title: 열린 회로가 토큰과 permit을 쓰기 전에 거절한다 topic: admission-budget-and-backpressure project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: concept:adapter-outbound-httpclient-c05 evidenceCapturedOn: 2026-09-01 assets: - key: adapter-outbound-httpclient-c05 file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c05.svg - key: adapter-outbound-httpclient-c05-diagram file: ../../../final/assets/diagrams/adapter-outbound-httpclient-c05.svg evidence: - ../../../final/evidence/raw/adapter-outbound-httpclient-c05.txt source: - 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L323 이다. module: adapter-outbound-httpclient --- # 열린 회로가 토큰과 permit을 쓰기 전에 거절한다 `AttemptResiliencePipeline`이 물리 시도마다 Circuit Breaker → Rate Limiter → Bulkhead → HTTP 호출을 고정 순서로 적용하고 역순으로 해제한다. 순서는 장식이 아니다. ## 본문 `AttemptResiliencePipeline`이 물리 시도마다 **Circuit Breaker → Rate Limiter → Bulkhead → HTTP 호출**을 고정 순서로 적용하고 역순으로 해제한다. ## 시도마다 지나는 가드 순서 :::evidence key="adapter-outbound-httpclient-c05-diagram" alt="회로 차단기와 요금 제한기와 벌크헤드와 HTTP 호출이 왼쪽에서 오른쪽으로 이어지고 화살표에 허가와 토큰과 permit 이 붙은 구조" caption="시도마다 지나는 가드 순서" zoom="false" ::: > "The order is not cosmetic. An open circuit must reject before a rate token or a bulkhead permit is spent, otherwise **a dead upstream keeps consuming the quota and concurrency that healthy upstreams need.**" > "A local rejection (rate limiter or bulkhead) is deliberately *not* recorded as a circuit error: the upstream never saw the request, and **counting our own back-pressure as upstream failure would open the breaker on a healthy dependency.**" ## 브레이커가 503을 보지 못하던 이력 이전에는 원시 전송만 파이프라인 안에서 돌고 응답→예외 매핑이 밖에서 일어나서 "a 503 completed the call normally, the breaker recorded a success, and **an upstream that answered nothing but 503 never opened its circuit. The thing the breaker is for was the one thing it could not see.**" 지금은 `remoteFailure` 분류기가 반환값을 보고 브레이커에 알린다. ## AttemptResiliencePipeline 참조 위치 :::evidence key="adapter-outbound-httpclient-c05" alt="코드베이스에서 AttemptResiliencePipeline 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AttemptResiliencePipeline 코드베이스 검색 — 16줄 · exit 0" zoom="true" ::: ## 규칙을 덮는 테스트와 프로토콜 증거 test 47개가 이 규칙들을 촘촘히 덮는다 — `appliesCircuitThenRateLimiterThenBulkheadPerAttempt`, `openCircuitDoesNotConsumeRateOrBulkheadPermit`, `bulkheadRejectionReleasesTheRateLimiterAndIsNotACircuitError`, `answeredStatusesDoNotRetryANonIdempotentOperation`, `deniesOneShotBodyEvenForPut`, `honorsRetryAfterOnlyInsideDeadline`, `protocolProofOfNonProcessingWinsOverEverything`, `streamAfterGoAwayLastIdIsPeerNotProcessed` 등. `Http2ProtocolEvidence`는 프로토콜 수준 증거를 다룬다 — `REFUSED_STREAM`과 GOAWAY의 last-stream-id보다 큰 스트림 id는 **피어가 처리하지 않았음의 증명**이라 `NOT_SENT`로 승격되고, 그 이하 id의 리셋은 여전히 모호하다(`streamAtOrBelowGoAwayLastIdStaysAmbiguous`, `aBareStreamResetProvesNothing`).