Files
llm-wiki/raw/branch-notes/feature-outbound-http-client-baseline.md
T

55 KiB
Raw Blame History

title, source_type, status, branch, parent_branch, governing_docs, related_projects, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
title source_type status branch parent_branch governing_docs related_projects tags created target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-outbound-http-client-baseline branch-note raw feature-outbound-http-client-baseline
wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound
ca-skeleton
branch
ca-skeleton
outbound-http
rest-client
adapter
2026-05-21 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-007 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-007
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TESTCONTAINERS-001@1
1 17707c8f1f903e49e2466e6f22228fa014375942bc5e97f2098b8b3ffdcac255

branch: feature-outbound-http-client-baseline

Layer: raw/branch-notes/ — outbound HTTP adapter 실패 분류와 RestClient baseline을 정의합니다. 구조 메모: 이 노트는 2026-05-21 생성(현 branch-note 템플릿 이전 포맷). 2026-06-10 /branch-spec 에서 템플릿 순서로 재정렬 + §구현 가이드·§엣지·실패·의존·§Audit & Findings·§관련 일일 노트 신설. 템플릿에 없는 pre-template 보조 섹션(Work Item Contract / Decisionized Work Items / 테스트 계약 / 외부 근거)은 삭제하지 않고 관련 템플릿 섹션 옆에 슬롯해 보존했다.

[!note] 개정 예정 고지 (2026-07-28) 본 branch 는 owner 를 유지하되 내용을 코드 사실에 맞춰 개정한다. 근거: docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md §5.3.

  • 개정 사유: globalCallTimeout 이 실행 중 호출을 취소하지 않고 다음 재시도 진입 여부만 판정하며, decorator 순서가 문서 서술과 다르다. total deadline 의 능동 취소와 순서 계약을 재판정한다.
  • 참조 설계: ca-tmpl docs/superpowers/specs/2026-07-27-httpclient-production-capability-design.md §3·§15·§17~§20 (등급 internal-design-doc — 공식 문서 아님)

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§11 Adapter Failure Contract — Outbound HTTP, §32.3 Outbound HTTP / Resilience, 외부 근거 Group G-C) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: timeout·retry·circuit-breaker contract test가 통과한다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1 Resilience4j가 default이며 Spring Retry는 simple blocking retry에만 허용한다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TESTCONTAINERS-001@1 Testcontainers는 persistence/outbound integration test에 사용하고 unit·architecture·contract test에서는 금지한다 Work Item 완료 조건에 적용 raw/project-notes/ca-skeleton-operational-contract

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

외부/내부 API 호출 실패를 HTTP status만으로 처리하면 원인과 운영 조치가 흐려집니다. RestClient를 기본 표준으로 두고 status, timeout, DNS, connect failure, retry/backoff/circuit breaker 기준을 정리합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • RestClient baseline.
  • upstream 4xx/5xx 분류.
  • timeout/connect/DNS failure 분류.
  • outbound dependency log field.
  • request/response body logging 금지.
  • allowlist 기반 redaction 기준.
  • retry/backoff/circuit breaker 도입 기준.

제외 범위

  • WebClient 기본 탑재.
  • provider-specific SDK 구현.
  • business-specific upstream contract.

근거 (필수, 최소 1개+)

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/official-docs/outbound-spring-restclient-baseline RestClient baseline 채택의 공식 근거 + RestTemplate maintenance 상태
raw/official-docs/outbound-resilience4j-vs-spring-retry Resilience4j 채택 + Spring Retry 좁은 예외 허용 + Hystrix 배제 근거
raw/official-docs/outbound-webclient-vs-restclient-spring WebClient를 baseline에서 배제하고 extension doc으로 미루는 이유 (reactor event-loop blocking risk
raw/official-docs/outbound-openfeign-declarative-client OpenFeign declarative 대안 + maintenance status + Spring 6
raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering retry + idempotency-key 결합, full-jitter backoff
raw/official-docs/resilience4j-micrometer-module D4 — CircuitBreaker resilience4j.circuitbreaker.calls/state metric 명 + default tag (kind/name) 의 vendor 공식 근거 (ca-tmpl 의 dependency.name/dependency.type/outcome 재매핑 대상)
raw/official-docs/spring-restclient-builder-reference D5/D7 mechanism — RestClient builder + 5개 ClientRequestFactory 추상화 (timeout 정량 값은 vendor 미권고 — UNSUPPORTED 유지) + default 4xx/5xx error handling
raw/official-docs/spring-smartlifecycle-reference D8 — SmartLifecycle interface (Lifecycle + Phased) + startup ascending/shutdown descending phase + stop(Runnable) graceful shutdown 의 vendor 공식 근거
raw/official-docs/rfc9110-http-semantics D6 — idempotent method 정의 (PUT/DELETE + safe GET/HEAD/OPTIONS/TRACE) + client SHOULD NOT auto-retry non-idempotent (RFC 9110 §9.2.2) 의 official-standard 근거

외부 근거 (Group G-C — Outbound HTTP)

ca-tmpl outbound HTTP baseline 결정 + 대안 비교 자료.

검색 키워드 기록: Spring RestClient maintenance RestTemplate, Resilience4j vs Spring Retry circuit breaker, WebClient blocking reactor event loop, Spring Cloud OpenFeign maintenance @HttpExchange, Stripe rate limiters engineering blog.

TODO

TODO drained — 결정은 아래 표/결정 사항 참조.

진행 중 메모

  • WebClient는 별도 extension 문서에서만 다루며, baseline은 RestClient로 고정합니다.

결정 사항

  • 2026-05-21: 기본 outbound HTTP는 RestClient 기준.
  • 2026-05-22: retry/circuit breaker 기본 라이브러리는 Resilience4j. Spring Retry는 simple blocking retry에만 예외 허용.
  • 2026-05-22: retry 기본값은 disabled이며, 활성화 시 retryable registry error와 low-cardinality retry metric이 필수.
  • 2026-05-22: circuit breaker metric은 dependency.name, dependency.type, outcome까지만 tag로 허용.
  • 2026-05-22: outbound HTTP timeout default = connect 2s / read 5s / global call 10s. timeout 미설정 또는 무한 timeout은 forbidden. per-endpoint override는 capability registry에 등록 시에만 허용.
  • 2026-05-22: retry 분기는 idempotent method(GET/HEAD/PUT/DELETE)만 default retry 허용, POST/PATCH는 idempotency key 헤더가 있을 때만 retry 허용.
  • 2026-05-22: response size limit default = 10MB streaming threshold. 초과 시 streaming 처리 의무.
  • 2026-05-22: shutdown 중 retry suppression 의무. ApplicationListener 또는 동등 mechanism으로 retry policy를 NO_RETRY로 전환. shutdown 중 신규 호출은 즉시 fail-fast (timeout 대기 금지).
  • 2026-05-22: retry/DLQ vocabulary는 background-job-async-contract SSOT consume. 본 branch는 outbound-specific Resilience4j 도구 결정만 owns.

Work Item Contract

각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.

field required rule
Decision yes 구현자가 선택해야 하는 기본값
Allowed yes 허용되는 예외와 조건
Forbidden yes 절대 금지되는 구현/문서 상태
Required registry update conditional error/env/header/log/metric/capability 변경 시 필수
Required contract test yes 계약 위반 시 실패해야 하는 테스트
Failure condition yes review/build에서 실패로 판정할 상태
Canonical extraction target yes wiki/projects 승급 위치

Decisionized Work Items

item Decision Allowed Forbidden Required test
client Spring RestClient baseline WebClient extension doc provider SDK bypassing mapper adapter contract
retry Resilience4j disabled by default Spring Retry simple blocking retry all 4xx retry classification
circuit breaker Resilience4j optional env disabled local no metric when enabled metric assertion
body logging request/response body off allowlisted metadata only raw upstream body in log/response leakage test
redaction allowlist only provider-specific safe fields blacklist-only secret control redaction test
timeout connect=2s, read=5s, global call=10s per-endpoint override via capability registry timeout 미설정 또는 무한 timeout outbound client bean이 timeout 미설정으로 등록되면 fail
retry method scope idempotent (GET/HEAD/PUT/DELETE) default retry POST/PATCH는 idempotency key 헤더 있을 때만 non-idempotent blind retry retry method scope test
response size 10MB streaming threshold streaming for oversize in-memory load for >10MB response size streaming test

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 증거는 company-case-study 로 표기하며 공식 best practice 로 승격하지 않음.

Decision ID Decision Supporting Claims Evidence Strength Open Risk
D1 기본 outbound HTTP = Spring RestClient baseline (RestTemplate 회피, WebClient 는 extension) raw/official-docs/outbound-spring-restclient-baseline.md#RESTCLIENT-C1, raw/official-docs/outbound-spring-restclient-baseline.md#RESTCLIENT-C2, raw/official-docs/outbound-spring-restclient-baseline.md#RESTCLIENT-C3, raw/official-docs/outbound-spring-restclient-baseline.md#RESTCLIENT-C4, raw/official-docs/outbound-spring-restclient-baseline.md#RESTCLIENT-C6, raw/official-docs/outbound-spring-restclient-baseline.md#RESTCLIENT-C7 official-vendor-doc (Spring 7.0 RestTemplate deprecated, 6.1 NOTE: RestClient 가 sync 표준) Spring Boot 3.x 의 RestClient auto-configuration / RestClient.Builder bean 노출 확인 필요 (RESTCLIENT Usage Boundaries 참조)
D2 retry/circuit breaker 기본 라이브러리 = Resilience4j, Spring Retry 는 simple blocking retry 에만 예외 허용 raw/official-docs/outbound-resilience4j-vs-spring-retry.md#R4J-C1, raw/official-docs/outbound-resilience4j-vs-spring-retry.md#R4J-C2, raw/official-docs/outbound-resilience4j-vs-spring-retry.md#R4J-C3, raw/official-docs/outbound-resilience4j-vs-spring-retry.md#R4J-C4 official-vendor-doc (Resilience4j vendor 공식) + Spring Retry/Hystrix 비교는 R4J-C6 가 needs-confirmation 명시 Spring Retry README + Hystrix maintenance 상태 별도 source 보강 필요 (R4J-C6 negative finding)
D3 retry 기본값 = disabled, 활성화 시 retryable registry error + low-cardinality retry metric 필수 raw/official-docs/outbound-resilience4j-vs-spring-retry.md#R4J-C3 (retry 모듈 존재) + UNSUPPORTED 보조 (default disabled 정책은 ca-tmpl 자체 결정 — vendor 가 default disabled 권고 안 함) official-vendor-doc + UNSUPPORTED_DECISION (default 정책 자체는 자체 결정) retry-after header honor 가 Resilience4j Retry default 가 아니라는 점 — 별도 검증 필요
D4 circuit breaker metric tag scope = dependency.name, dependency.type, outcome 만 허용 (low-cardinality) raw/official-docs/resilience4j-micrometer-module.md#R4J-MICROMETER-C1 (Micrometer 모듈 지원), raw/official-docs/resilience4j-micrometer-module.md#R4J-MICROMETER-C2 (CircuitBreaker resilience4j.circuitbreaker.calls metric + kind/name default tag), raw/official-docs/resilience4j-micrometer-module.md#R4J-MICROMETER-C3 (resilience4j.circuitbreaker.state gauge + 5 state vocabulary) official-vendor-doc (Resilience4j vendor 공식 metric 명 + default tag 매핑 증거) — ca-tmpl 의 dependency.name/dependency.type/outcome 으로의 재매핑 자체 (MeterFilter 사용) 는 자체 정책이므로 vendor 가 권고하는 것은 아님 (default 는 kind/name) vendor default tag (kind/name) 를 ca-tmpl tag scope (dependency.name/dependency.type/outcome) 로 변환하는 MeterFilter 구현 + Prometheus scrape cardinality 측정 필요. metrics-alerting branch 와 cross-link. tag 표기 underscore 정합은 §Audit F2
D5 timeout default = connect 2s / read 5s / global call 10s, 미설정 또는 무한 timeout forbidden Mechanism (SUPPORTED): raw/official-docs/spring-restclient-builder-reference.md#SPRING-RESTCLIENT-REF-C1 (RestClient = synchronous + HTTP library 추상화), raw/official-docs/spring-restclient-builder-reference.md#SPRING-RESTCLIENT-REF-C2 (builder 옵션 — HTTP library 선택), raw/official-docs/spring-restclient-builder-reference.md#SPRING-RESTCLIENT-REF-C4 (5개 ClientRequestFactory 구현체 — JDK/Apache/Jetty/Reactor Netty/Simple). Quantitative (UNSUPPORTED_DECISION): connect 2s / read 5s / call 10s 정량 값은 cited official-doc 중 직접 인용 없음 — SPRING-RESTCLIENT-REF-C4 는 default timeout 값이 "본 인용 범위 밖" 임을 명시. 단 정량 값은 registry 계약으로 고정됨 (ca-tmpl/docs/registries/env-keys.yaml:489·503·516 — §구현 가이드 B) official-vendor-doc (mechanism — RequestFactory 추상화 + 5 구현체) + UNSUPPORTED_DECISION (정량 값 2s/5s/10s 는 SRE 운영 경험 기반, vendor 권고 부재) 각 RequestFactory 의 setConnectTimeout/setReadTimeout API 별 페이지 추가 보강 필요. 정량 값은 운영 측정 후 재검토 — 별도 source 없음. per-endpoint override 의 capability row 부재는 §Audit F3
D6 retry 분기 = idempotent method (GET/HEAD/PUT/DELETE) default retry, POST/PATCH 는 idempotency key 헤더 있을 때만 raw/official-docs/rfc9110-http-semantics.md#RFC9110-C1 (idempotent 정의 — PUT/DELETE + safe methods GET/HEAD/OPTIONS/TRACE 가 idempotent), raw/official-docs/rfc9110-http-semantics.md#RFC9110-C2 (client SHOULD NOT automatically retry non-idempotent method — POST/PATCH 자동 retry 금지의 normative 근거) + raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering.md#STRIPE-RL-C1, raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering.md#STRIPE-RL-C2 (industry case 보강) official-standard (RFC 9110 §9.2.2 — idempotent normative + client SHOULD NOT auto-retry non-idempotent) + company-case-study (Stripe 사례 보강, best practice 승격 금지) RFC9110-C1 의 "POST/PATCH 가 idempotent 가 아니라는 normative 진술은 본 인용에 직접 포함 안됨 (열거 부재가 의미상 함의됨)" — 추가 corroboration (RFC 9110 §9.2.1 safe methods enumeration) 권고. idempotency-key header 패턴 자체는 RFC 9110 가 표준화하지 않음 (application-level). outbound 방향 헤더 계약 부재는 §Audit F4
D7 response size limit default = 10MB streaming threshold, 초과 시 streaming 처리 의무 Mechanism (SUPPORTED): raw/official-docs/spring-restclient-builder-reference.md#SPRING-RESTCLIENT-REF-C5 (RestClient default 4xx/5xx → RestClientException throw + onStatus override — error path 추상화 존재). Quantitative (UNSUPPORTED_DECISION): 10MB 정량 임계값은 cited official-doc 중 직접 인용 없음. 단 10MB 는 registry 계약으로 고정됨 (env-keys.yaml APP_OUTBOUND_HTTP_RESPONSE_SIZE_LIMIT default 10MB — §구현 가이드 G) official-vendor-doc (mechanism — onStatus error handling + streaming API 추상화 존재) + UNSUPPORTED_DECISION (10MB 정량 임계값 vendor 권고 부재) RestClient streaming API (exchange(...) + ClientHttpResponse.getBody()) 의 별도 페이지 보강 필요. 10MB 정량 값은 자체 정책 — 별도 source 부재
D8 shutdown 중 retry suppression 의무 (ApplicationListener<ContextClosedEvent> 또는 동등 mechanism 으로 retry policy NO_RETRY 전환, 신규 호출 즉시 fail-fast) raw/official-docs/spring-smartlifecycle-reference.md#SPRING-SMARTLC-C2 (SmartLifecycle interface = Lifecycle + Phased 확장 + isAutoStartup() + stop(Runnable)), raw/official-docs/spring-smartlifecycle-reference.md#SPRING-SMARTLC-C3 (startup ascending / shutdown descending phase 순서 — retry-가능 컴포넌트를 outbound client 보다 먼저 stop 시킬 수 있는 phase 메커니즘 근거), raw/official-docs/spring-smartlifecycle-reference.md#SPRING-SMARTLC-C7 (stop(Runnable) async 시맨틱 + DefaultLifecycleProcessor phase-level timeout 대기 — graceful shutdown 의 정식 메커니즘) official-vendor-doc (Spring Framework SmartLifecycle 공식 mechanism — phase 순서 + graceful stop callback) — ca-tmpl 의 retry policy → NO_RETRY 전환 자체 (ContextClosedEvent listener 또는 SmartLifecycle.stop() 내부 구현) 는 자체 정책이며 Spring 이 권고하지는 않음 raw/branch-notes/feature-runtime-health-lifecycle-contract 와 cross-link 필요. SPRING-SMARTLC-C7 의 timeout default 값 (30s) 은 본 인용 범위 밖 — DefaultLifecycleProcessor.setTimeoutPerShutdownPhase 별도 검증. ContextClosedEvent listener vs SmartLifecycle.stop() 중 어느 쪽이 outbound client 에 적합한지 구현 결정 필요
D9 (대안 비교) OpenFeign declarative client 배제 raw/official-docs/outbound-openfeign-declarative-client.md#OPENFEIGN-C1, raw/official-docs/outbound-openfeign-declarative-client.md#OPENFEIGN-C2, raw/official-docs/outbound-openfeign-declarative-client.md#OPENFEIGN-C3 official-vendor-doc + OPENFEIGN-C5 가 negative finding (maintenance-only 상태는 본 페이지로 미증명) OpenFeign 배제의 1차 근거가 "maintenance-only" 라면 별도 source 보강 필수 (현재는 needs-confirmation). @HttpExchange 대체 가능성도 별도 검증 필요
D10 (대안 비교) WebClient 를 baseline 에서 배제, reactor event-loop blocking risk raw/official-docs/outbound-webclient-vs-restclient-spring.md#WEBCLIENT-C1, raw/official-docs/outbound-webclient-vs-restclient-spring.md#WEBCLIENT-C3, raw/official-docs/outbound-webclient-vs-restclient-spring.md#WEBCLIENT-C5, raw/official-docs/outbound-webclient-vs-restclient-spring.md#WEBCLIENT-C6 official-vendor-doc + WEBCLIENT-C7 가 negative finding (event loop deadlock 정확 문구는 본 페이지 미발견 — needs-confirmation) reactor scheduler / event loop deadlock 경고는 별도 출처 (Project Reactor 문서) 보강 필요
D11 (보강) Stripe rate limit + retry + idempotency-key 사례 — ca-tmpl default-disabled 의 보수성 vs Stripe default-enabled 대비 raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering.md#STRIPE-RL-C1, raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering.md#STRIPE-RL-C2, raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering.md#STRIPE-RL-C3, raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering.md#STRIPE-RL-C4 company-case-study (best practice 승격 금지 — Stripe 사례 한정) STRIPE-RL-C5 가 negative — 409/429/500/502/503/504 + idempotency-key 자동 + full jitter 0.5x~1.5x + 2-3 attempts 의 정확한 정책은 stripe-java SDK 코드 별도 확인 필요
D12 upstream 실패 분류 contract = DEPENDENCY_* 6종 (TIMEOUT 504/2s · CONNECT_FAILED 503/2s · DNS_FAILED 503/5s · 4XX_CLIENT 502 non-retryable · 5XX_SERVER 502/2s · CIRCUIT_OPEN 503/10s) project-decision — registry 계약으로 고정됨 (ca-tmpl/docs/registries/error-codes.yaml:636~711, 전 row owner_branch: feature-outbound-http-client-baseline, category TRANSIENT_DEPENDENCY/PERMANENT_DEPENDENCY — 2026-06-10 Tiered Extraction 인용 검증 PASS). 분류 체계 자체의 외부 표준 인용은 없음 project-decision + registry-ground-truth (계약 row 는 actually-implemented; OperationalError enum 6 constants + DependencyFailureExceptionactually-implemented + locally-verified 2026-06-11; OutboundHttpErrorMapperactually-implemented + locally-verified 2026-06-11 — OutboundHttpErrorMapperTest 19/19 PASS; web-layer GlobalExceptionHandler.handleDependencyFailure + RetryAfterAdvisor 5 dependency entries 는 actually-implemented + locally-verified 2026-06-11 — GlobalExceptionHandlerTest 7 new PASS + RetryAfterAdvisorTest 6 new PASS) 4XX 일괄 PERMANENT 분류는 408(Request Timeout)/429(Too Many Requests) 같은 의미상 retryable 4xx 엣지 미해결 (§엣지 — D12 Open Risk, documented)
D13 request/response body logging 금지 + allowlist 기반 redaction UNSUPPORTED_DECISION (외부 인용 없음 — OWASP Logging Cheat Sheet 등 보강 deferred, §9 funnel 계상). 단 부분 구현 실재: support/OutboundDependencyLogger 가 body/recipient/provider payload 를 시그니처 차원에서 받지 않음 — "PII cannot reach a log line by construction" (ca-tmpl/src/adapter-outbound/CLAUDE.md:18-21, src grep 2026-06-10). OutboundHttpDependencyLogger 도 동일 by-construction 계약 — 시그니처에 body/URI/payload 없음 (actually-implemented + locally-verified 2026-06-11 — OutboundHttpDependencyLoggerTest 8/8 PASS, failure_log_contains_only_exception_class_and_message_not_body PASS) UNSUPPORTED_DECISION (rationale) + actually-implemented + locally-verified (outbound HTTP 경로 포함) allowlist redaction 의 구체 필드 목록 미정의 — 외부 근거 (OWASP/vendor) 보강 후 확정 권고. §Audit F1 필드명 불일치는 OutboundHttpDependencyLogger 에서 registry 필드명(dependency_name 등)으로 해소됨

구현 가이드

R1(Trace 필수)·R2(UNSUPPORTED_IMPL_DECISION 라벨)·R3(범위 밖 이관) — CLAUDE.md §15.5. ca-tmpl ground truth 는 2026-06-10 Tiered Extraction(codex 발che, 인용 56/56 결정론 검증 PASS) + src/ grep 으로 확인.

현재 코드 상태 요약 (2026-06-11 Task 4 완료 이후): adapter-outbound/httpclient/ seam 완전 구현OutboundHttpClient(static baseline(...) factory), OutboundHttpErrorMapper(6 DEPENDENCY_* codes), OutboundHttpDependencyLogger(registry log fields, body-free), OutboundHttpTimeoutEnforcer(BeanPostProcessor), OutboundHttpShutdownGuard(SmartLifecycle), OutboundHttpResilienceConfig+OutboundHttpResilience+OutboundRetryPolicy (all actually-implemented + locally-verified 2026-06-11). 모든 Task 14 완료: application.yml app.outbound.http 블록, src/.env 6키, application-test.yml test defaults, verifyEnvKeys/verifyCleanArchitectureDependencies/CleanArchitectureTest/:app-bootstrap:test/test (full suite) ALL GREEN 2026-06-11.

1. Client 배치

Trace: D1 (RESTCLIENT-C1~C7) + D9/D10 (대안 배제).

  • UNSUPPORTED_IMPL_DECISION: client bean 명명·구성 단위(전역 1 bean vs dependency 별 bean)는 근거 raw 가 권고하지 않음 — trade-off: dependency 별 분리가 D4 metric tag(dependency_name) 주입과 D12 분류 주입에 단순.
항목 명세 등급
구현 위치 src/adapter-outbound/.../adapter/outbound/httpclient/ — CLAUDE.md 가 "external HTTP client seam (httpclient/, currently empty)" 로 예약 seam 예약 actually-implemented / 본체 planned
client 종류 Spring RestClient (sync). WebClient 는 extension 문서 전용 (D10), OpenFeign 배제 (D9) planned
선례 sample-portfolioRepoStatsPortClient 가 RestClient 사용 (sample 모듈 한정 — baseline 구현 아님, src grep 2026-06-10) 참고

2. Timeout 적용

Trace: D5 (SPRING-RESTCLIENT-REF-C1·C2·C4) + registry env-keys.yaml:489·503·516.

  • UNSUPPORTED_IMPL_DECISION: global call 10s 의 적용 지점(Resilience4j TimeLimiter vs 자체 wrapper)은 인용 근거 없음 — trade-off: TimeLimiter 가 D2 라이브러리 선택과 일관되고 metric 일원화.
항목 명세 등급
env 계약 APP_OUTBOUND_HTTP_CONNECT_TIMEOUT=2s(required) · APP_OUTBOUND_HTTP_READ_TIMEOUT=5s(required) · APP_OUTBOUND_HTTP_GLOBAL_CALL_TIMEOUT=10s(required) — owner_branch 본 branch registry actually-implemented
connect/read 적용 RestClient.Builder.requestFactory(...) + factory 별 setConnectTimeout/setReadTimeout (5종 ClientRequestFactory — C4) planned
미설정 차단 timeout 미설정 outbound client bean 등록 시 ApplicationContext 시작 실패 (Decisionized "timeout" Forbidden · Claims To Verify 행) — bean post-processor 검사 planned
per-endpoint override capability registry 등록 시에만 허용 (D5 Allowed) — capabilities.yaml 에 해당 row 부재 → 신규 제안 필요 (§Audit F3). 기존 값처럼 단정 금지 planned + 신규 제안

3. Retry / Circuit Breaker

Trace: D2 (R4J-C1~C4) · D3 (R4J-C3 + env-keys.yaml:531 주석) · D4 (R4J-MICROMETER-C1~C3 + metrics.yaml).

항목 명세 등급
env 계약 APP_OUTBOUND_HTTP_RETRY_ENABLED=false(optional) · APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED=false(optional) — env-keys.yaml:529·543 registry actually-implemented
라이브러리 Resilience4j (D2) — src·build.gradle grep 0건 (2026-06-10): 의존성 미추가 planned
metric 계약 resilience4j.retry.calls(log: dependency_name/outcome/retry_attempt) · resilience4j.circuitbreaker.state(dependency_name) · resilience4j.circuitbreaker.calls(dependency_name/outcome/duration_ms) — metrics.yaml:97·116~, owner_branch 본 branch registry actually-implemented
tag 재매핑 vendor default tag(kind/name) → dependency_name/dependency_type/outcome 은 MeterFilter (D4 — vendor 미권고 자체 정책). 표기 정합은 §Audit F2 planned
활성화 가드 retry enabled 인데 retryable registry error + low-cardinality metric 부재 → forbidden (D3) — enforcement 지점(기동 검사 vs 계약 테스트)은 미정 planned

4. Retry method scope

Trace: D6 (RFC9110-C1·C2 + STRIPE-RL-C1·C2).

항목 명세 등급
default retry 대상 GET/HEAD/PUT/DELETE (RFC 9110 idempotent) planned
POST/PATCH Idempotency-Key 헤더 동반 시에만 retry — headers.yaml:43 의 row 는 direction: inbound (owner: raw/branch-notes/feature-rate-limit-idempotency-contract) → outbound 첨부 계약 미정의 (§Audit F4) planned + cross-branch 협의

5. 실패 분류

Trace: D12 (error-codes.yaml:636~711 — 전 row owner_branch 본 branch).

  • UNSUPPORTED_IMPL_DECISION: mapper 클래스 명명·배치(httpclient/ 내부 vs support/)는 근거 없음 — trade-off: httpclient/ 내부가 RestClient 예외 타입(RestClientException 계열)과 응집. 채택: httpclient/OutboundHttpErrorMapper (actually-implemented + locally-verified 2026-06-11).

registry 계약 (row 는 actually-implemented, 매핑 코드는 actually-implemented + locally-verified 2026-06-11 — OutboundHttpErrorMapperTest 19/19 PASS; web-layer mapping 은 actually-implemented + locally-verified 2026-06-11 — Task 3 아래 참조):

code category HTTP retryable retry_after
DEPENDENCY_TIMEOUT TRANSIENT_DEPENDENCY 504 true 2s
DEPENDENCY_CONNECT_FAILED TRANSIENT_DEPENDENCY 503 true 2s
DEPENDENCY_DNS_FAILED TRANSIENT_DEPENDENCY 503 true 5s
DEPENDENCY_4XX_CLIENT PERMANENT_DEPENDENCY 502 false
DEPENDENCY_5XX_SERVER TRANSIENT_DEPENDENCY 502 true 2s
DEPENDENCY_CIRCUIT_OPEN TRANSIENT_DEPENDENCY 503 true 10s

6. Dependency 로그

Trace: D13 + metrics.yaml:90 (log_field_mapping) + adapter-outbound/CLAUDE.md:18-21.

항목 명세 등급
기존 구현 support/OutboundDependencyLoggerdependency/operation/outcome/correlationId 만 로깅, body·recipient·payload 는 시그니처가 받지 않음 (by construction) actually-implemented (notification 경로)
outbound HTTP 로그 필드 registry log_field_mapping = dependency_name/dependency_type/outcome/duration_ms — 기존 logger 필드와 불일치 (§Audit F1). RestClient 경로 구현 시 registry 필드명 채택 권고 planned
body 금지·redaction D13 — allowlist 구체 필드 목록 미정의 (외부 근거 보강 deferred) planned

7. Response size / streaming

Trace: D7 (SPRING-RESTCLIENT-REF-C5) + registry env-keys.yaml (APP_OUTBOUND_HTTP_RESPONSE_SIZE_LIMIT default 10MB, optional).

  • 10MB 초과 응답은 streaming 처리 의무 — RestClient streaming API (exchange(...) + ClientHttpResponse.getBody()) 의 공식 페이지 보강 필요 (D7 Open Risk). 전부 planned.

8. Shutdown retry suppression

Trace: D8 (SPRING-SMARTLC-C2·C3·C7).

  • UNSUPPORTED_IMPL_DECISION: ContextClosedEvent listener vs SmartLifecycle.stop() 선택은 근거 없음 — trade-off: SmartLifecycle 은 phase 순서로 retry-가능 컴포넌트를 outbound client 보다 먼저 정지 가능(C3), listener 는 구현 단순. raw/branch-notes/feature-runtime-health-lifecycle-contract 의 phase 배치와 협의 필수.
  • retry policy → NO_RETRY 전환 + shutdown 중 신규 호출 즉시 fail-fast (timeout 대기 금지). 전부 planned.

엣지·실패·의존

  • 실패·엣지 경로:
    • upstream timeout → DEPENDENCY_TIMEOUT 504 retryable(2s) — 테스트 계약 "retryable dependency failure 분류"와 일치 (D12).
    • connect/DNS 실패 → 503 retryable (각 2s/5s) — DNS 만 retry_after 5s 인 이유는 registry 에 명시 근거 없음 (해석).
    • upstream 4xx → DEPENDENCY_4XX_CLIENT 502 non-retryable — 408/429 가 4xx 이면서 의미상 retryable 인 엣지 미해결 (D12 Open Risk). 기대 동작 미정 → 구현 전 결정 필요.
    • circuit open → DEPENDENCY_CIRCUIT_OPEN 503 retry_after 10s — upstream 미호출 fail-fast.
    • shutdown 중 신규 outbound 호출 → 즉시 fail-fast, timeout 대기 금지 (D8). retry 진행 중 shutdown 시그널 수신 → NO_RETRY 전환.
    • 응답 >10MB → streaming 의무 (D7). in-memory 적재는 테스트 계약 위반.
    • POST/PATCH 에 Idempotency-Key 부재 → retry 금지 (D6). outbound 첨부 계약 자체가 미정의 (§Audit F4) — 정의 전까지 POST/PATCH retry 는 사실상 전면 금지가 안전 동작.
    • retry enabled + retryable registry/metric 미충족 → forbidden (D3) — enforcement 지점 미정 (§구현 가이드 3).
  • 다른 계약 의존:

테스트 계약

  • upstream timeout은 retryable dependency failure로 분류되어야 함.
  • upstream raw error body가 response/log에 노출되면 실패.
  • 401/403은 credential/scope/config 문제로 분류되어야 함.
  • outbound log에 dependency.name/type/duration_ms가 없으면 실패.
  • retry/circuit breaker enabled인데 Resilience4j metric과 retryable classification이 없으면 실패.
  • shutdown phase에서 outbound HTTP 호출이 retry를 시도하면 실패.

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.

Claim Why uncertain How to verify Status
ca-tmpl 의 connect 2s / read 5s / call 10s timeout 이 Spring RestClient JdkClientHttpRequestFactory 로 실제 적용 D5 UNSUPPORTED_DECISION — Spring Boot 3.x auto-configuration 의 default factory 확인 필요 RestClient.Builder.requestFactory(factory) + JdkClientHttpRequestFactory.setReadTimeout + JDK HttpClient.connectTimeout + OutboundHttpClientTest.t2_read_timeout_* PASS actually-verified 2026-06-11 (read timeout DEPENDENCY_TIMEOUT PASS)
outbound HTTP client bean 이 timeout 미설정으로 등록되면 ApplicationContext post-processor 가 fail timeout 미설정 검증 자체 메커니즘 미정의 OutboundHttpTimeoutEnforcer BeanPostProcessor — OutboundHttpTimeoutEnforcerTest 4/4 PASS actually-verified 2026-06-11
Resilience4j retry/circuit breaker metric 이 outcome tag 만 노출 (kind tag 제거) + CB state tag uppercase D4 — vendor default tag 는 kind/name, ca-tmpl 재매핑은 MeterFilter 자체 구현 필요 (vendor 미권고) custom MeterFilter.map() + filter-first order + OutboundHttpClientTest.t8/t9 PASS actually-verified 2026-06-11 (Micrometer 1.15.x requires custom map(), not replaceTagValues)
shutdown phase 에서 outbound HTTP 호출이 retry 를 시도하지 않음 (D8) D8 mechanism 은 SPRING-SMARTLC-C2/C3/C7 로 SUPPORTED, OutboundHttpShutdownGuard.stop() 로 flag set OutboundHttpClientTest.t10_shutdown_* PASS — stop() 후 호출 즉시 DEPENDENCY_CIRCUIT_OPEN + outcome="REJECTED", 서버 hit count 0 actually-verified 2026-06-11
WebClient 의 reactor event-loop blocking risk (D10 의 deadlock 가능성) WEBCLIENT-C7 negative — 정확 문구 미발견 Project Reactor 문서 fetch + 통합 테스트로 WebClient.block() in single-thread scheduler deadlock 재현 needs-confirmation
OpenFeign maintenance-only 상태 (D9 의 배제 정당화) OPENFEIGN-C5 negative — 본 페이지 미명시 spring-cloud-openfeign GitHub README + Spring blog announcement 별도 fetch needs-confirmation
upstream raw error body 가 response/log 에 노출되지 않음 DefaultResponseErrorHandler 의 4xx → HttpClientErrorException / 5xx → HttpServerErrorException 매핑 + error mapper 의 응답 sanitize grep + 통합 테스트로 upstream 500 응답 body 가 log/response 에 등장하지 않는지 확인 planned
401/403 이 credential/scope/config 문제로 정확히 분류 error mapper 분류 logic 자체 검증 필요 통합 테스트로 401 → AUTH_, 403 → AUTHZ_ 분류 확인 planned
Stripe 의 retry + idempotency-key 자동 첨부 정책이 ca-tmpl 의 POST/PATCH retry 정책과 정합 (D6) STRIPE-RL-C5 negative — 정확한 정책 미증명 stripe-java SDK StripeResponseGetter 코드 별도 확인 + ca-tmpl idempotency-key 정책 cross-link needs-confirmation

관심사 커버리지

coverage-auditor 자동 생성 (2026-06-10 — verdict: Covered, Blocking 0 / Should-fix 2 / Advisory 2).

governing_docs: wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound (§ Outbound HTTP documented-only + hub §11 Outbound HTTP + §32.3)

관심사 상태 owner 심각도 근거
RestClient baseline 채택 (RestTemplate 회피 / WebClient extension 분리 / OpenFeign 배제) covered-here D1 / D9 / D10
upstream 실패 분류 6종 (TIMEOUT/CONNECT_FAILED/DNS_FAILED/4XX_CLIENT/5XX_SERVER/CIRCUIT_OPEN) covered-here D12; error-codes.yaml:636~711
timeout 3계층 (connect 2s / read 5s / global call 10s) + 미설정 forbidden covered-here D5; env-keys.yaml:489/503/516
retry/CB 라이브러리 = Resilience4j, default disabled covered-here D2 / D3; env-keys.yaml:529/543
retry method scope (idempotent default / POST·PATCH idempotency-key 조건부) covered-here D6; §구현 가이드 4
response size limit (10MB streaming threshold) covered-here D7; env-keys.yaml:557
shutdown 중 retry suppression (NO_RETRY 전환 + fail-fast) covered-here D8; §구현 가이드 8
dependency log field (dependency_name/type/outcome/duration_ms) covered-here D13; metrics.yaml:90 log_field_mapping
request/response body logging 금지 + allowlist redaction covered-here D13; adapter-outbound/CLAUDE.md:18-21
Resilience4j metric 3종 + low-cardinality tag scope covered-here D4; metrics.yaml:97/116/136
dependency.client.requests timer delegated raw/branch-notes/feature-metrics-alerting-contract OK metrics.yaml:71
EXTERNAL_OUTBOUND_ALLOWED capability gate delegated raw/branch-notes/feature-repository-access-permission-contract OK capabilities.yaml:92; adapter-outbound/CLAUDE.md:49-52
Idempotency-Key header (inbound row 소유 + outbound row 미정의 gap) delegated raw/branch-notes/feature-rate-limit-idempotency-contract Should-fix (HEADER_DIRECTION_GAP) headers.yaml:43 direction:inbound; §Audit F4
per-endpoint timeout override capability row 신설 delegated raw/branch-notes/feature-repository-access-permission-contract Should-fix (CAPABILITY_ROW_ABSENT) §Audit F3
env key 검증 스키마 delegated raw/branch-notes/feature-env-driven-runtime-configuration Advisory §다른 계약 의존
retry/DLQ vocabulary SSOT delegated raw/branch-notes/feature-background-job-async-contract Advisory 결정 사항 2026-05-22
shutdown phase 순서 협의 delegated raw/branch-notes/feature-runtime-health-lifecycle-contract Advisory §구현 가이드 8; §다른 계약 의존

Audit & Findings (2026-06-10 ground-truth 정합 감사)

/branch-spec 실행 시 ca-tmpl registry·코드 대조 결과 (Tiered Extraction codex 발췌 56/56 인용 검증 + src grep). 사용자 결정 영역은 rewrite 하지 않고 정합 권고만 기록.

# Finding 내용 권고
F1 LOG_FIELD_DRIFT 로그 필드 3원 불일치 — 본 노트 테스트 계약 dependency.name/type/duration_msmetrics.yaml:90 log_field_mapping [dependency_name, dependency_type, outcome, duration_ms] ↔ 코드 OutboundDependencyLogger 실 출력 dependency/operation/outcome/correlationId (duration 부재) RestClient 경로 구현 시 registry 필드명(dependency_name 등) 채택. 기존 logger 는 notification adapter 용 — outbound HTTP 전용 로깅은 별도 구현
F2 TAG_NAME_DRIFT D4·결정 사항의 tag 표기 dependency.name/dependency.type(dot) vs metrics.yaml:71~ 실제 tag dependency_name/dependency_type(underscore) registry 가 계약 SSOT — 노트 표기의 underscore 정합 권고 (사용자 결정 영역 — 자동 rewrite 안 함)
F3 CAPABILITY_ROW_ABSENT D5 Allowed "per-endpoint override 는 capability registry 등록 시에만" — capabilities.yaml 에 timeout-override capability row 부재 (현재 outbound 관련 row 는 EXTERNAL_OUTBOUND_ALLOWED 뿐) 신규 row 제안 필요 — capability vocabulary owner 인 raw/branch-notes/feature-repository-access-permission-contract 와 협의
F4 HEADER_DIRECTION_GAP D6 의 outbound Idempotency-Key 첨부 vs headers.yaml:43direction: inbound 만 정의 outbound row 신설 또는 direction 확장 — header owner raw/branch-notes/feature-rate-limit-idempotency-contract 와 협의. 정의 전까지 POST/PATCH retry 전면 금지가 안전 동작

마주친 문제

Task 2b (2026-06-11) — OutboundHttpClientTest 작성 중 발견된 production bug 4건

  1. t3 connect-refused 포트 획득 방법: HttpServer.create().stop(0) 는 포트를 TIME_WAIT 상태로 남겨 즉시 ConnectException 대신 TIMEOUT 발생. 해결: ServerSocket(0)close() 패턴으로 교체.

  2. DNS failure 분류 오류 (OutboundHttpErrorMapper bug): JDK 21 HttpClient 는 DNS 실패를 ConnectException(cause=ConnectException(cause=UnresolvedAddressException)) 으로 래핑. 기존 단일 패스 cause-chain walk 에서 ConnectException 이 먼저 매칭되어 DEPENDENCY_DNS_FAILED 대신 DEPENDENCY_CONNECT_FAILED 반환. 수정: ConnectException 매칭 시 hasDnsCauseInChain() 로 서브 체인을 추가 스캔, DNS 근원 발견 시 DEPENDENCY_DNS_FAILED 우선 반환.

  3. retry 미작동 (공유 인스턴스 계약 위반): OutboundHttpResilience 내부의 RetryretryPolicy::shouldRetryretryOnException predicate 로 등록. shouldRetryretryPolicy.beginCall() 로 세팅된 ThreadLocal context 를 확인. 테스트에서 retryPolicy(settings) 를 두 번 호출하면 서로 다른 인스턴스 → shouldRetry 가 항상 null context → return false → retry 0회. 해결: sharedPolicy 변수 하나로 resilience 와 client 에 동일 인스턴스 전달.

  4. MeterFilter ordering 및 replaceTagValues 호환성 문제 (OutboundHttpResilienceConfig bug — 2건):

    • TaggedCircuitBreakerMetrics.bindTo() 가 state gauge 를 eager 등록 → 이후 filter 설치 → map() 미호출 → uppercase 미적용. 수정: applyMeterFilters()bindTo() BEFORE 로 이동.
    • Micrometer 1.15.x 에서 MeterFilter.replaceTagValues() / renameTag()FunctionCounter / DefaultGauge 에 대해 map() 를 신뢰성 있게 호출하지 않음 (Micrometer 1.15.11 + Resilience4j 2.2.0 로컬 검증). 수정: 각 meter 에 대해 tag iteration + id.replaceTags() 를 직접 수행하는 custom MeterFilter 3개로 교체.

후속 리팩터

  • 2026-06-16 OutboundHttpClient orchestration 분리 (god-object 초입 완화, behavior-preserving): 리뷰가 "client 가 생성+관측+분류 정책을 모두 들고 있어 god object 초입"이라 지적 → 사용자 지시로 과분할 금지, 딱 2개만 추출. SDD 루프(ca-implementer + 머신 검증)로 수행.

    • OutboundHttpRestClientFactory (package-private, static Clients create(name, baseUrl, settings) + nested record Clients(buffered, streaming)): 단일 공유 JdkClientHttpRequestFactory + 두 RestClient(buffered=Trace+SizeBounding, streaming=Trace) 생성을 생성자에서 추출. 변경-이유 축 = timeout 적용/interceptor 조립/RestClient 구현체. static 메서드라 B7 제외, 빈 아님(timeout enforcer 는 RestClient 만 금지).
    • OutboundHttpCallObserver (package-private): duration 계산 + success/failure 로그 + outcomeFor(DFE→outcome) + shutdown-rejection 생성 흡수. recordSuccess / recordFailure(...)→DFE / rejectShutdown(msg)→DFE (호출부는 throw observer.recordFailure(...) 로 throw 가시성 유지). 메서드 package-private 라 B7 제외.
    • OutboundHttpClient 는 shutdown 체크 → deadline/retryPolicy → buildSupplier → CB/retry 데코레이션 → 실행으로 슬림화. baseline(...) 8-param 시그니처 불변(포크/테스트 호환). size-exception 비분류 전파는 client 에 잔류.
    • 동작 무변경 보존: 로그 필드·retryAttempt=max(0,n-1)·stream=0·REJECTED(0,0)·공유 request factory·interceptor 순서 모두 동일. 검증: :adapter-outbound:test 190/190, :app-bootstrap:test --tests '*CleanArchitectureTest' 49/49 PASS (B7·의존방향 위반 0).
    • 컨트롤러 개입: implementer 가 범위 밖 OutboundHttpDependencyLogger 의 PII-safety(D13) JavaDoc 2블록을 삭제 → 문서 회귀로 판단해 git checkout HEAD 로 되돌림. 신규 main 2개·test 2개만 잔류.
    • 보류(동일 리뷰의 나머지): DependencyLogFields 공통 helper(②), TraceContextPropagationInterceptor FORK LANDMINE 주석 이관(④) — 사유는 raw/branch-notes/feature-integration-adapter-templates 2026-06-16 rename 항목과 동일(②는 효익 적음, ④는 in-file 유지가 안전).
  • 2026-06-16 httpclient 관심사별 서브패키지화 (하이브리드 C, behavior-preserving): 리뷰가 "13개 한 폴더 → 관심사 폴더로(execution/transport/resilience/diagnostics)" 제안. package-private 캡슐화를 깨지 않는 하이브리드 C로 진행 — package-private 묶음(OutboundHttpClient+OutboundHttpRestClientFactory+OutboundHttpCallObserver+ResponseSizeBoundingInterceptor)은 root 유지, 이미 public·독립적인 쌍만 분리: httpclient/resilience/(OutboundHttpResilience,OutboundHttpResilienceConfig) + httpclient/diagnostics/(OutboundHttpDependencyLogger,OutboundHttpErrorMapper). 전체 5분할(B) 미채택 근거: observer/factory를 public으로 올려야 해 직전 캡슐화를 되돌림 + 기존 outbound 서브패키징이 "백엔드별"(cache/redis 등)이라 "관심사별"은 축 불일치(스켈레톤 가독성).

    • 가드레일 무영향: ArchUnit 규칙 전부 ..adapter.outbound.. 재귀 패턴 + .adapter.outbound. substring 체크(CleanArchitectureTest:436)라 서브패키지 자동 커버 → Prime Directive "서브패키지 추가 시 규칙 확장" 불필요. Gradle 매트릭스는 모듈 단위라 무관.
    • 이동 main 4 + test 4, package 선언 + import 정정(컴파일러 주도). app-bootstrap MetricsContractConfig FQN javadoc 2곳(@see/{@code})을 .resilience. 로 갱신. 검증: httpclient 스코프 테스트 119/119 PASS, CleanArchitectureTest 49/49 PASS, 모듈 컴파일 0 에러.
    • 사고(tooling): test-file import 삽입 sed\&(리터럴 앰퍼샌드) 버그로 5개 test 파일 1행 package 선언을 &로 덮음(gradle-runner 포착) → 복구 후 재검증 green. 교훈: sed replacement 에서 매치 텍스트 보존은 비이스케이프 &, \& 는 리터럴 &.
    • 컨텍스트: 동시점에 사용자가 messaging/notification 을 core/ 서브패키지로 병행 리팩터 중 — 본 작업은 httpclient 에만 한정, messaging/notification 미접촉.

묶음 (이 branch에서 파생된 자료)

2026-06-11 Phase C2 실 구현 완료 — 파생 error note 3건 생성 (아래 wikilink). interview/blog 시드는 인라인 보관, standalone 추출은 canonical 요청 시.

오류 기록 (본 feature 작업 중 발생)

  • raw/errors/jdk-httpclient-dns-unresolvedaddress-connectexception-2026-06-11DNS 분류 오류 (OutboundHttpErrorMapper): JDK 21 HttpClient DNS failure → ConnectException 래핑 패턴이 단일 패스 cause-chain walk 를 뚫고 지나감. 해결 → hasDnsCauseInChain() helper 로 서브 체인 추가 스캔. actually-fixed + locally-verified 2026-06-11.
  • raw/errors/micrometer-meterfilter-replacetagvalues-functioncounter-2026-06-11MeterFilter ordering + replaceTagValues compat (OutboundHttpResilienceConfig): eager gauge 등록 전 filter 적용 + Micrometer 1.15.x FunctionCounter/DefaultGauge 에서 replaceTagValues/renameTag 미적용. 해결 → filter-first 순서 + custom MeterFilter.map(). actually-fixed + locally-verified 2026-06-11.
  • raw/errors/spring-configuration-bean-factory-method-not-processed-2026-06-11@Configuration 팩토리 등록 함정 (테스트 배선): @Configuration 클래스를 다른 구성 클래스의 @Bean 팩토리 반환값으로 등록하면 내부 @Bean 정의가 처리되지 않아 D3 기동-실패 테스트가 false-green. 해결 → withUserConfiguration(...) 직접 등록. actually-fixed + locally-verified 2026-06-11.
  • 공유 retryPolicy 인스턴스 계약: resilience 와 client 에 동일 OutboundRetryPolicy 인스턴스를 전달해야 ThreadLocal context 공유 가능. actually-documented + locally-verified 2026-06-11. (단독 error note 불요 — 설계 계약으로 §Task 2b 기록에 보존)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • JDK HttpClient 가 DNS 실패를 ConnectException 으로 래핑하는 이유와 cause-chain walk 기반 분류 전략의 우선순위 문제.
  • Micrometer MeterFilter.map() 호출 시점 (meter 등록 시점 한정) 과 eager vs lazy 등록 패턴 (FunctionCounter = lazy, DefaultGauge = eager) 의 차이 — filter-first 순서 중요성.
  • replaceTagValues 와 custom MeterFilter 의 차이 및 FunctionCounter 에서 발생하는 호환성 문제.
  • ThreadLocal 기반 call context (OutboundRetryPolicy) 를 공유 인스턴스로 주입해야 하는 이유.

Blog topics

  • "JDK HttpClient DNS failure classification: why UnresolvedAddressException hides inside ConnectException and how to handle it robustly" — cause-chain walk 전략 + 우선순위 처리.
  • "Micrometer MeterFilter gotcha with Resilience4j: why replaceTagValues silently fails on FunctionCounter in 1.15.x" — filter-first 순서 + custom map() 필요성.

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 nav 유지.

  • raw/daily-notes/2026-06-11 (파일 미생성 — 일일 노트는 별도 생성)
  • 2026-06-16 — OutboundHttpClient orchestration 분리 후속 리팩터(동작 무변경). 위 §후속 리팩터 참조.

Task 4 — Bootstrap wiring (2026-06-11 완료)

Mode: bootstrap-only (+ settings files .env, adapter-outbound/CLAUDE.md)

파일 변경 내용 상태
src/app-bootstrap/src/main/resources/application.yml app.outbound.http 블록 추가 (3 required + 3 optional with defaults; feature-outbound-http-client-baseline D5/D3/D7 header comment) actually-implemented
src/.env APP_OUTBOUND_HTTP_* 6종 추가 (CONNECT_TIMEOUT=2s, READ_TIMEOUT=5s, GLOBAL_CALL_TIMEOUT=10s, RETRY_ENABLED=false, CIRCUIT_BREAKER_ENABLED=false, RESPONSE_SIZE_LIMIT=10MB) actually-implemented
src/adapter-outbound/CLAUDE.md Responsibility bullet 업데이트 (httpclient/ 구현 설명 + Allowed 목록에 spring-web/micrometer-core/resilience4j 추가) actually-implemented
src/app-bootstrap/src/test/resources/application-test.yml app.outbound.http test defaults 추가 (OutboundHttpSettings requires 3 non-zero timeouts; @ConfigurationPropertiesScan via CaSkeletonApplication picks it up in any full-context test) actually-implemented

검증 결과 (2026-06-11):

  • ./gradlew verifyEnvKeys → OK — 81 env keys, 73 required placeholders covered, 68 APP_ keys registered.
  • ./gradlew verifyCleanArchitectureDependencies → BUILD SUCCESSFUL
  • ./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' → BUILD SUCCESSFUL
  • ./gradlew :app-bootstrap:test → BUILD SUCCESSFUL
  • ./gradlew test (full suite) → BUILD SUCCESSFUL

주의: Spring Boot의 RestClientAutoConfiguration이 prototype-scoped RestClient.Builder bean을 자동등록하나, prototype beans는 BeanPostProcessor.postProcessAfterInitialization에서 인스턴스화 온디맨드이므로 OutboundHttpTimeoutEnforcer가 이를 트립하지 않는다 — 실제로 전체 suite 통과로 확인.

완료 후 정리

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목: application.yml app.outbound.http, src/.env 6종, OutboundHttpClient baseline factory, OutboundHttpErrorMapper 6 codes, OutboundHttpDependencyLogger, OutboundHttpTimeoutEnforcer, OutboundHttpShutdownGuard, OutboundHttpResilienceConfig
    • locally-verified 항목: timeout enforcement, DNS classification fix, MeterFilter ordering fix, retry shared-instance contract
    • prod-verified 항목: (없음 — 아직 prod 배포 미완)
  • 추출하지 않을 항목 (planned / documented-only / abandoned):
    • per-endpoint timeout override capability row (CAPABILITY_ROW_ABSENT — F3)
    • outbound Idempotency-Key header row (HEADER_DIRECTION_GAP — F4)