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>
12 KiB
/shared/codebase/clean-architecture-backend-template/src/grpc-advanced/grpc-advanced-compat/src/main/resources/envoy/envoy.yaml
grpc-advanced-compat 완전 해부
상태: COMPLETE 재오픈 게이트: cycle 2 —
src/mainproduction 17파일 축자 통독 완료.STRUCTURAL_ONLY잔여 없음. 기준 revision:21234e38cdb9a926cbc92bb97a2aee2e4a7d2916분석 범위:src/grpc-advanced/grpc-advanced-compatSSOT owner:grpc-advanced-compatintegration/family document:analysis/20-grpc-platform.md(secondary, INTEGRATION_ONLY)
0. SSOT identity / 커버리지
runtime_memberships:[]— build-only- 다섯 다리: gRPC-Web · Servlet · Spring Integration · Reactor · Kotlin
| 패키지 | 파일 | LOC |
|---|---|---|
web |
4 | 220 |
kotlin |
3 | 164 |
servlet |
3 | 152 |
reactor |
4 | 233 |
integration |
3 | 193 |
Coverage ledger
| scope | count | disposition | reason |
|---|---|---|---|
main/java/** |
17 | FULL_READ |
962줄 전 본문 |
main/resources/envoy/envoy.yaml |
1 | FULL_READ |
70줄 전문. 참조 프록시 설정 — 이전 판의 ledger 에 아예 없었다 |
test/java/** |
5 | FULL_READ |
523줄 |
build.gradle |
1 | FULL_READ |
전문 |
gradle.lockfile |
1 | STRUCTURAL_ONLY |
잠금 파일 |
UNCLASSIFIED 0.
1. 모듈의 정체와 코틀린 레인의 처리
// build.gradle:6-10
// No Kotlin source set (adaptation D7): this repository has no Kotlin toolchain, so the Kotlin lane
// is expressed as a Java-side boundary contract whose compatibility gate fails closed until a real
// toolchain lane exists. Everything the gate would otherwise assert — one schema source, coroutine
// cancellation propagation, Flow backpressure inside the Stable buffer limits, evidence type
// preservation — is a checkable contract without it.
그리고 게이트가 그 판단을 코드로 반복한다.
"Fails closed in this repository, and says so rather than reporting a pass it cannot justify… a gate that reported success anyway would put an unverified claim in the support matrix."
blockers 는 다섯 항목을 낸다. 넷은 프로파일에서 확인 가능하고, 다섯째가 툴체인 레인 부재다. supportableHere() 는 상수 거짓이다.
이 처리가 이 저장소의 다른 곳(grpc-advanced-diagnostics 의 인프라 테스트킷 계약)과 같은 원칙이다 — 인프라 없이 도는 묶음은 통과하고 아무것도 세우지 않는다.
2. 다리마다 무엇을 거절하는가
| 다리 | 거절 |
|---|---|
| gRPC-Web | 브라우저에 노출된 메서드 중 gRPC-Web 이 나를 수 없는 RPC 종류 |
| Servlet | 컨테이너가 제공하지 않는 전송 설정 요구 |
| Kotlin | 다섯 블로커(툴체인 레인 포함) |
| Spring Integration | 변환기 없는 다리, 허용 목록 밖 헤더 |
| Reactor | (§17.2) |
gRPC-Web 의 근거:
"Two schemas — one for browsers, one for services — is how a field ends up meaning something different depending on which client asked, and the divergence is only visible to whoever reads both files."
Servlet 의 근거:
"Refuses rather than warns, because the setting would otherwise be accepted and ignored. A keepalive configured on a Servlet deployment does nothing, the connections behave as the container decides, and the investigation starts from the assumption that the setting is in force."
그리고 Servlet 실행이 Netty 인증을 대신할 수 없다는 것을 상수로 못박는다.
3. Spring Integration 다리가 무엇을 약속하지 않는가
"A Spring Integration
Messageaccumulates headers as it moves through a flow — routing keys, correlation ids, errors channels, whatever a transformer added — and copying them onto gRPC metadata sends a service's internal plumbing across the network."
"The bridge does not add durability. Spring Integration channels can look like a broker, and a bridge that implied acknowledgement or redelivery semantics would be promising something gRPC does not do."
변환기가 없는 다리는 생성자가 거부한다 — 반사에 맡기는 것이 예상 밖 타입이 유선에 닿는 경로다.
12. negative-space probes
12.1 도달성. Advanced 가족이므로 배선 경로가 없다. 그 위에 이 리프에는 테스트조차 없는 타입이 둘 있다(§17.2).
12.2 대조군 — 메타데이터 경로 둘. grpc-client 의 GrpcClientMetadataPolicy.materialize 는 허용 목록으로 거른 뒤 budget.check(accepted) 를 부른다. 이 리프의 GrpcIntegrationBridgePolicy.metadataFrom 은 허용 목록으로 거르고 예산을 부르지 않는다(§17.1).
12.3 리프 전체의 외부 참조가 0 이다. 재통독에서 다섯 패키지를 각각 확인했다.
dev.caskeleton.grpc.advanced.{web, servlet, integration, reactor, kotlin} → 리프 밖 참조 0
같은 Advanced 가족의 grpc-advanced-bootstrap 조차 이 리프의 타입을 하나도 부르지 않는다. Advanced 는 기능 플래그로 도달한다는 것이 이 가족의 규약인데, 그 플래그가 가리킬 대상이 배선되어 있지 않다.
12.4 드리프트. build.gradle 이 서술한 다섯 다리와 코틀린 레인의 처리 방식이 코드와 일치한다. 다만 ledger 가 main/resources 를 세지 않고 있었다(§17.3 의 재료가 거기 있다).
16. 확인하지 못한 것
- 실제 브라우저·프록시·서블릿 컨테이너로 어떤 다리도 돌리지 않았다. 그 인프라가 필요하다는 것이 이 가족의 기록이다.
- 코틀린 툴체인이 없으므로 코틀린 계약 넷을 실행으로 확인할 수 없다.
17. 손볼 것
17.1 P3 — 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다
public Map<GrpcMetadataKey, String> metadataFrom(Map<String, Object> messageHeaders) {
Map<GrpcMetadataKey, String> metadata = new LinkedHashMap<>();
headerAllowlist.forEach(key -> {
Object value = messageHeaders.get(key.name());
if (value != null) { metadata.put(key, String.valueOf(value)); }
});
return Map.copyOf(metadata);
}
허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다.
클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget."
그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다.
// GrpcClientMetadataPolicy.materialize
proposed.forEach((key, value) -> { if (allowed.contains(key) && value != null) accepted.put(key, value); });
budget.check(accepted); // ← 이 줄이 이 다리에는 없다
String.valueOf(value) 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다. Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어가므로 값 하나가 클 수 있다.
수정은 이 record 에 GrpcMetadataBudget 를 성분으로 추가하고 metadataFrom 끝에서 검사하는 것이다. 형태가 이미 옆 리프에 있다.
17.2 P3 — 반응형 표면 두 타입은 테스트조차 없다
ReactiveGrpcClient 저장소 전체에서 등장하는 파일 1개 (자기 자신)
ReactiveGrpcServerAdapter 저장소 전체에서 등장하는 파일 1개 (자기 자신)
이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 GrpcReactorCancellationBridge 는 2개 파일, GrpcReactorContextBridge 는 4개 파일에 등장한다.
두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다. 어긋나는 것은 검증이다. 채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다.
ReactiveGrpcClient 의 javadoc 이 "Exposes a unary call as a Mono and a server stream as a Flux" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다. 같은 리프의 GrpcReactorCancellationBridge 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다.
17.3 P3 — 저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다
이 리프에는 두 가지가 함께 있다.
GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins)— 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드.src/main/resources/envoy/envoy.yaml— 그 설정의 참조 구현.
그리고 설정 파일 자신이 그 관계를 주장한다.
# Shipped as a resource rather than as documentation prose because GrpcWebProxyContract asserts
# against it: the CORS allowlist, the exposed trailer headers and the TLS termination are the three
# things a browser client silently fails without, and a contract nobody checks is a contract that
# drifts from whatever is actually deployed.
GrpcWebProxyContract 는 이 파일에 대해 아무것도 단언하지 않는다. 판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다.
// GrpcWebCompatibilityGateTest
assertThat(GrpcWebProxyContract.violations(profile, requiredExposedHeaders(), Set.of("*"))) // ← 리터럴
.anySatisfy(v -> assertThat(v).contains("defeats the profile's allowlist"));
…
String envoy = resource("envoy/envoy.yaml");
assertThat(envoy)
.contains("expose_headers: \"grpc-status,grpc-message") // ← 부분 문자열
.contains("exact: \"https://app.example.com\"");
그래서 참조 설정이 grpc-status 를 노출하는지는 문자열이 확인하고, 그 노출이 충분한지 는 requiredExposedHeaders() 가 정의하는데, 둘을 잇는 코드가 없다. 필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다.
이 리프의 다른 판정기들과 다른 점은 재료가 이미 저장소에 있다는 것이다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다.
수정은 시험이 envoy.yaml 의 expose_headers 와 allow_origin(exact:)을 뽑아 GrpcWebProxyContract.violations 에 넣고 비어 있음을 단언하는 것이다. 그러면 참조 설정과 계약이 한 곳에서 함께 움직인다.
확인된 설계(문제 아님)
- 코틀린 게이트가 닫힌 실패를 하고 그 이유를 말하는 것 — 정당화할 수 없는 통과를 보고하지 않는다.
- 코틀린 계약 넷을 툴체인 없이도 확인 가능하게 만든 것 — 나중에 필요한 것은 레인 추가이지 계약 작성이 아니다.
- 브라우저와 기본 클라이언트가 한 스키마를 쓰게 한 것.
- Servlet 이 제공하지 않는 설정을 경고가 아니라 거절로 다룬 것.
- Servlet 실행이 Netty 인증을 대신하지 못한다고 못박은 것.
- 통합 다리가 브로커 의미론을 약속하지 않는다고 상수로 밝힌 것.
- 변환기 없는 다리를 생성자가 거부한 것.
- 다리가 생성된 스텁·서비스 API 를 대체하지 않는다고 밝힌 것.
Source anchors
src/grpc-advanced/grpc-advanced-compat/build.gradle:1-14
main/java/…/kotlin/GrpcKotlinCompatibilityGate.java:1-69
main/java/…/web/GrpcWebCompatibilityGate.java:1-56
main/java/…/servlet/GrpcServletStartupValidator.java:1-51
main/java/…/integration/GrpcIntegrationBridgePolicy.java:1-78
main/java/…/reactor/(ReactiveGrpcClient · ReactiveGrpcServerAdapter · GrpcReactorCancellationBridge · GrpcReactorContextBridge)
main/java/…/web/(GrpcWebProfile · GrpcWebProxyContract · GrpcWebRpcSupport)
main/java/…/servlet/(GrpcServletCompatibilityProfile · GrpcServletCapabilityMatrix)
main/java/…/kotlin/(GrpcCoroutineContextBridge · GrpcKotlinProfile)
main/java/…/integration/(GrpcIntegrationInboundGateway · GrpcIntegrationOutboundGateway)
src/grpc/grpc-client/…/GrpcClientMetadataPolicy.java (예산 검사 대비)