Files
document-haness/docs/clean-architecture-backend-template/analysis/grpc/grpc-advanced-edition.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

17 KiB

grpc-advanced-edition 완전 해부

상태: COMPLETE 재오픈 게이트: cycle 2 — src/main production 6파일 326줄, 스키마 리소스 1개 28줄, test 2파일 211줄 축자 통독 완료. STRUCTURAL_ONLY 잔여 없음. 기준 revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 분석 범위: src/grpc-advanced/grpc-advanced-edition SSOT owner: grpc-advanced-edition integration/family document: analysis/20-grpc-platform.md (secondary, INTEGRATION_ONLY)


0. SSOT identity / 커버리지

  • allowed_dependencies: ["grpc-core-api", "grpc-proto-contract", "grpc-advanced-bootstrap"]
  • runtime_memberships: [] — build-only
파일 LOC
GrpcEditionCompatibilityReport 72
GrpcEdition2026WatchReport 70
GrpcEdition2024Gate 61
GrpcEdition2026Guard 48
GrpcEdition2024Policy 47
GrpcEdition2026Status 28
main java 합계 (6파일) 326
compatibility.proto 28
GrpcEdition2024GateTest · GrpcEdition2026GuardTest 118 · 93

Coverage ledger

scope count disposition reason
main/java/** 6 FULL_READ 전 본문 축자 확인
main/resources/proto/edition2024/*.proto 1 FULL_READ 28줄 전문
test/java/** 2 FULL_READ 211줄 전 본문 · 테스트 13개
build.gradle 1 FULL_READ 10줄
gradle.lockfile 1 STRUCTURAL_ONLY 잠금 파일

UNCLASSIFIED 0.


1. 모듈의 정체

// build.gradle:3-5
// Protobuf Edition lanes. Edition 2024 is an opt-in Advanced lane that must produce cross-consumer
// compile evidence before anything public moves onto it; Edition 2026 is a watch lane that records
// release/toolchain status and is refused as a Stable contract source.

두 레인이 성격이 다르다. 하나는 증거를 만들어야 승격되는 레인이고, 하나는 사실만 기록하는 감시 레인이다.

2. Edition 2024 — 두 결정을 분리한다

GrpcEdition2024Policy 는 모듈 옵트인과 공개 서비스 이동을 따로 다룬다.

public boolean serviceMayMove(String serviceName) {
  return !publicServices.contains(serviceName) || promotionApproved;
}

"the opt-in is a build decision and the promotion is a consumer-migration decision."

그 이유가 클래스 javadoc 에 있다.

"an edition change is invisible to the schema's owner and consequential for its consumers: the wire bytes are usually identical, so nothing fails locally, and the breakage appears in whichever consumer's generator handles the edition's features differently."

3. 세 종류의 호환성

GrpcEditionCompatibilityReport 는 하나가 아니라 셋을 본다.

비교 답하는 질문
wire 저장된 메시지와 이동 중 메시지가 계속 디코딩되는가
JSON 전사 프록시와 브라우저 클라이언트가 계속 동작하는가
source(툴체인별) 생성된 코드가 여전히 컴파일되는가

"An edition migration can preserve the first two and break the third for a language whose generator handles the edition's features differently — which is exactly the failure this lane exists to find before a public service moves."

그리고 툴체인 결과가 비어 있으면 생성자가 거부한다 — "Java alone is not cross-language evidence."

4. 레인 실패의 범위

blocksStableRelease()    항상 false
blocksEditionPromotion()  항상 true

"Without that split, an opt-in lane that nobody depends on can hold up every release, and the first response to that is to stop running the lane."

두 메서드 모두 상수를 돌려주고 javadoc 이 그 이유를 적는다 — "Stated as a method so the property is tested rather than described."

5. Edition 2026 — 감시 레인

네 게이트를 따로 추적한다 — 명세, protoc, Buf, 자바 런타임.

"An edition can be released by the specification while protoc does not emit it, or emitted while Buf cannot lint it, or lintable while the Java runtime does not implement its features. A single 'supported yes/no' flag collapses four different waiting states into one."

보고서는 날짜를 필수로 요구한다 — 날짜 없는 감시 기록은 오래된 메모와 구분되지 않는다.

그리고 가드가 보고서와 무관하게 거부한다.

"The guard is deliberately not conditional on the watch report… letting the same record also authorise use means the moment somebody marks four fields SUPPORTED, a schema can move onto an edition with no promotion decision, no consumer migration and no ADR. Turning the watch into a lane that can be used is a code change here, and that is the point."

10. 테스트 레인

두 테스트 211줄 · 13개.

GrpcEdition2024GateTest 7개 — 모듈 옵트인의 기본 꺼짐, 공개 서비스의 승격 요구, 자바 단독이 증거가 아님(빈 툴체인 맵 거부 포함), 세 호환성의 분리, 승격 차단 셋, 레인 실패의 격리, 그리고 픽스처 파일 자체를 리소스로 읽어 edition = "2024"; 로 시작하는지와 features.field_presence = EXPLICIT 를 담는지 대조하는 것.

GrpcEdition2026GuardTest 6개 — 네 게이트의 개별 추적, 날짜 필수, SUPPORTED 만 usable, 전부 SUPPORTED 여도 가드가 거부, 거부 메시지의 미해결 항목, 감시 레인이 Stable 빌드를 막지 않음.

theGuardIsNotConditionalOnTheReport 가 이 레인에서 가장 중요한 한 줄을 붙든다 — 보고서가 readyToEvaluate() == true 인 상태를 만들어 놓고, 그래도 requireNotUsedAsSource 가 던지는지 확인한다. 기록이 사용을 허가하지 않는다는 설계가 테스트로 고정되어 있다.

12. negative-space probes

12.1 도달성. 리프 밖에서 이 리프를 참조하는 것이 하나도 없다 — 자바 코드도, build.gradle 도.

$ grep -rn "advanced.edition" --include=*.java src/ | grep -v grpc-advanced-edition/
grpc-core-api/…/GrpcStableModuleCatalog.java:38:   "grpc-advanced-edition",   ← 목록 안의 문자열
$ grep -rn "grpc-advanced-edition" --include=*.gradle src/
(매치 없음)

12.2 선언된 의존 셋이 전부 쓰이지 않는다.

api project(':grpc:grpc-core-api')
api project(':grpc:grpc-proto-contract')
api project(':grpc-advanced:grpc-advanced-bootstrap')

이 리프의 자바 6파일에는 dev.caskeleton 으로 시작하는 import 가 한 줄도 없다.

$ grep -rn "^import dev.caskeleton" grpc-advanced/grpc-advanced-edition/src/main/java/
(매치 없음)

여섯 파일이 쓰는 것은 java.time · java.util 뿐이다. 세 의존 중 어느 것도 코드에 닿지 않는다.

셋 중 둘은 의도를 읽을 수 있다 — grpc-proto-contractcompatibility.proto 가 그쪽 스키마 규칙의 관할이라는 선언으로, grpc-advanced-bootstrapGrpcAdvancedCapability.EDITION_2024 가 이 레인의 등급을 들고 있다는 선언으로. 다만 어느 쪽도 코드로 연결되어 있지 않고, 그 연결 없음이 §17.2 가 지적한 "두 게이트가 서로를 부르지 않는다" 와 같은 사실의 빌드 파일 쪽 표현이다.

grpc-advanced-bootstrap §12.1 이 반대편에서 같은 것을 기록했다 — 그 리프를 의존 선언한 다섯 모듈 중 실제로 부르는 것은 둘뿐이고, 이 리프는 부르지 않는 셋 중 하나다.

12.3 대조군. 무조건 상수를 돌려주고 그것을 테스트가 붙드는 형태가 같은 가족의 GrpcAdvancedPromotionGate.capabilitiesDraggedAlong 과 같다. 이 리프에는 그런 메서드가 넷 있다 — blocksStableRelease · blocksEditionPromotion · allowedAsStableSource · blocksStableBuild.

그중 셋(blocksStableRelease · blocksEditionPromotion · blocksStableBuild)은 capabilitiesDraggedAlong 과 같은 한계를 갖는다 — 리터럴을 리터럴과 비교하므로, 그 속성이 실제로 지켜지는지는 이 저장소에 릴리스 파이프라인이 생겨야 알 수 있다. grpc-advanced-bootstrap §17.5 에 그 판정을 적어 두었다.

넷째 allowedAsStableSource 는 다르다. 같은 클래스의 requireNotUsedAsSource 가 그 상수와 독립적으로 무조건 던지고, theGuardIsNotConditionalOnTheReport 가 "전부 SUPPORTED 인 보고서"라는 실제 상태를 만들어 그 독립성을 확인한다. 상수 하나를 읽는 것이 아니라 설계 속성을 실행으로 밟는다.

12.2 대조군. 무조건 상수를 돌려주고 그것을 테스트가 붙드는 형태가 같은 가족의 GrpcAdvancedPromotionGate.capabilitiesDraggedAlong 과 같다. 이 저장소가 "주석이 주장하는 대신 테스트가 붙든다" 를 반복해서 쓴다.

12.5 저장소의 .proto 넷. 이 리프의 compatibility.protoedition = "2024"; 로 시작하므로 grpc-proto-contractPROTO3_SYNTAX 규칙에 걸린다. 그 검증기의 커밋 스키마 테스트가 파일 목록을 하드코딩해 이 파일을 판정하지 않으므로 지금은 충돌하지 않는다. 그 테스트를 전수 훑기로 바꾼다면(그쪽 §17.3) 이 파일에 대한 면제가 함께 필요하다.

12.4 드리프트. build.gradle 이 서술한 두 레인의 성격이 코드와 일치한다. 드리프트 없음.

16. 확인하지 못한 것

  • protoc 을 돌려 이 편집 파일이 실제로 컴파일되는지 확인하지 않았다. 저장소에 protobuf 플러그인이 없다.
  • 테스트를 실행하지 않았다. 13개 전부 본문으로만 확인했다.
  • 세 의존이 쓰이지 않는다는 것(§12.2)은 ^import dev.caskeleton grep 으로 판정했다. 같은 패키지 안의 타입이나 완전 한정명 사용이라면 잡히지 않는다 — 다만 이 리프의 패키지는 dev.caskeleton.grpc.advanced.edition 하나이고 세 의존의 패키지와 겹치지 않는다.
  • 편집 기능(features.field_presence = EXPLICIT)이 proto3 의 optional 과 같은 유선 결과를 내는지 확인하지 않았다. 그것이 이 레인의 질문이고 §17.1 이 그 질문에 답할 수 없는 이유다.

17. 손볼 것

17.1 P2 — 비교 픽스처에 비교 대상이 없다

compatibility.proto 의 주석이 존재 이유를 적는다.

"It exists to be compiled beside its proto3 twin and compared: same fields, same numbers, same JSON names, with presence expressed by the edition's features rather than by optional. The lane's question is whether the two produce the same wire bytes and the same JSON, and answering it needs both files to exist."

그 쌍둥이가 저장소에 없다.

$ grep -rn "DocumentSummary" --include=*.proto --include=*.java .
./src/grpc-advanced/grpc-advanced-edition/src/main/resources/proto/edition2024/compatibility.proto:17

한 곳뿐이다. 같은 필드와 번호를 proto3 로 선언한 파일이 없으므로 비교가 성립하지 않는다.

그리고 두 번째 전제도 없다. 이 저장소에는 protobuf 플러그인이 어디에도 없다 — grpc-proto-contractadapter-inbound-grpc 의 build.gradle 이 그 사실을 주석으로 명시한다. 그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다.

결과적으로 GrpcEditionCompatibilityReport 는 사람이 손으로 채우는 기록이 된다. 승격 게이트가 그것을 읽어 판정하므로, 게이트의 입력이 측정이 아니라 선언이다.

등급. Advanced 가족이라 오늘의 배포에는 영향이 없다. 기록하는 이유는 이 리프의 목적이 "공개 서비스가 옮겨 가기 전에 그 실패를 찾는 것" 이고, 그 실패를 찾을 장치가 픽스처 하나만 있고 짝이 없다는 점이다.

수정. compatibility_proto3.proto 를 같은 디렉터리에 두어 필드·번호·JSON 이름을 맞추고, 두 파일을 컴파일해 산출물을 비교하는 레인을 만든다. 그 레인이 생기기 전까지는 GrpcEditionCompatibilityReport 가 측정이 아니라 선언이라는 것을 자바독에 적는 편이 낫다.

17.2 P3 — 승격 차단 목록에 담금 기간과 실환경 항목이 없다

GrpcEdition2024Gate.promotionBlockers 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR.

같은 가족의 GrpcAdvancedPromotionGateEDITION_2024 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. 두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다.

두 게이트가 각각 다른 것을 묻는다고 볼 수도 있다 — 하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도. 다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다. 승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다.

수정은 promotionBlockersGrpcAdvancedPromotionGate.evaluate 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다.

17.3 P3 — 정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다

첫째, 서술과 코드가 어긋난다.

/** Copies both sets and refuses an approval nobody recorded. */
public GrpcEdition2024Policy {
  if (optedInModules == null || publicServices == null) {
    throw new IllegalArgumentException("an edition policy states both sets");
  }
  optedInModules = Set.copyOf(optedInModules);
  publicServices = Set.copyOf(publicServices);
}

"refuses an approval nobody recorded" 에 해당하는 검사가 없다. promotionApproved 는 읽히지도 검증되지도 않고 그대로 저장된다. new GrpcEdition2024Policy(Set.of(), Set.of(), true) — 옵트인한 모듈도 공개 서비스도 없는데 승인만 참인 정책 — 이 아무 저항 없이 만들어지고, serviceMayMove 는 모든 서비스에 참을 답한다.

둘째, 같은 사실이 두 곳에 따로 있다.

어디 무엇
GrpcEdition2024Policy.promotionApproved 승격이 승인되었는가 (record 성분)
GrpcEdition2024Gate.promotionBlockers(..., boolean promotionAdr) 승격 ADR 이 있는가 (메서드 인자)

게이트는 정책을 인자로 받지도, 참조하지도 않는다. 그래서 "ADR 이 없다"고 판정한 게이트와 "승인되었다"고 답하는 정책이 동시에 성립할 수 있고, 둘을 맞추는 코드가 없다. §17.2 가 지적한 "두 게이트가 서로를 부르지 않는다" 와 같은 구조가 정책과 게이트 사이에도 있다.

왜 P3 인가. 정책도 게이트도 production 호출자가 없고(§12.1), 승격은 사람이 수행하는 절차다. 다만 이 리프가 존재하는 이유가 "그 절차를 코드로 적어 두는 것" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다.

수정. promotionBlockersGrpcEdition2024Policy 를 받아 promotionApprovedpromotionAdr 자리에 쓰고, 정책 생성자가 자바독대로 "승인이 참이면 그 근거(공개 서비스 집합이 비어 있지 않을 것 등)"를 요구한다. 어느 쪽도 하지 않겠다면 자바독의 그 문장을 지운다.

확인된 설계(문제 아님)

  • 모듈 옵트인과 공개 서비스 이동을 분리한 것 — 빌드 결정과 소비자 이관 결정은 다른 결정이다.
  • 호환성을 셋으로 나눈 것 — 앞의 둘이 보존돼도 셋째가 깨지는 것이 이 레인이 찾는 실패다.
  • 툴체인 결과가 비면 생성자가 거부하는 것 — 자바 하나는 교차 언어 증거가 아니다.
  • 레인 실패가 Stable 릴리스를 막지 않게 한 것과 그 근거 — 막으면 사람들이 레인을 끄게 된다.
  • 감시 레인의 네 게이트를 따로 추적한 것.
  • 감시 보고서에 날짜를 필수로 둔 것.
  • 가드를 보고서와 무관하게 만든 것 — 기록이 사용을 허가하지 않는다. 사용하려면 코드를 고쳐야 한다.

Source anchors

src/grpc-advanced/grpc-advanced-edition/build.gradle:1-10
main/java/…/edition/GrpcEditionCompatibilityReport.java:1-72
main/java/…/edition/GrpcEdition2026WatchReport.java:1-70
main/java/…/edition/GrpcEdition2024Gate.java:1-61
main/java/…/edition/GrpcEdition2026Guard.java:1-48
main/java/…/edition/GrpcEdition2024Policy.java:1-47
main/java/…/edition/GrpcEdition2026Status.java:1-28
main/resources/proto/edition2024/compatibility.proto:1-28
test/java/…/edition/GrpcEdition2024GateTest.java:1-118
test/java/…/edition/GrpcEdition2026GuardTest.java:1-93