# 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. 모듈의 정체 ```groovy // 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` 는 모듈 옵트인과 공개 서비스 이동을 따로 다룬다. ```java 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. 레인 실패의 범위 ```java 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 선언된 의존 셋이 전부 쓰이지 않는다.** ```groovy 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-contract` 는 `compatibility.proto` 가 그쪽 스키마 규칙의 관할이라는 선언으로, `grpc-advanced-bootstrap` 은 `GrpcAdvancedCapability.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.proto` 는 `edition = "2024";` 로 시작하므로 `grpc-proto-contract` 의 `PROTO3_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-contract` 와 `adapter-inbound-grpc` 의 build.gradle 이 그 사실을 주석으로 명시한다. 그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다. 결과적으로 `GrpcEditionCompatibilityReport` 는 사람이 손으로 채우는 기록이 된다. 승격 게이트가 그것을 읽어 판정하므로, 게이트의 입력이 측정이 아니라 선언이다. **등급.** Advanced 가족이라 오늘의 배포에는 영향이 없다. 기록하는 이유는 이 리프의 목적이 "공개 서비스가 옮겨 가기 전에 그 실패를 찾는 것" 이고, 그 실패를 찾을 장치가 픽스처 하나만 있고 짝이 없다는 점이다. **수정.** `compatibility_proto3.proto` 를 같은 디렉터리에 두어 필드·번호·JSON 이름을 맞추고, 두 파일을 컴파일해 산출물을 비교하는 레인을 만든다. 그 레인이 생기기 전까지는 `GrpcEditionCompatibilityReport` 가 측정이 아니라 선언이라는 것을 자바독에 적는 편이 낫다. ### 17.2 P3 — 승격 차단 목록에 담금 기간과 실환경 항목이 없다 `GrpcEdition2024Gate.promotionBlockers` 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR. 같은 가족의 `GrpcAdvancedPromotionGate` 는 `EDITION_2024` 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. 두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다. 두 게이트가 각각 다른 것을 묻는다고 볼 수도 있다 — 하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도. 다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다. 승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다. 수정은 `promotionBlockers` 가 `GrpcAdvancedPromotionGate.evaluate` 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다. ### 17.3 P3 — 정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다 **첫째, 서술과 코드가 어긋난다.** ```java /** 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), 승격은 사람이 수행하는 절차다. 다만 이 리프가 존재하는 이유가 "그 절차를 코드로 적어 두는 것" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다. **수정.** `promotionBlockers` 가 `GrpcEdition2024Policy` 를 받아 `promotionApproved` 를 `promotionAdr` 자리에 쓰고, 정책 생성자가 자바독대로 "승인이 참이면 그 근거(공개 서비스 집합이 비어 있지 않을 것 등)"를 요구한다. 어느 쪽도 하지 않겠다면 자바독의 그 문장을 지운다. ### 확인된 설계(문제 아님) - **모듈 옵트인과 공개 서비스 이동을 분리한 것** — 빌드 결정과 소비자 이관 결정은 다른 결정이다. - **호환성을 셋으로 나눈 것** — 앞의 둘이 보존돼도 셋째가 깨지는 것이 이 레인이 찾는 실패다. - **툴체인 결과가 비면 생성자가 거부하는 것** — 자바 하나는 교차 언어 증거가 아니다. - **레인 실패가 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 ```