--- kind: CONCEPT slug: adapter-outbound-httpclient-c01 title: 닿지 않는 설정을 무시하지 않고 거부한다 topic: admission-budget-and-backpressure project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: concept:adapter-outbound-httpclient-c01 evidenceCapturedOn: 2026-09-01 assets: - key: adapter-outbound-httpclient-c01 file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c01.svg evidence: - ../../../final/evidence/raw/adapter-outbound-httpclient-c01.txt source: - 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L90 이다. module: adapter-outbound-httpclient --- # 닿지 않는 설정을 무시하지 않고 거부한다 `ClientProfileValidator`는 바인딩은 되지만 어떤 전송에도 닿지 않는 설정을 무시하지 않고 거부한다. 앞선 열 개 모듈에서 반복해 발견한 "선언되었으나 아무것도 하지 않는 설정" 패턴을 이 모듈은 명시적 거부로 처리한다. ## 본문 이 저장소에서 본 가장 조밀한 설정 검증기다. `validate(profile, environment)`가 11개 검사 그룹을 돌리고 결과를 정렬해 "a configuration error reports deterministically across runs and machines"를 보장한다. ## 소비자가 없던 설정을 거부로 바꾼 이유 특히 이 leaf에서만 보이는 태도가 하나 있다 — **바인딩은 되지만 어떤 전송에도 닿지 않는 설정을 무시하지 않고 거부한다.** > "Three of them had no consumer anywhere: `timeout.dns`, `proxy.credential-provider` and `proxy.import-ambient-no-proxy`. An operator who set a DNS timeout believed resolution was bounded and it was not; one who named a proxy credential provider believed the proxy was authenticated and it was not… **the honest position is to refuse a value the platform cannot honour instead of accepting it and doing nothing.**" 기본값은 통과시키므로 "only a deliberate, unmet request fails"다. 같은 논리가 관측 설정에도 적용된다 — `full-url-recording`은 아무도 읽지 않았고 `body-logging`은 actuator 보고에만 닿았다. "Leaving them that way is the worse of the two failure modes — an operator who set them believed the platform was recording full URLs or bodies, and an operator who left them false had no assurance that it was not." 지금은 production에서 둘 다 거부된다. ## ClientProfileValidator 참조 위치 :::evidence key="adapter-outbound-httpclient-c01" alt="코드베이스에서 ClientProfileValidator 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ClientProfileValidator 코드베이스 검색 — 8줄 · exit 0" zoom="true" ::: ## 나머지 검사가 막는 조용한 다운그레이드 - **`REACTIVE_REDIRECT_UNSUPPORTED`** — 엔진 리다이렉트는 모든 전송에서 꺼져 있고 hop별 재검증을 하는 coordinator는 블로킹 스택에만 있다. 리액티브 프로파일이 redirect를 켜면 "the caller received the 302 as an ordinary response and read its empty body as the answer." 거부가 정직한 결과다 — "a configured guarantee that silently does nothing is worse than one the platform declines to offer." - **`HTTP2_REQUIRED_TRANSPORT_UNSUPPORTED`** — `ProtocolIntent`가 "H2를 선호"와 "H2를 요구"를 구분한다. JDK 클라이언트는 `HTTP_2`를 선호로 다뤄 조용히 HTTP/1.1로 협상하고 Apache classic은 HTTP/1.1 전용이라, `HTTP_2`만 선언한 프로파일이 "ran happily over HTTP/1.1, and nothing anywhere said so." - **`TLS_PROTOCOL_SET_REQUIRED`** — 빈 집합이 통과하면 JVM 기본값이 선택되어, "a profile that meant to pin a TLS floor got whatever the platform default happened to be." - **`DYNAMIC_TARGET_PROXY_UNSUPPORTED`** — 포워드 프록시는 호스트명을 자기 쪽에서 다시 해석하므로 "The SSRF defence would be present, correct, and bypassed." - **`RETRY_POLICY_CONTRADICTS_ATTEMPTS`** — `policy`를 실행 경로에서 아무도 읽지 않아 "the actuator could report `retryPolicy: none` for a profile that was retrying three times." ## 이 검증기는 실제로 조립돼 있다 `app-bootstrap`의 `HttpClientStartupValidator:37`이 이 검증기를 생성한다(`168-...` §8.1). 이 leaf는 앞선 cache-redis와 달리 **실제로 조립돼 있다** — app-bootstrap에 이 leaf를 위한 auto-configuration 12개가 있다.