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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
@@ -0,0 +1,348 @@
|
||||
# grpc-codegen 완전 해부
|
||||
|
||||
> 상태: COMPLETE
|
||||
> 재오픈 게이트: cycle 2 — `src/main` production 9파일 692줄, test 3파일 414줄, 소비자 픽스처 리소스 2파일 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음.
|
||||
> 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916`
|
||||
> 분석 범위: `src/grpc/grpc-codegen`
|
||||
> SSOT owner: `grpc-codegen`
|
||||
> integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY)
|
||||
|
||||
---
|
||||
|
||||
## 0. SSOT identity / 커버리지
|
||||
|
||||
- `allowed_dependencies`: `["grpc-core-api", "grpc-proto-contract"]`
|
||||
- `runtime_memberships`: **`[]`** — build-only
|
||||
|
||||
| 파일 | LOC |
|
||||
|---|---:|
|
||||
| `GrpcConsumerFixture` | 158 |
|
||||
| `GrpcSchemaArtifactPublisher` | 113 |
|
||||
| `GrpcCodegenManifest` | 81 |
|
||||
| `GrpcBufPolicy` | 75 |
|
||||
| `GrpcGeneratedPackagePolicy` | 68 |
|
||||
| `GrpcDescriptorArtifact` | 59 |
|
||||
| `GrpcCodegenOutput` | 58 |
|
||||
| `GrpcBreakingCategory` | 43 |
|
||||
| `GrpcSchemaBaseline` | 37 |
|
||||
| **main 합계** | **692** |
|
||||
| test 3파일 | 248 + 88 + 78 |
|
||||
| 소비자 픽스처 리소스 | 33 + 17 |
|
||||
|
||||
### Coverage ledger
|
||||
|
||||
| scope | count | disposition | reason |
|
||||
|---|---:|---|---|
|
||||
| `main/java/**` | 9 | `FULL_READ` | 692줄 전 본문 |
|
||||
| `test/java/**` | 3 | `FULL_READ` | 414줄 · 테스트 21개 |
|
||||
| `test/resources/consumer-fixtures/**` | 2 | `FULL_READ` | 50줄 |
|
||||
| `build.gradle` | 1 | `FULL_READ` | 12줄 |
|
||||
| `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 |
|
||||
|
||||
`UNCLASSIFIED` 0.
|
||||
|
||||
---
|
||||
|
||||
## 1. 모듈의 정체
|
||||
|
||||
```groovy
|
||||
// build.gradle:3-7
|
||||
// Contract governance: Buf format/lint/breaking policy, the single codegen owner declaration, and
|
||||
// the descriptor/schema-hash release artifact with its consumer-compile gate.
|
||||
//
|
||||
// The Buf rules are implemented here rather than shelled out to the Buf CLI (adaptation D5): the
|
||||
// CLI is not present in this toolchain, and a gate that silently no-ops when a binary is missing is
|
||||
// worse than one that computes the same judgement from the committed schema.
|
||||
```
|
||||
|
||||
마지막 문장이 이 저장소의 반복 원칙이다 — 도구가 없을 때 조용히 통과하는 게이트는 없는 것보다 나쁘다.
|
||||
|
||||
## 2. 파괴적 변경 범주 — 왜 FILE 인가
|
||||
|
||||
| 범주 | 소스 파괴 감지 | 파일 이동 감지 |
|
||||
|---|---|---|
|
||||
| `FILE` | 예 | 예 |
|
||||
| `PACKAGE` | 예 | 아니오 |
|
||||
| `WIRE_JSON` | 아니오 | 아니오 |
|
||||
| `WIRE` | 아니오 | 아니오 |
|
||||
|
||||
> "A team that gates on WIRE ships a field rename, watches its own integration tests pass, and finds
|
||||
> out at the consumer's next build."
|
||||
|
||||
`GrpcBufPolicy` 정규 생성자가 소스 파괴를 감지하지 못하는 범주를 거부하고, 형식·린트를 선택 사항으로 두지 않는다.
|
||||
|
||||
## 3. 기준선은 브랜치가 아니라 릴리스다
|
||||
|
||||
`GrpcSchemaBaseline` 은 `-SNAPSHOT` 버전을 거부하고 `sha256:` 접두 해시를 요구한다.
|
||||
|
||||
> "Comparing against the previous commit answers 'did this commit break anything', which is not the
|
||||
> question: a breaking change introduced two commits ago and refined since then passes every
|
||||
> commit-to-commit check while being broken against everything that has actually been deployed."
|
||||
|
||||
## 4. 생성물의 자리
|
||||
|
||||
`GrpcCodegenOutput` 은 모든 경로가 빌드 디렉터리 아래일 것을 요구하고, 절대 경로와 `..` 를 거부하며, 서술자 집합 확장자를 `.desc`/`.binpb` 로 제한한다.
|
||||
|
||||
> "A generator that writes into a source tree produces files that get committed, then edited, then
|
||||
> silently reverted by the next regeneration — and the diff that reverts them looks like the
|
||||
> generator working correctly."
|
||||
|
||||
## 5. 생성자는 하나여야 한다
|
||||
|
||||
`GrpcCodegenManifest` 는 소유자 하나와 관리 플랫폼에서 오는 두 버전 출처를 요구한다.
|
||||
|
||||
> "Two generators for one schema is the state in which a type exists twice with different options
|
||||
> and the classpath decides which one a consumer gets."
|
||||
|
||||
> "A pinned protobuf version beside a BOM-managed gRPC version is how the runtime and the generator
|
||||
> drift into a combination nobody tested, and the symptom is a `NoSuchMethodError` in generated code."
|
||||
|
||||
그리고 생성 패키지와 손으로 쓴 패키지가 겹치면 생성 시점에 던진다.
|
||||
|
||||
`caSkeleton()` 의 소유자는 Gradle protobuf 플러그인이고, javadoc 이 그것이 아직 이 빌드에서 돌지 않는다고 적는다 — "this manifest is what a future decision to turn it on has to satisfy rather than replace."
|
||||
|
||||
## 6. 소비자 컴파일 게이트
|
||||
|
||||
`GrpcConsumerFixture.fromJavaSource` 가 릴리스된 소비자의 자바 소스에서 요구 사항 셋을 기계적으로 유도한다.
|
||||
|
||||
> "a hand-written requirement list is a second copy of what the client already says and the copy is
|
||||
> the one that stops being updated."
|
||||
|
||||
세 규칙이다.
|
||||
|
||||
```
|
||||
생성 자바 패키지 = fixture 클래스가 import 하는 패키지 중 접미가 맞는 것
|
||||
서비스 = <Name>Grpc import → <proto package>.<Name>
|
||||
메서드 = stub.<name>( 호출 → <service>/<UpperCamelName>
|
||||
```
|
||||
|
||||
그리고 그것이 컴파일의 근사라는 것과, 근사인 이유(ADR-GRPC-002)를 함께 적는다.
|
||||
|
||||
`breaksAgainst` 는 세 종류를 따로 보고한다 — 서비스 경로, 메서드 경로, 자바 패키지. 하나의 개수로 합치지 않는다.
|
||||
|
||||
## 10. 테스트 레인
|
||||
|
||||
세 테스트 414줄 · 21개.
|
||||
|
||||
`GrpcBufPolicyTest` 6개 — Stable 게이트가 `FILE` 이라는 것, `WIRE`/`WIRE_JSON` 거부, 형식·린트 비선택, 수명주기 태스크 이름(§17.1), 기준선의 불변 릴리스 요구, 해시 일치.
|
||||
|
||||
`GrpcCodegenManifestTest` 5개 — 소유자 유일성, 리터럴 버전 거부, 출력 경로가 `build/` 아래여야 한다는 것과 서술자 확장자, 패키지 겹침의 양방향 감지.
|
||||
|
||||
`GrpcDescriptorArtifactTest` 10개 — 산출물의 불변 버전과 세 digest, 파괴 종류별 보고, 넓어진 스키마가 아무것도 깨지 않는다는 것, 커밋된 픽스처의 유도 결과, 메서드 이름 변경이 발행을 막는다는 것, 요구가 빈 픽스처 거부, 픽스처 build 파일의 고정 버전, 소비자 실패의 발행 차단, 같은 버전 다른 바이트 거부, 같은 바이트 재발행 허용.
|
||||
|
||||
**픽스처를 리소스에서 읽는다.** `resource(path)` 가 클래스로더로 `consumer-fixtures/v1/...` 를 읽어 실제 커밋된 텍스트를 넣는다 — 유도 규칙을 리터럴 문자열이 아니라 저장소에 있는 파일에 대고 돌린다. `theFixturePinsItsSchemaVersion` 은 픽스처의 `build.gradle.kts` 본문까지 대조한다.
|
||||
|
||||
## 12. negative-space probes
|
||||
|
||||
**12.1 도달성.** build-only 이지만 타입 참조는 리프 밖에 있다. 실제 참조 지점은 다섯이다.
|
||||
|
||||
| 참조 | 형태 |
|
||||
|---|---|
|
||||
| `grpc-testkit/…/release/GrpcStableReleaseGate.java:3` | `import …codegen.GrpcSchemaArtifactPublisher` — production `src/main` 코드 |
|
||||
| `grpc-testkit/build.gradle:55` | `api project(':grpc:grpc-codegen')` |
|
||||
| `grpc-spring-boot-starter/build.gradle:19` | `implementation project(':grpc:grpc-codegen')` |
|
||||
| `grpc-proto-contract/…/GrpcProtoContractValidator.java:22` | javadoc 언급만 |
|
||||
| `grpc-core-api/…/GrpcStableModuleCatalog.java:24` | 목록 안의 `"grpc-codegen"` 문자열 |
|
||||
|
||||
`GrpcStableReleaseGate.evaluate` 는 `GrpcSchemaArtifactPublisher.PublishDecision` 을 **인자로 받는다** — 발행자를 만들지 않는다. 그리고 그 게이트 자신도 리터럴을 먹이는 테스트 말고는 호출자가 없다(`grpc-testkit` §17). 즉 타입 수준 연결은 실재하지만 그 사슬 어디에도 실행 시점 생산자가 없다.
|
||||
|
||||
`grpc-spring-boot-starter` 의 의존 선언에는 대응하는 자바 참조가 없다 — 스타터 소스 전체에 `codegen` 문자열이 나오지 않는다. 쓰이지 않는 의존이다.
|
||||
|
||||
**12.2 저장소의 스키마에는 service 가 하나도 없다.**
|
||||
|
||||
```
|
||||
$ grep -rn "^service" --include=*.proto src/ (매치 없음)
|
||||
$ grep -rn "^package" --include=*.proto src/
|
||||
grpc-proto-contract/…/v1/stream.proto:3: package hyeonworks.grpc.common.v1;
|
||||
grpc-proto-contract/…/v1/error.proto:3: package hyeonworks.grpc.common.v1;
|
||||
grpc-advanced-edition/…/edition2024/compatibility.proto:3: package hyeonworks.grpc.edition.v1;
|
||||
messaging-schema-protobuf/src/test/proto/order_created_v1.proto:3: package dev.caskeleton.messaging.sample;
|
||||
```
|
||||
|
||||
두 실물 proto 는 message 와 enum 만 담는다. 그런데 `GrpcDescriptorArtifact` 정규 생성자는 메서드가 비면 거부한다 — "a schema artifact with no methods describes nothing". **이 산출물 타입은 이 저장소의 실제 스키마를 표현할 수 없다.**
|
||||
|
||||
그래서 소비자 게이트 전체가 저장소에 없는 표면(`hyeonworks.document.v1.DocumentService`)을 상대로만 돌아간다. `GrpcCodegenManifest.caSkeleton()` 이 선언하는 생성 패키지는 `hyeonworks.grpc.common.v1.generated` 이고 픽스처가 유도하는 패키지는 `hyeonworks.document.v1.generated` 다 — 매니페스트와 픽스처가 서로 다른 스키마를 서술한다.
|
||||
|
||||
결함으로 세지 않는 이유는 build.gradle 과 매니페스트 javadoc 이 이 리프를 "protoc 을 켜기로 하는 미래의 결정이 만족시켜야 할 선언"으로 규정하기 때문이다(D6). 다만 §17.1·§17.4 의 검사들이 지금 무엇에 대해서도 돌지 않는다는 사실의 뿌리가 여기다.
|
||||
|
||||
**12.3 도달 불가 분기.** `GrpcSchemaArtifactPublisher.evaluate` 의 두 번째 차단 사유는 발화할 수 없다.
|
||||
|
||||
```java
|
||||
if (!policy.breakingCategory().detectsSourceBreak()) {
|
||||
blockers.add("the active breaking category does not detect source breaks");
|
||||
}
|
||||
```
|
||||
|
||||
`GrpcBufPolicy` 정규 생성자가 이미 그런 범주를 거부하므로, 구성된 정책은 언제나 소스 파괴를 감지한다. 이 저장소에서 반복해서 나타나는 형태다 — 선행 검증이 후행 검증을 가린다. 보안 효과는 그대로이므로 결함이 아니라 기록으로 남긴다.
|
||||
|
||||
**12.4 드리프트.** build.gradle 이 서술한 세 요소(Buf 정책·단일 생성자 선언·서술자 산출물과 소비자 게이트)가 전부 존재한다. 드리프트 없음.
|
||||
|
||||
## 16. 확인하지 못한 것
|
||||
|
||||
- 실제 `protoc` 이나 Buf CLI 를 돌리지 않았다. 저장소에 둘 다 없다.
|
||||
- 서비스가 둘 이상인 픽스처를 만들어 §17.3 을 재현하지 않았다. 유도 코드로 판정했다.
|
||||
- §17.4 의 어긋난 짝(`publish(다른 후보, 이 결정)`)을 실제로 실행해 보지 않았다. `publish` 본문에 대조 코드가 없다는 것으로 판정했다.
|
||||
- 테스트를 실행하지 않았다. 21개 전부 본문으로만 확인했다.
|
||||
- `grpc-spring-boot-starter` 가 이 모듈을 의존 선언만 하고 쓰지 않는 것은 문자열 grep 으로 판정했다 — 그쪽 SSOT 에서 다시 본다.
|
||||
|
||||
## 17. 손볼 것
|
||||
|
||||
### 17.1 P3 — Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다
|
||||
|
||||
정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다.
|
||||
|
||||
> "Keeping the task names in the policy rather than only in a workflow file means **a missing stage
|
||||
> is a test failure rather than a stage nobody noticed was gone.**"
|
||||
|
||||
그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다.
|
||||
|
||||
```
|
||||
$ grep -rn "bufFormatCheck\|bufLint\|bufBreaking" --include=*.gradle src/
|
||||
(매치 없음)
|
||||
```
|
||||
|
||||
그리고 테스트가 비교하는 대상이 실제 등록 태스크 집합이 아니다.
|
||||
|
||||
```java
|
||||
assertThat(GrpcBufPolicy.requiredTasks())
|
||||
.containsExactly("bufFormatCheck", "bufLint", "bufBuild", "bufBreaking");
|
||||
assertThat(GrpcBufPolicy.missingTasks(Set.of("bufFormatCheck", "bufLint", "bufBuild")))
|
||||
…
|
||||
assertThat(GrpcBufPolicy.missingTasks(Set.copyOf(GrpcBufPolicy.requiredTasks()))).isEmpty();
|
||||
```
|
||||
|
||||
첫 단언은 목록을 리터럴과, 셋째는 목록을 자기 자신과 비교한다. 어느 것도 빌드가 그 단계를 등록했는지 묻지 않는다.
|
||||
|
||||
Buf CLI 가 이 툴체인에 없다는 것은 build.gradle 이 이미 밝힌 사실이므로 태스크가 없는 것 자체는 놀랍지 않다. 어긋난 것은 javadoc 의 주장이다 — 지금 형태에서 단계가 사라져도 테스트는 초록이다.
|
||||
|
||||
수정은 `missingTasks` 에 Gradle 이 실제로 등록한 태스크 이름 집합을 넣는 검사를 만들거나(다른 가족의 레인 등록 검사와 같은 형태), CLI 가 없는 동안에는 그 문장을 "CI 환경이 채울 계약" 으로 낮추는 것이다.
|
||||
|
||||
### 17.2 P3 — 릴리스 버전 불변성이 프로세스 안에서만 성립한다
|
||||
|
||||
```java
|
||||
private final Map<String, String> publishedHashesByVersion = new LinkedHashMap<>();
|
||||
…
|
||||
String alreadyPublished = publishedHashesByVersion.get(candidate.schemaVersion());
|
||||
if (alreadyPublished != null && !alreadyPublished.equals(candidate.schemaHash())) {
|
||||
blockers.add("version '…' is already published with a different schema hash; a released schema version is immutable");
|
||||
}
|
||||
```
|
||||
|
||||
발행 이력이 발행자 인스턴스의 필드다. 새 프로세스는 아무것도 기억하지 못하므로 같은 버전을 다른 해시로 다시 발행하려는 시도가 통과한다.
|
||||
|
||||
이 클래스가 존재하는 이유가 그 규칙이다 — "refuses to let a released version change underneath its consumers." 그 규칙이 지켜지는 범위가 한 발행자 인스턴스의 수명이다.
|
||||
|
||||
빌드마다 새 프로세스가 도는 것이 정상 형태이므로, 실제로 이 검사가 무언가를 막으려면 이력이 산출물 저장소나 파일에서 와야 한다. `GrpcSchemaBaseline` 이 이미 릴리스된 해시를 들고 있으므로 그 방향의 재료는 있다.
|
||||
|
||||
덧붙여 이 맵은 동기화되지 않는다. 발행자를 공유해 병렬로 평가하면 경합한다.
|
||||
|
||||
### 17.3 P3 — 픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다
|
||||
|
||||
```java
|
||||
while (calls.find()) {
|
||||
String method = calls.group(1);
|
||||
String upperCamel = Character.toUpperCase(method.charAt(0)) + method.substring(1);
|
||||
servicePaths.forEach(service -> methodPaths.add(service + "/" + upperCamel));
|
||||
}
|
||||
```
|
||||
|
||||
`stub.<name>(` 호출 하나가 그 파일이 import 한 **모든** 서비스에 대해 메서드 경로를 만든다.
|
||||
|
||||
javadoc 의 규칙 서술은 단수형이다 — "a method is a `stub.<name>(` call, mapped to `<service>/<UpperCamelName>`". 서비스가 둘 이상일 때 어느 서비스인지는 소스 텍스트만으로 알 수 없고, 코드는 전부에 붙이는 쪽을 골랐다.
|
||||
|
||||
결과는 존재하지 않는 메서드 경로를 요구하는 픽스처다. 서비스 둘과 메서드 셋이면 요구 경로가 여섯 개가 되고, 그중 셋은 어떤 후보 스키마에도 없으므로 `breaksAgainst` 가 항상 `METHOD_PATH` 파괴를 보고한다. 그러면 `GrpcSchemaArtifactPublisher.evaluate` 가 모든 발행을 거부한다.
|
||||
|
||||
커밋된 픽스처는 서비스가 하나(`DocumentServiceGrpc`)라 지금은 정확하다. 두 번째 소비자 픽스처를 추가하는 순간 성립한다.
|
||||
|
||||
수정은 호출자 변수의 선언 타입을 함께 읽어 메서드를 서비스에 귀속시키거나, 서비스가 둘 이상인 픽스처를 거부하는 것이다. 후자는 지금 형태의 근사를 명시적으로 만든다.
|
||||
|
||||
### 17.4 P2 — `publish` 가 결정을 그 결정이 판정한 후보에 묶지 않는다
|
||||
|
||||
```java
|
||||
public void publish(GrpcDescriptorArtifact candidate, PublishDecision decision) {
|
||||
if (decision == null || !decision.allowed()) {
|
||||
throw new IllegalStateException("refusing to publish '…'");
|
||||
}
|
||||
publishedHashesByVersion.put(candidate.schemaVersion(), candidate.schemaHash());
|
||||
}
|
||||
```
|
||||
|
||||
`decision` 이 `candidate` 를 판정한 결정인지 확인하는 코드가 없다. `PublishDecision` 은 `(boolean allowed, List<String> blockers)` 뿐이라 자기가 무엇을 판정했는지 들고 있지도 않다.
|
||||
|
||||
그래서 이렇게 쓸 수 있다.
|
||||
|
||||
```java
|
||||
PublishDecision ok = publisher.evaluate(harmlessArtifact, List.of()); // 통과
|
||||
publisher.publish(breakingArtifact, ok); // 그대로 기록된다
|
||||
```
|
||||
|
||||
두 번째 줄에서 `breakingArtifact` 는 어떤 소비자 픽스처와도 대조되지 않고, 이미 발행된 버전인지도 확인되지 않은 채 이력에 들어간다. 이 클래스의 존재 이유인 두 규칙 — 소비자 컴파일 게이트와 릴리스 버전 불변성 — 을 둘 다 우회한다.
|
||||
|
||||
**왜 이 형태가 생겼나.** 판정과 기록이 두 호출로 나뉘어 있고 그 사이를 묶는 것이 호출자의 규율뿐이다. 이 저장소가 여러 가족에서 반복해 온 check-then-act 형태와 같다. 다만 여기서는 경합이 아니라 **인자 짝 맞추기**가 깨진 지점이다.
|
||||
|
||||
테스트는 안전한 형태만 쓴다 — `identicalRepublishIsAllowed` 는 `publisher.publish(artifact, publisher.evaluate(artifact, List.of()))` 로 한 줄에서 짝을 맞춘다. 그 규율을 코드가 강제하지 않는다.
|
||||
|
||||
**수정.** `PublishDecision` 이 판정 대상의 `schemaVersion`·`schemaHash` 를 들고, `publish` 가 후보와 대조한다. 또는 `evaluate` 가 발행 가능한 후보를 감싼 토큰을 돌려주고 `publish` 가 그 토큰만 받는다 — 짝이 어긋날 수 없는 형태가 된다.
|
||||
|
||||
### 17.5 P3 — `sha256:` 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다
|
||||
|
||||
같은 검사가 두 곳에 손으로 복사돼 있다.
|
||||
|
||||
```java
|
||||
// GrpcDescriptorArtifact.requireDigest
|
||||
if (digest == null || !digest.startsWith("sha256:") || digest.length() < 15) throw …;
|
||||
|
||||
// GrpcSchemaBaseline 정규 생성자
|
||||
if (schemaHash == null || !schemaHash.startsWith("sha256:") || schemaHash.length() < 15) throw …;
|
||||
```
|
||||
|
||||
`"sha256:"` 이 7자이므로 뒤에 8자만 있으면 통과한다. sha256 digest 는 hex 64자다.
|
||||
|
||||
그리고 이 헐거움이 테스트에 이미 드러나 있다.
|
||||
|
||||
```java
|
||||
assertThat(policy.unchangedFromBaseline("sha256:ffffffffffffffffffffffffffffffff")).isFalse();
|
||||
```
|
||||
|
||||
32자 — sha256 이 아니다. 여기서는 "다른 해시" 역할이라 결과가 바뀌지 않지만, 형식 검사가 이런 값을 유효한 해시로 받는다는 사실 자체가 이 값 객체의 주장("the hashes that prove which bytes it was built from")을 약하게 만든다.
|
||||
|
||||
**수정.** `sha256:` 뒤 64자 hex 를 정규식으로 요구하고, 검사를 한 곳에 둔다 — 두 record 가 같은 규칙을 각자 적고 있는 지금 형태에서는 한쪽만 조여도 다른 쪽이 남는다.
|
||||
|
||||
### 확인된 설계(문제 아님)
|
||||
|
||||
- **Buf CLI 를 부르지 않고 같은 판정을 계산한 것과 그 근거** — 바이너리가 없을 때 조용히 통과하는 게이트보다 낫다.
|
||||
- **파괴적 범주를 소스 파괴 감지 여부로 나눈 것** — 유선 호환만 보면 이름 변경이 호환으로 통과한다.
|
||||
- **기준선을 릴리스에 고정한 것** — 커밋 대 커밋 비교가 답하는 질문이 다르다.
|
||||
- **생성물 경로를 빌드 디렉터리로 강제한 것.**
|
||||
- **생성자를 하나로 못박고 버전 출처를 관리 플랫폼으로 제한한 것.**
|
||||
- **소비자 요구 사항을 손으로 적지 않고 소스에서 유도한 것** — 손으로 적은 목록이 갱신을 멈춘다.
|
||||
- **파괴 종류를 셋으로 나눠 보고하는 것** — 하나의 개수로 합치지 않는다.
|
||||
- **근사임을 자바독에 명시하고 그 한계의 근거를 ADR 로 지목한 것.**
|
||||
- **픽스처를 의존이 아니라 테스트 리소스로 커밋한 것** — 픽스처의 `build.gradle.kts` 가 스키마 산출물을 `1.4.0` 으로 고정하고, 그 이유("a fixture that floats to the latest version cannot detect a break, because it is always built against the schema it is meant to be testing")를 파일 안에 적어 두었다.
|
||||
- **요구 사항이 빈 픽스처를 거부한 것** — 아무것도 요구하지 않는 픽스처는 모든 스키마를 통과시킨다.
|
||||
- **`PublishDecision` 정규 생성자가 허용과 차단 사유의 모순을 거부한 것** — 허용인데 차단 사유가 있거나, 거부인데 사유가 없으면 던진다.
|
||||
- **테스트가 픽스처를 클래스로더로 실제 파일에서 읽는 것** — 유도 규칙을 리터럴이 아니라 커밋된 텍스트에 대고 돌린다.
|
||||
|
||||
---
|
||||
|
||||
## Source anchors
|
||||
|
||||
```
|
||||
src/grpc/grpc-codegen/build.gradle:1-12
|
||||
main/java/…/codegen/GrpcConsumerFixture.java:1-158
|
||||
main/java/…/codegen/GrpcSchemaArtifactPublisher.java:1-113
|
||||
main/java/…/codegen/GrpcCodegenManifest.java:1-81
|
||||
main/java/…/codegen/GrpcBufPolicy.java:1-75
|
||||
main/java/…/codegen/GrpcGeneratedPackagePolicy.java:1-68
|
||||
main/java/…/codegen/GrpcDescriptorArtifact.java:1-59
|
||||
main/java/…/codegen/GrpcCodegenOutput.java:1-58
|
||||
main/java/…/codegen/GrpcBreakingCategory.java:1-43
|
||||
main/java/…/codegen/GrpcSchemaBaseline.java:1-37
|
||||
test/java/…/codegen/GrpcDescriptorArtifactTest.java:1-248
|
||||
test/java/…/codegen/GrpcBufPolicyTest.java:1-88
|
||||
test/java/…/codegen/GrpcCodegenManifestTest.java:1-78
|
||||
test/resources/consumer-fixtures/v1/src/main/java/fixture/DocumentClientFixture.java:1-33
|
||||
test/resources/consumer-fixtures/v1/build.gradle.kts:1-17
|
||||
grpc/grpc-testkit/…/release/GrpcStableReleaseGate.java:3,35-38 (PublishDecision 소비 지점)
|
||||
grpc/grpc-testkit/build.gradle:55 · grpc/grpc-spring-boot-starter/build.gradle:19 (의존 선언)
|
||||
```
|
||||
Reference in New Issue
Block a user