The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
4.4 KiB
Markdown
57 lines
4.4 KiB
Markdown
---
|
|
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`는 바인딩은 되지만 어떤 전송에도 닿지 않는 설정을 무시하지 않고 거부한다. 앞선 열 개 모듈에서 반복해 발견한 "선언되었으나 아무것도 하지 않는 설정" 패턴을 이 모듈은 명시적 거부로 처리한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
이 저장소에서 본 가장 조밀한 설정 검증기다. `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개가 있다.
|
|
|
|
<!-- body:end -->
|