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>
18 KiB
grpc-spring-boot-starter 완전 해부
상태: COMPLETE 재오픈 게이트: cycle 2 —
src/mainproduction 4파일 468줄, 등록 파일 1개, test 1파일 267줄 축자 통독 완료.STRUCTURAL_ONLY잔여 없음. 기준 revision:21234e38cdb9a926cbc92bb97a2aee2e4a7d2916분석 범위:src/grpc/grpc-spring-boot-starterSSOT owner:grpc-spring-boot-starterintegration/family document:analysis/20-grpc-platform.md(secondary, INTEGRATION_ONLY)
0. SSOT identity / 커버리지와 숫자 지도
runtime_memberships:[]— build-only- 선언 의존:
apiproject 7 ·implementationproject 3 + vendor 2
| 파일 | LOC | 성격 |
|---|---|---|
GrpcPlatformStartupValidator |
188 | 5개 검증 묶음, static 유틸 |
GrpcPlatformProperties |
139 | ca-skeleton.grpc.platform.* 결속 |
GrpcPlatformAutoConfiguration |
106 | 빈 9개 |
GrpcPlatformConfigurationException |
35 | 위반 목록 예외 |
| main java 합계 | 468 | |
AutoConfiguration.imports |
1 | 자동 설정 1개 등록 |
GrpcPlatformStartupValidatorTest |
267 | 테스트 |
build.gradle |
24 | 의존 선언 |
Coverage ledger
| scope | count | disposition | reason |
|---|---|---|---|
main/java/** |
4 | FULL_READ |
188+139+106+35 전 본문 |
main/resources/META-INF/spring/*.imports |
1 | FULL_READ |
1줄 |
test/java/** |
1 | FULL_READ |
267줄 · 테스트 12개 |
build.gradle |
1 | FULL_READ |
24줄 |
gradle.lockfile |
1 | STRUCTURAL_ONLY |
잠금 파일 |
UNCLASSIFIED 0.
1. 모듈의 정체와 격리 규칙
// build.gradle:3-7
// The platform's composition boundary: typed properties, auto-configuration and the startup
// validator that refuses a deployment whose configuration contradicts a Stable invariant.
//
// It must never reach `:grpc-advanced:*`. That is not a comment — the registry's
// allowed_dependencies for this leaf omits every advanced id, `verifyCleanArchitectureDependencies`
// enforces it, and GrpcPlatformStartupValidatorTest asserts the same rule from the Java side.
격리 규칙은 세 겹이다 — 레지스트리, 빌드 검증 태스크, 그리고 자바 쪽 단언. 세 번째는 validateAdvancedIsolation 이 GrpcStableBuildInvariant.requireNoAdvancedDependency 를 부르는 형태다.
2. 자동 설정이 만드는 것
@ConditionalOnProperty(prefix = "ca-skeleton.grpc.platform", name = "enabled", havingValue = "true", matchIfMissing = false) — 기본 꺼짐.
| 빈 | 만들어지는 값 |
|---|---|
GrpcExecutorProfile |
boundedPool(maxPoolSize, queueCapacity) |
GrpcServerProfile |
stableNetty(executorProfile) |
GrpcAdmissionController |
forExecutor(executorProfile) |
GrpcServiceHealthRegistry |
new …(GrpcHealthPolicy.standalone()) |
GrpcReflectionPolicy |
설정 값이 없으면 defaultFor(environment) |
GrpcAdminExposurePolicy |
standard() |
GrpcDrainPolicy |
stable() |
GrpcContextBinder |
new …(GrpcContextPropagationPolicy.stable()) |
GrpcErrorMapper |
new …("grpc-platform", UUID::randomUUID) |
전부 정책·프로파일·레지스트리다. 서버도, 인터셉터 사슬도, 서비스 어댑터 등록도 없다.
3. 설정 표면
@ConfigurationProperties(prefix = "ca-skeleton.grpc.platform", ignoreUnknownFields = false).
두 판단이 javadoc 에 적혀 있다.
"Off by default, like every other optional capability in this repository. A platform that starts because its jar is on the classpath is a platform that opens a port on a deployment nobody decided to give one to."
"
ignoreUnknownFields = falseso a misspelled key fails startup rather than silently leaving a setting at its default."
4. 검증기가 담은 규칙
javadoc 이 선정 기준을 적는다.
"Every rule here is a mistake whose runtime symptom is either silence or a misattributed failure… None of them fails a smoke test."
다섯 묶음이다.
- 전송·보안 — production 전송이 아니면 거부, 배포 환경에서 TLS 미사용·trust-all·반사 전체 공개 거부
- 실행기 — 큐 용량 1 미만(무제한) 거부, 풀 크기 양수 요구
- 메서드 — 단항인데 사용 가능한 마감이 0, 명시적 재시도가 멱등 프로파일과 모순, 멱등 키 필수인데 원장 비활성, Stable 범위 밖 RPC 종류
- 채널 — Stable 스킴 요구, 두 재시도 소유자가 동시에 in-process 재시도
- 고급 격리 — Stable 스타터가 advanced 의존을 끌면 위반
그리고 한 번에 전부 모아 실패한다 — "so a deployment learns the whole list in one restart."
10. 테스트 레인
GrpcPlatformStartupValidatorTest 267줄 · 12개. 검증기의 규칙별 거부와 통과를 직접 호출로 확인한다. 자동 설정 컨텍스트를 세우는 테스트는 없다 — ApplicationContextRunner 도, 슬라이스 테스트도 없다. 빈 아홉 개가 실제로 조립되는지는 이 레인이 답하지 않는다.
aCoherentConfigurationStarts 가 통과 쪽을, 나머지 아홉이 규칙별 거부 쪽을 잡는다 — in-process 전송, TLS 둘, 반사, 무제한 실행기, 원장 없는 멱등 키, client-streaming, 재시도 소유자 둘, advanced 누출. aRefusalNamesEveryViolation 이 세 개를 동시에 깨뜨려 목록이 한 번에 나오는지 본다.
마지막 하나가 형태로 특이하다.
void theAutoConfigurationIsRegisteredAndStable() {
assertThat(read(Path.of("src/main/resources/META-INF/spring/…imports")).strip())
.isEqualTo("dev.caskeleton.grpc.boot.GrpcPlatformAutoConfiguration");
// The dependency declaration, not the word: the build file's own comment says it must never
// reach an advanced module, and matching on the prose would fail on the sentence stating the rule.
assertThat(read(Path.of("build.gradle"))).doesNotContain("project(':grpc-advanced");
}
단위 테스트가 자기 모듈의 build.gradle 을 파일로 읽어 의존 선언을 단언한다. 주석이 왜 낱말이 아니라 선언 문법에 맞추는지까지 적어 두었다 — 규칙을 서술한 문장 자체가 낱말 검색에 걸리기 때문이다. verifyCleanArchitectureDependencies 가 도는 것과 별개로 이 레인 안에서도 격리가 붙들린다.
12. negative-space probes
12.1 도달성. 리프 밖에서 이 리프의 타입을 부르는 코드가 0 이고, 이 리프를 의존하는 모듈도 0 이다.
$ grep -rn "grpc-spring-boot-starter" --include=*.gradle src/
(매치 없음)
$ grep -rn "ca-skeleton.grpc.platform" --include=*.yml --include=*.yaml --include=*.properties .
(매치 없음 — 이 리프 자신을 빼고)
$ grep -rn "GrpcPlatformAutoConfiguration\|GrpcPlatformProperties\|GrpcPlatformStartupValidator" --include=*.java src/ | grep -v grpc-spring-boot-starter
(매치 없음)
| 타입 | production 호출자 |
|---|---|
GrpcPlatformAutoConfiguration |
등록 파일 1줄 — 그러나 이 스타터를 클래스패스에 올리는 모듈이 없다 |
GrpcPlatformStartupValidator |
0 |
GrpcPlatformConfigurationException |
검증기 안에서만 |
enabled=true 를 쓰는 설정 파일도 저장소에 없다. 즉 @ConditionalOnProperty 가 참이 되는 배포가 지금 하나도 없고, 아홉 빈은 아직 한 번도 만들어진 적이 없다. §17.1 의 등급을 P2 로 둔 근거가 이것이다 — 오늘의 사고가 아니라, 이 스타터를 처음 채택하는 배포가 맞을 상태다.
12.3 선언만 있고 쓰이지 않는 의존 셋.
implementation project(':grpc:grpc-proto-contract')
implementation project(':grpc:grpc-codegen')
implementation project(':grpc:grpc-operation-ledger-jpa')
이 리프의 자바 4파일 어디에도 dev.caskeleton.grpc.contract · …grpc.codegen · 운영 원장 타입의 import 가 없다. operation-ledger-enabled 는 boolean 프로퍼티일 뿐 원장 타입을 참조하지 않는다.
세 의존 모두 build-only 판정 도구다 — 스키마 규칙 엔진, 코드 생성 거버넌스, JPA 원장. 스타터가 그것들을 런타임 조립에 쓰지 않으면서 클래스패스에 끌고 온다. 이 리프의 존재 이유가 "구성 경계"이므로, 경계가 끌어오는 것이 실제로 필요한 것인지가 다른 리프보다 더 중요하다.
같은 사실을 반대편에서도 기록해 두었다 — grpc-codegen §12.1, grpc-proto-contract §12.1.
12.2 설정 키별 소비자.
| 키 | 읽는 곳 |
|---|---|
enabled |
@ConditionalOnProperty |
executor-queue-capacity · executor-max-pool-size |
자동 설정 + 검증기 |
environment |
자동 설정(반사 정책) + 검증기 |
reflection-mode |
자동 설정 + 검증기 |
transport |
검증기뿐 |
tls-enabled · trust-all-certificates |
검증기뿐 |
operation-ledger-enabled |
검증기뿐 |
default-unary-deadline |
없음 |
12.4 드리프트. build.gradle 이 서술한 세 요소(타입 있는 설정·자동 설정·시작 검증기)가 전부 존재한다. 어긋난 것은 세 번째가 시작 시 돌지 않는다는 점이고 §17.1 이다.
16. 확인하지 못한 것
- 스타터를 실제 애플리케이션에 올려 컨텍스트를 세우지 않았다. build-only 이고, 이 스타터를 의존하는 모듈이 저장소에 없다(§12.1).
verifyCleanArchitectureDependencies태스크를 이 리비전에서 실행하지 않았다.- 테스트를 실행하지 않았다. 12개 전부 본문으로만 확인했다.
- 세
implementation의존이 쓰이지 않는다는 것(§12.3)은 패키지 이름 grep 으로 판정했다. 상수나 문자열을 통한 간접 사용이라면 잡히지 않는다.
17. 손볼 것
17.1 P2 — 시작 검증기가 시작 시 실행되지 않는다
GrpcPlatformStartupValidator 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트.
src/grpc/grpc-spring-boot-starter/src/main/java/…/GrpcPlatformStartupValidator.java
src/grpc/grpc-spring-boot-starter/src/test/java/…/GrpcPlatformStartupValidatorTest.java
GrpcPlatformAutoConfiguration 은 빈 9개를 만들고 requireValid 를 부르지 않는다. 초기화 콜백도, @PostConstruct 도, ApplicationRunner 도 없다.
그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — "Refuses to start on a configuration that would be wrong in a way nobody would notice." 지금은 그 설정으로 그냥 시작한다.
함께 사라지는 것. 검증기가 유일한 소비자인 설정 키가 넷이다.
transport— production 이 아닌 전송을 거부할 곳이 없다. 게다가 자동 설정은 이 값을 보지 않고GrpcServerProfile.stableNetty(...)를 하드코딩한다(§17.2).tls-enabled·trust-all-certificates— 배포 환경의 TLS 바닥을 강제할 곳이 없다.operation-ledger-enabled— 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다.
같은 저장소가 이 형태를 두 번 기록했다 — WebPlatformStartupValidator 가 시작 시 실행되지 않고, BrokerAclManifest 의 시작 자기점검이 없다. 반대로 messaging 의 StartupProfileValidation 은 InitializingBean.afterPropertiesSet 으로 돌려 그 문제를 이미 한 번 해결했고, fileserver 는 attestMapping() 을 app-bootstrap 의 @Bean 으로 연결했다. 정본이 저장소 안에 둘 있다.
왜 배선되지 않았는지가 서명에 보인다. violations 는 넷을 받는다.
public static List<String> violations(
GrpcPlatformProperties properties, // 자동 설정이 @EnableConfigurationProperties 로 가진다
GrpcMethodPolicyCatalog catalog, // 이 자동 설정에 빈 정의 없음
List<GrpcNamedChannelProfile> channelProfiles, // 빈 정의 없음
Set<String> stableModuleDependencies) // 이것을 런타임에 계산하는 코드가 저장소에 없음
넷 중 셋에 생산자가 없다. 특히 마지막은 "스타터가 해석한 모듈 id 집합" 인데, 그것을 실행 중에 산출하는 코드가 저장소 어디에도 없다 — 테스트는 Set.of("grpc-core-api", "grpc-policy", "grpc-server", "grpc-client") 리터럴을 넣는다. 검증기가 요구하는 입력을 구성 경계가 만들지 않으므로, 지금 형태로는 부를 수가 없다.
수정. 자동 설정에 검증기를 부르는 InitializingBean(또는 SmartInitializingSingleton) 빈을 하나 추가하되, 세 입력의 생산자를 함께 정한다.
GrpcMethodPolicyCatalog·List<GrpcNamedChannelProfile>—ObjectProvider로 받고 비어 있을 때의 동작(건너뛸지, 그 자체를 위반으로 볼지)을 정한다.stableModuleDependencies— 런타임에 계산할 방법이 없다면GrpcStableModuleCatalog가 아는 정적 목록으로 대체하거나, 이 규칙을 빌드 태스크 쪽에만 남기고 검증기 서명에서 뺀다. 지금은 같은 불변식을 세 겹으로 둔다고 §1 이 말하지만, 세 번째 겹이 실행되려면 아무도 만들지 않는 입력이 필요하다.
17.2 P3 — 자동 설정이 transport 를 읽지 않고 전송을 하드코딩한다
@Bean @ConditionalOnMissingBean
public GrpcServerProfile grpcServerProfile(GrpcExecutorProfile executorProfile) {
return GrpcServerProfile.stableNetty(executorProfile);
}
GrpcPlatformProperties.transport 는 GrpcServerTransport 열거형이고 기본값이 NETTY_SHADED 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다.
지금은 무해에 가깝다 — 기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다. 그러나 설정 키가 존재하고 문서화되어 있으므로 운영자는 그것이 전송을 고른다고 읽는다.
수정은 프로파일 팩토리를 transport 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다.
17.3 P3 — default-unary-deadline 은 읽는 코드가 저장소에 없다
/** The default deadline applied to a Stable unary method that declares none. */
private Duration defaultUnaryDeadline = Duration.ofSeconds(2);
getDefaultUnaryDeadline() 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 policy.deadline().usable() 이고 그 값이 0 이면 위반을 낸다. 즉 자바독이 말하는 "선언하지 않은 메서드에 적용되는 기본 마감" 을 적용하는 코드가 없다.
ignoreUnknownFields = false 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다.
수정은 그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다.
17.4 P3 — 반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다
@Bean @ConditionalOnMissingBean
public GrpcReflectionPolicy grpcReflectionPolicy(GrpcPlatformProperties properties) {
return properties.getReflectionMode() == null
? GrpcReflectionPolicy.defaultFor(properties.getEnvironment())
: new GrpcReflectionPolicy(
properties.getReflectionMode(),
java.util.Set.of("admin"), // ← 리터럴
java.util.Set.of("ROLE_PLATFORM_ADMIN")); // ← 리터럴
}
두 갈래가 만드는 것이 같은 종류의 값이 아니다.
- 설정하지 않으면
defaultFor(environment)— 환경이 서비스 목록과 역할 목록을 함께 결정한다. - 설정하면 모드만 운영자 것이고, 허용 서비스와 허용 역할은 이 자동 설정에 박힌 리터럴이 된다.
운영자가 조정한다고 생각하는 것은 노출 수위 하나인데, 실제로는 노출 대상 집합까지 바뀐다. 그리고 그 두 리터럴은 설정 표면에 노출되어 있지 않으므로 되돌릴 방법이 reflection-mode 를 다시 비우는 것뿐이다.
ca-skeleton.grpc.platform 은 ignoreUnknownFields = false 를 걸어 "오타가 조용히 기본값으로 남지 않게" 한 설정 표면이다. 같은 규율로 보면, 값을 하나 설정했을 때 설정하지 않은 두 값이 함께 바뀌는 것도 같은 종류의 침묵이다.
수정. 허용 서비스·역할을 GrpcPlatformProperties 에 올리거나, 명시 모드에서도 defaultFor(environment) 가 만든 정책의 모드만 바꾼 사본을 쓴다. 후자가 이 저장소의 다른 곳에서 쓰는 형태다(GrpcProtoStyleManifest.allowingWellKnownTypes 처럼 넓힌 사본).
확인된 설계(문제 아님)
- 기본 꺼짐과 그 근거 — 클래스패스에 있다는 이유로 포트를 여는 플랫폼이 되지 않는다.
ignoreUnknownFields = false— 오타가 조용히 기본값으로 남지 않는다.- 고급 격리를 세 겹으로 둔 것 — 레지스트리·빌드 태스크·자바 단언.
- 검증기가 한 번에 전부 보고하는 것 — 재시작 한 번으로 목록 전체를 배운다.
- 검증 규칙 선정 기준 — 증상이 침묵이거나 오귀인인 실수만 담는다.
@ConditionalOnMissingBean을 아홉 빈 전부에 둔 것 — 채택자가 개별 정책을 갈아끼울 수 있다.
Source anchors
src/grpc/grpc-spring-boot-starter/build.gradle:1-24
main/java/…/boot/GrpcPlatformAutoConfiguration.java:1-106
main/java/…/boot/GrpcPlatformStartupValidator.java:1-188
main/java/…/boot/GrpcPlatformProperties.java:1-139
main/java/…/boot/GrpcPlatformConfigurationException.java:1-35
main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:1
test/java/…/boot/GrpcPlatformStartupValidatorTest.java:1-267