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:
DongHyeonka
2026-09-04 22:51:59 +09:00
co-authored by Claude Opus 5
parent 43bccd08a8
commit b2963105a8
5017 changed files with 372751 additions and 4943 deletions
@@ -0,0 +1,141 @@
---
kind: CASE
slug: a05-f019-ssot
title: 정본이 어디인지 주석에 적어 두고 정의는 다시 타이핑한다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a05-f019-ssot
evidenceCapturedOn: 2026-09-02
assets:
- key: a05-f019-ssot
file: ../../../final/evidence/rendered/a05-f019-ssot.svg
evidence:
- ../../../final/evidence/raw/a05-f019-ssot.txt
source:
- 원본 분석 절은 `analysis/05-adapter-outbound-persistence-jpa.md` §51 이다. 두 목록의 이름과 차이가 `postgresql` 과 `h2` 라는 것이 §51.1 과 §51.2 에, 리프 목록이 바깥 소비자를 스캔하지 않는다는 것이 §51.3 에 있다. 같은 문서 §55 의 backlog 가 이것을 P2/P3 아키텍처 거버넌스 강화로 분류한다.
- 사본의 javadoc 이 정본이 어디인지 적어 두고도 그 값을 코드로 읽지 않는다는 것은 여기서 확인했다.
---
# 정본이 어디인지 주석에 적어 두고 정의는 다시 타이핑한다
리프가 내보내는 패키지 목록을 선언하고, 컴포지션 루트의 소비자 규칙이 같은 목록을 자기 안에 다시 적는다. 사본의 javadoc 은 정본이 리프 쪽이라고 이름으로 적지만, 그 이름을 코드로 읽는 곳은 없다. 두 목록은 이미 두 항목 다르다.
## 관계
- **문서의 수치는 세지 말고 파생하거나 게이트로 붙든다**
문서의 수치를 세는 대신 파생하거나 게이트로 붙들라는 규칙이다.
- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다**
한쪽에 항목을 더해도 다른 쪽이 조용한 형태가 같다.
- **같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다**
같은 값을 두 곳이 각자 적고 있어 어느 쪽이 정본인지 정해야 한다.
## 문제
이 리프에는 jar 하나에 공개 구현 타입이 많이 들어 있다. 그래서 자바 접근 제어와 아키텍처 노출 목록을 각각 따로 둔다.
내보낼 패키지 목록을 두 곳이 각각 들고 있다.
## 결론
리프의 경계 시험 쪽에는 열 개가 선언돼 있다. springdata 와 querydsl 은 거기 없다.
컴포지션 루트의 아키텍처 시험이 같은 열 개를 자기 안에 다시 적고, 벤더 진입점 둘을 더 넣는다. postgresql 과 h2 다. 이유는 그 자리 주석에 적혀 있다. 벤더 설정이 코어 JPA 설정을 임포트하는 방향이라 컴포지션 루트가 대신 막을 단일 내부 진입점이 없고, 방향을 뒤집으면 패키지 순환이 생겼다는 것이다.
사본의 javadoc 에는 이 목록이 리프의 export 허용 목록을 옮겨 적은 것이고 정의는 리프의 경계 시험에 있다고 적혀 있다.
그 문장이 컴포지션 루트에서 그 클래스를 언급하는 유일한 줄이다. 선언 파일 밖에서 그 목록 상수를 참조하는 자바 코드는 저장소 전체에 0 이다. 어느 쪽이 정본인지는 산문이 말하고, 값은 사람이 옮겨 적는다.
리프 목록은 바깥 소비자를 검사하지도 않는다. 그 목록을 쓰는 시험 둘은 목록에 적힌 패키지가 실제로 있는지, 그 패키지가 카탈로그의 거버넌스 대상인지를 본다. 임포트 관계는 컴포지션 루트 쪽 규칙이 따로 본다.
그래서 두 시험 모두 통과한다. 통과는 각자의 규칙을 만족한다는 뜻이고, 두 목록이 같다는 뜻은 아니다. 지금 이미 두 항목 다르다.
권고는 등록부를 한 곳에 두고 두 검사가 같은 데이터를 보게 하라는 것이다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 두 목록의 항목 추출과 집합 대조, 정본 참조 계수, 리프 시험의 검사 범위 확인
소스 수정 : x
실행 : 없음. 정적 검색과 집합 연산이다.
## 재현 조건
1. 리프 경계 시험의 export 목록을 읽는다.
2. 컴포지션 루트의 소비자 규칙 안에 있는 같은 이름의 목록을 읽는다.
3. 두 집합을 뽑아 차집합을 구한다.
4. 사본의 javadoc 이 정본을 어떻게 지목하는지 읽는다.
5. 컴포지션 루트에서 그 정본 클래스를 언급하는 줄과, 선언 파일 밖에서 그 상수를 참조하는 코드를 각각 센다.
6. 리프 목록을 쓰는 시험 둘이 무엇을 확인하는지 읽는다.
## 본문
<!-- body:start -->
이 리프는 하나의 jar 안에 공개 구현 타입이 많다. 그래서 자바의 `public` 과 아키텍처가 내보내는 패키지를 따로 관리한다.
내보내는 패키지 목록이 두 곳에 있다.
## 두 목록과 그 차이
:::evidence key="a05-f019-ssot" alt="리프 경계 시험이 선언하는 export 패키지 목록, 컴포지션 루트의 소비자 규칙 안에 다시 적힌 같은 목록과 거기 더해진 벤더 진입점 둘과 그 이유 주석, 두 집합의 크기와 차집합, 사본의 javadoc 이 정본을 지목하는 줄과 그 정본 클래스를 언급하는 줄 수와 선언 파일 밖의 상수 참조 수, 그리고 리프 목록을 쓰는 두 시험이 무엇을 보는지를 출력한 터미널 기록." caption="리프 10개와 루트 12개, 차이는 postgresql 과 h2 · 사본 javadoc 이 정본을 어디라고 적는지 · 그 클래스 언급 1줄은 그 주석뿐 · 상수 참조 0 · 리프 시험 둘은 목록의 자기 정합만 확인 — 65줄 · exit 0" zoom="true"
:::
리프의 경계 시험이 열 개를 선언한다.
```java
private static final Set<String> EXPORTED_PACKAGES =
Set.of(
"api", "notification.configuration", "transaction", "security",
"observation", "migration", "hibernate", "fileserver", "failure", "config");
```
컴포지션 루트의 아키텍처 시험은 같은 열 개에 둘을 더 적는다. 그 자리의 주석이 이유를 적는다.
```text
The two vendor entry points. A vendor configuration imports the core JPA config rather
than the reverse, so there is no single internal entry the composition root could gate
instead — inverting the import to make one produced a package cycle.
```
집합으로 빼면 차이가 정확히 둘이다.
```text
리프 10개 / 루트 12개
루트에만 있는 것: ['h2', 'postgresql']
리프에만 있는 것: []
```
## 사본이 정본을 지목하는 방식
```text
The list is the leaf's export allowlist, mirrored here because this is the consumer side
of the same boundary. JpaModuleBoundaryTest owns the definition.
```
그 문장이 컴포지션 루트에서 리프 경계 시험을 언급하는 유일한 줄이다. 선언 파일 밖에서 `EXPORTED_PACKAGES` 를 참조하는 자바 코드는 저장소 전체에 0 이다.
정본을 지목하는 것은 산문이고, 값은 손으로 옮겨져 있다.
## 리프 목록은 바깥 소비자를 보지 않는다
그 목록을 쓰는 시험은 둘이다. 목록에 적힌 패키지가 소스 트리에 실제로 있는지, 그리고 그 패키지가 카탈로그의 거버넌스 대상인지를 본다.
누가 무엇을 임포트하는지는 컴포지션 루트의 규칙이 따로 본다. split 은 실수가 아니라 역할 분리의 결과이고, 그래서 어느 쪽도 상대를 검사할 이유가 없다.
## 두 시험이 각각 무엇을 보는가
두 시험의 입력이 겹치지 않는다. 리프 시험은 리프의 소스 트리와 자기 카탈로그만, 루트 시험은 루트의 임포트 그래프와 자기 목록만 읽는다.
한쪽 목록이 늘어도 다른 쪽 단언의 입력은 그대로다. 실패할 근거가 없다.
## 고칠 방향
내보내는 패키지 등록부를 한 곳으로 옮기고, 리프의 패키지 그래프 검사와 소비자 규칙이 같은 데이터를 읽게 한다. 분석 문서의 권고가 그것이다.
## 확인하지 못한 것
한쪽 목록에 항목을 더해 다른 쪽이 조용한지 실행으로 확인하지 않았다. 두 선언을 대조하고 참조를 센 것까지가 확인 범위다.
<!-- body:end -->
@@ -0,0 +1,186 @@
---
kind: CASE
slug: a06-f016-flamingock
title: javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a06-f016-flamingock
evidenceCapturedOn: 2026-09-02
assets:
- key: a06-f016-flamingock
file: ../../../final/evidence/rendered/a06-f016-flamingock.svg
- key: a06-f016-flamingock-probe
file: ../../../final/evidence/rendered/a06-f016-flamingock-probe.svg
evidence:
- ../../../final/evidence/raw/a06-f016-flamingock.txt
- ../../../final/evidence/raw/a06-f016-flamingock-probe.txt
source:
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L942 이다. 등급은 P3 이다. 그 절이 담은 것은 넷이다. 펜스 반환값과 그 근거, javadoc 마지막 문장의 범위, 82행 검사가 적용 스트림보다 앞이라는 위치, 그리고 시험이 이 조합을 다루지 않는다는 것.
- 빈 목록도 같은 문장으로 거절된다는 것, 같은 마이그레이션이 펜스가 있는 리스에서는 완료된다는 것, 154행이 재개 가능한 마이그레이션에만 걸리는 펜스 조건이라는 것, 그리고 검사를 옮겨도 157행에서 다시 막힌다는 것은 이 기록에서 확인했다.
---
# javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다
잠금 어댑터는 펜스로 미지정 값을 돌려주고 그 근거를 javadoc 에 적는다. 이어지는 한 문장이 실행자의 거부 범위를 재개 가능한 마이그레이션으로 한정한다. 실제 조건은 리스의 펜스 값 하나여서 마이그레이션이 하나도 없어도 거부한다.
## 관계
- **fenced lease — 만료 시각만으로는 부족한 이유**
펜싱이 무엇을 보장하는지 다룬 개념이다.
- **recordApplied가 펜스를 비교하지 않아 밀려난 실행자가 원장을 차지한다**
같은 원장의 펜스 처리 사례다.
- **문서가 UUIDv7이라 말하고 생성되는 것은 v4다**
두 사례 모두 javadoc 이 적은 것과 코드가 만드는 값이 다르다.
## 문제
펜스 메서드가 미지정 값을 돌려주는 것과 그 근거는 옳다. 로컬 카운터로 펜싱을 흉내내면 펜싱처럼 보이면서 아무것도 보호하지 않고, 프로세스마다 따로 세는 수는 다른 프로세스가 무엇을 획득했는지에 대해 아무 말도 하지 않는다.
어긋난 것은 같은 javadoc 의 마지막 한 문장이다. 실행자가 바로 이 이유로 재개 가능한 마이그레이션을 거부한다고 적는다.
## 결론
그 문장이 가리키는 장치는 실재한다. 재개 가능한 마이그레이션만 체크포인트를 남기고, 그 저장에 펜스가 넘어가며, 원장은 미지정 펜스로 들어온 체크포인트를 거절한다. 그 자리를 지키는 조건은 재개 가능성 하나만 본다.
닿지 못할 뿐이다. 진입점의 세 관문 중 세 번째가 리스의 펜스 값만 보고 던지고, 그 자리는 적용 스트림보다 앞이다. 조건에 마이그레이션이 들어가지 않으므로 목록이 비어 있어도 같은 문장이 나온다.
탐침으로 확인했다. 빈 목록을 이 리스로 적용해도 거절된다. 목록에 아무것도 없어 재개 가능한지 여부가 존재하지 않는데도 같다. 체크포인트를 만들지 않는 마이그레이션 한 건도 마찬가지이고, 그 한 건은 펜스가 있는 리스에서는 완료로 끝난다.
거절 문장 자체는 정확하다. 펜스 없는 리스는 멈춘 실행자를 배제할 수 없다고 말하고 재개 가능성을 언급하지 않는다. 그리고 거절 자체가 안전한 선택이다. 문제는 javadoc 을 읽고 이 어댑터를 고른 쪽이 재개 불가능한 마이그레이션은 돈다고 읽는다는 것이다.
수정은 사실상 하나다. javadoc 을 실제 범위로 고치는 것이다. 검사를 스트림 안으로 옮기는 쪽은 둘에 막힌다. 재개 가능성은 실행이 체크포인트를 돌려준 뒤에야 드러나므로 실행 전에 판별할 수 없고, 옮기더라도 적용이 끝난 뒤 같은 미지정 값이 원장 기록으로 넘어가 거기서 거절된다. 거절 시점이 실행 전에서 실행 후로 밀릴 뿐이다.
지금 배포를 멈추는 결함은 아니다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 이 어댑터를 만드는 곳은 시험 두 곳이며 그 시험들은 적용을 부르지 않는다. 이 리프를 가져다 조립하는 쪽이 처음 실행할 때 드러난다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 밀폐된 탐침으로 실행자 진입점 호출
소스 수정 : x
## 재현 조건
1. 잠금 어댑터의 펜스 메서드가 돌려주는 값과 그 javadoc 을 읽는다.
2. 진입점이 지나는 세 관문과 적용 스트림의 줄 번호를 확인한다.
3. 그 파일에서 리스의 펜스를 읽는 곳을 전수로 세고, 각각이 그 값을 어디로 넘기는지 확인한다.
4. 원장이 미지정 펜스를 어떻게 다루는지 읽는다.
5. 마이그레이션 인터페이스에 재개 가능성을 선언하는 멤버가 있는지 확인한다.
6. 어댑터로 빈 목록을 적용한다.
7. 체크포인트를 만들지 않는 마이그레이션 한 건을 같은 리스로, 그리고 펜스가 있는 리스로 각각 적용한다.
8. 어댑터가 나오는 곳과 프로덕션 참조를 전수로 센다.
## 본문
<!-- body:start -->
잠금 어댑터의 펜스 메서드는 미지정 값을 돌려주고, javadoc 이 그 선택의 근거를 적는다.
## 근거 문단과 그 뒤의 한 문장
:::evidence key="a06-f016-flamingock" alt="잠금 어댑터의 펜스 메서드와 그 javadoc, 실행자 진입점이 지나는 세 관문, 그 파일에서 리스의 펜스를 읽는 곳 전수와 각각의 줄 번호, 원장이 미지정 펜스를 거절하는 자리, 마이그레이션 인터페이스의 멤버 목록, 이 어댑터가 나오는 곳 전수, 실행자와 원장과 잠금의 프로덕션 참조, 그리고 마이그레이션을 적용하는 진입점 전수를 출력한 터미널 기록." caption="펜스는 UNFENCED 를 반환하고 javadoc 은 재개 가능한 것을 거부한다고 적음 · 관문은 보유 검사 74행, 목록 검증 80행, 펜스 검사 82행이고 적용 스트림은 93행 · 펜스를 읽는 곳은 82·154·157행이고 뒤 둘은 원장으로 넘어가 미지정이면 거절 · 인터페이스에 재개 가능성 멤버 없음 · 프로덕션 참조 0 — 73줄 · exit 0" zoom="true"
:::
```java
/**
* Unfenced: the engine's lock exposes no monotonic token.
*
* <p>Reported honestly rather than synthesised from a local counter, which would look like
* fencing and protect nothing — a per-process counter says nothing about what another process
* acquired. The runner refuses resumable migrations under an unfenced lease for exactly this
* reason.
*/
```
앞 두 문장은 왜 흉내내지 않았는지를 적는다. 문제는 마지막 문장이 거절 범위를 재개 가능한 마이그레이션으로 한정한 것이다.
## 그 문장이 가리키는 장치는 실재한다
실행자가 리스의 펜스를 읽는 곳은 셋이다.
```text
82: if (lock.fence() == MongoMigrationLock.UNFENCED) {
154: result.resumePoint().ifPresent(checkpoint -> ledger.saveCheckpoint(checkpoint, lock.fence()));
157: migration.id(), migration.checksum(), context.operator(), clock.instant(), lock.fence());
```
154행은 재개 지점을 남긴 마이그레이션에만 걸린다. 그 값을 받은 원장은 미지정이면 거절한다.
```java
private static void requireCurrentFence(long fence, String what) {
if (fence == MongoMigrationLock.UNFENCED) {
```
재개 가능성에만 반응하는 펜스 조건이 그 자리에 있다. javadoc 의 마지막 문장은 근거 없는 착오가 아니다.
## 세 번째 관문이 먼저 던진다
```text
74: if (!lock.held()) {
80: migrations.forEach(this::validate);
82: if (lock.fence() == MongoMigrationLock.UNFENCED) {
93: return migrations.stream()
```
82행의 조건은 리스의 펜스 값 하나다. 80행은 목록을 순회하지만 원장과 체크섬을 볼 뿐이고, 82행은 그 결과도 마이그레이션도 참조하지 않는다. 154행에 닿으려면 93행의 스트림에 들어가야 하는데 82행이 그보다 앞이다.
그 자리에서 나오는 문장도 재개 가능성을 말하지 않는다.
```java
"this migration lease exposes no fencing token, so a stalled runner cannot be excluded;"
+ " use a fenced lease or run the migration inside a maintenance window with the"
+ " application stopped"
```
## 빈 목록도 거절된다
:::evidence key="a06-f016-flamingock-probe" alt="잠금 어댑터가 보고하는 보유 여부와 펜스 값, 빈 마이그레이션 목록과 체크포인트를 만들지 않는 마이그레이션 한 건을 그 리스로 적용한 결과와 거절 문장, 그리고 같은 마이그레이션을 펜스가 있는 리스로 적용한 결과를 출력한 터미널 기록." caption="펜스는 -1 · 빈 목록도 거절, 재개 불가 한 건도 같은 문장으로 거절 · 같은 마이그레이션이 펜스가 있는 리스에서는 COMPLETED — 11줄 · exit 0" zoom="true"
:::
이 어댑터로 리스를 만들고 아무것도 없는 목록을 적용했다.
```text
[검사가 마이그레이션을 보기 전에 걸린다]
마이그레이션 0건 -> 거절, MongoOperationRejectedException
this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ...
재개 불가 1건 -> 거절, MongoOperationRejectedException
this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ...
```
목록이 비어 있으면 재개 가능한지 여부가 존재하지 않는데 같은 문장이 나온다. 한 건을 넣어도 결과는 같다. 그 한 건은 한 번의 호출로 완료를 돌려주므로 체크포인트를 남기지 않는다.
같은 마이그레이션을 펜스가 있는 리스로 적용하면 이렇게 된다.
```text
[같은 마이그레이션, 펜스가 있는 리스]
재개 불가 1건 -> 실행, 결과 1건 COMPLETED
```
거절의 대상은 마이그레이션이 아니라 리스다.
## 검사를 옮기는 수정은 두 곳에 막힌다
마이그레이션 인터페이스에는 재개 가능성을 선언하는 멤버가 없다.
```text
17: MongoMigrationId id();
20: MongoMigrationChecksum checksum();
23: MongoMigrationPrecondition precondition();
26: MongoMigrationResult execute(MongoMigrationContext context);
29: MongoMigrationPostcondition postcondition();
```
재개 가능한지는 `execute` 가 체크포인트를 돌려준 뒤에야 드러난다. 그리고 82행을 스트림 안으로 옮겨도 157행이 같은 미지정 값을 원장 기록으로 넘긴다. 원장은 그것을 거절하므로, 데이터베이스는 바뀌고 원장에는 아무것도 남지 않는 상태가 된다.
## 이 어댑터가 나오는 곳
어댑터를 만드는 곳은 시험 두 곳이다. 하나는 획득 실패를, 다른 하나는 해제와 해제 뒤 갱신 거절을 확인한다. 적용을 부르는 시험은 없어서 javadoc 과 82행의 어긋남이 시험에도 걸리지 않는다.
조립 쪽도 같다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 자동 구성이 이 패키지에서 만드는 빈도 없다. 지금 도는 배포를 멈추는 결함이 아니라, 이 리프를 가져다 조립하는 쪽이 처음 실행하는 순간에 드러날 결함이다.
## 확인하지 못한 것
이 어댑터를 실제 배포에서 골랐을 때의 운영 경험은 다루지 않았다. 확인한 것은 세 번째 관문의 조건과 그 조건이 만드는 거절의 범위다.
<!-- body:end -->
@@ -0,0 +1,86 @@
---
kind: CASE
slug: grpc-advanced-bootstrap-f02
title: 승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-advanced-bootstrap-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-advanced-bootstrap-f02
file: ../../../final/evidence/rendered/grpc-advanced-bootstrap-f02.svg
evidence:
- ../../../final/evidence/raw/grpc-advanced-bootstrap-f02.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-advanced-bootstrap.md#L182 이다.
module: grpc-advanced-bootstrap
priority: P3
---
# 승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다
던지는 경우는 널과 from == to 둘뿐이다. "이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다.
## 문제
던지는 경우는 널과 from == to 둘뿐이다.
"이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다.
## 결론
그래서 하향 전이가 승격 규칙으로 판정된다.
능력을 철회하려는 결정이 증거 부족을 이유로 막힌다.
방향이 뒤집혀 있다.
지금은 도달성이 낮다 — 이 게이트를 부르는 production 코드가 없고 테스트도 상향 전이만 넣는다.
기록하는 이유는 javadoc 이 그 거부를 이미 약속했다는 점이다.
수정은 to.ordinal() 이 아니라 등급의 서열을 명시한 뒤 상향 전이만 받고 나머지는 던지는 것이다.
철회는 별도 경로가 필요하다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : 승격 게이트가 던지는 조건 전수 확인과 javadoc 이 약속한 거부의 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-advanced-bootstrap.md#L182 에 있다.
## 본문
<!-- body:start -->
던지는 경우는 널과 `from == to` 둘뿐이다. "이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다.
## 게이트가 던지는 두 경우
:::evidence key="grpc-advanced-bootstrap-f02" alt="분석 문서 analysis/grpc/grpc-advanced-bootstrap.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-bootstrap.md 발췌 — 15줄" zoom="true"
:::
## 하향 전이가 승격 규칙으로 판정된다
능력을 철회하려는 결정이 증거 부족을 이유로 막힌다. 방향이 뒤집혀 있다.
## 지금은 도달성이 낮다
이 게이트를 부르는 production 코드가 없고 테스트도 상향 전이만 넣는다. 기록하는 이유는 javadoc 이 그 거부를 이미 약속했다는 점이다.
## 수정
`to.ordinal()` 이 아니라 등급의 서열을 명시한 뒤 상향 전이만 받고 나머지는 던지는 것이다. 철회는 별도 경로가 필요하다.
## 확인하지 못한 것
하향 전이를 실제로 넣어 게이트의 판정을 관측하지 않았다. 던지는 조건이 널과 from == to 둘뿐이라는 것으로 판정했다.
<!-- body:end -->
@@ -0,0 +1,78 @@
---
kind: CASE
slug: grpc-advanced-edition-f02
title: 승격 차단 목록에 담금 기간과 실환경 항목이 없다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-advanced-edition-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-advanced-edition-f02
file: ../../../final/evidence/rendered/grpc-advanced-edition-f02.svg
evidence:
- ../../../final/evidence/raw/grpc-advanced-edition-f02.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-advanced-edition.md#L206 이다.
module: grpc-advanced-edition
priority: P3
---
# 승격 차단 목록에 담금 기간과 실환경 항목이 없다
GrpcEdition2024Gate.promotionBlockers 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR. 같은 가족의 GrpcAdvancedPromotionGate 는 EDITION_2024 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다.
## 문제
GrpcEdition2024Gate.promotionBlockers 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR.
같은 가족의 GrpcAdvancedPromotionGate 는 EDITION_2024 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다.
## 결론
두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다.
두 게이트가 각각 다른 것을 묻는다고 볼 수도 있다 — 하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도.
다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다.
승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다.
수정은 promotionBlockers 가 GrpcAdvancedPromotionGate.evaluate 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : GrpcEdition2024Gate 참조 9건 검색과 GrpcAdvancedPromotionGate 의 요구 항목 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-advanced-edition.md#L206 에 있다.
## 본문
<!-- body:start -->
`GrpcEdition2024Gate.promotionBlockers` 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR.
## GrpcEdition2024Gate 참조 위치
:::evidence key="grpc-advanced-edition-f02" alt="코드베이스에서 GrpcEdition2024Gate 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcEdition2024Gate 코드베이스 검색 — 9줄 · exit 0" zoom="true"
:::
## 같은 가족의 다른 게이트는 다른 기준을 쓴다
`GrpcAdvancedPromotionGate``EDITION_2024` 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. 두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다.
## 각각 다른 것을 묻는다고 볼 수도 있다
하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도다. 다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다. 승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다. 수정은 `promotionBlockers``GrpcAdvancedPromotionGate.evaluate` 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다.
## 확인하지 못한 것
두 게이트를 실제로 실행해 판정 차이를 관측하지 않았다. 차단 목록의 구성 요소 대조로 판정했다.
<!-- body:end -->
@@ -0,0 +1,88 @@
---
kind: CASE
slug: grpc-advanced-resilience-f01
title: 부트스트랩 대조가 문서 어디든의 부분 문자열을 본다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-advanced-resilience-f01
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-advanced-resilience-f01
file: ../../../final/evidence/rendered/grpc-advanced-resilience-f01.svg
evidence:
- ../../../final/evidence/raw/grpc-advanced-resilience-f01.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-advanced-resilience.md#L136 이다.
module: grpc-advanced-resilience
priority: P3
---
# 부트스트랩 대조가 문서 어디든의 부분 문자열을 본다
세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다. JSON 파서를 쓰지 않은 이유는 자바독이 밝힌다 — 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다.
## 문제
세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다.
JSON 파서를 쓰지 않은 이유는 자바독이 밝힌다 — 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다.
## 결론
그 판단 자체는 이 저장소의 다른 결정들과 일관된다.
다만 검사의 형태가 그 판단보다 느슨하다.
"tls" 가 문서 어디에든 있으면 통과한다.
통제 평면 채널이 insecure 로 설정되어 있고 다른 곳(예: 서버 리스너 설정)에 tls 라는 낱말이 있으면 두 번째 검사가 지나간다.
자원 이름공간이 주석·다른 필드·다른 서버 항목에 있어도 통과한다.
세 번째 검사가 막으려는 것은 "이 클라이언트가 자기 이름공간 밖을 구독하는 것" 인데, 문자열이 어딘가에 있다는 것은 그것이 이 클라이언트의 구독 대상이라는 뜻이 아니다.
그리고 이 검사가 막으려는 실패는 자바독이 스스로 "조용하다" 고 적은 것이다 — 아무것도 오류가 되지 않는 종류다.
느슨한 검사와 조용한 실패의 조합이 이 항목을 기록하는 이유다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : 세 검사가 쓰는 대조 방식과 자바독이 밝힌 파서 미사용 근거의 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-advanced-resilience.md#L136 에 있다.
## 본문
<!-- body:start -->
세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다.
## 세 검사가 쓰는 대조 방식
:::evidence key="grpc-advanced-resilience-f01" alt="분석 문서 analysis/grpc/grpc-advanced-resilience.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-resilience.md 발췌 — 15줄" zoom="true"
:::
## 파서를 쓰지 않은 판단 자체는 일관된다
자바독이 밝히듯 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다. 다만 검사의 형태가 그 판단보다 느슨하다.
## 낱말이 어디에 있든 통과한다
`"tls"` 가 문서 어디에든 있으면 통과한다. 통제 평면 채널이 `insecure` 로 설정되어 있고 다른 곳(예: 서버 리스너 설정)에 `tls` 라는 낱말이 있으면 두 번째 검사가 지나간다. 자원 이름공간이 주석·다른 필드·다른 서버 항목에 있어도 통과한다. 세 번째 검사가 막으려는 것은 "이 클라이언트가 자기 이름공간 밖을 구독하는 것" 인데, 문자열이 어딘가에 있다는 것은 그것이 이 클라이언트의 구독 대상이라는 뜻이 아니다.
## 느슨한 검사와 조용한 실패의 조합
이 검사가 막으려는 실패는 자바독이 스스로 "조용하다" 고 적은 것이다. 수정은 파서를 들이지 않고도 가능하다 — `"channel_creds"` 를 포함하는 객체 범위 안에서 `"type"` 값을 찾는 정도의 구조 인식이면 두 번째 검사가 실제 조건에 가까워진다. 또는 파서를 테스트 범위에만 두고 이 가드는 형태를 좁힌 정규식으로 바꾼다.
## 확인하지 못한 것
실제 xDS 통제 평면을 세워 부트스트랩 대조를 재현하지 않았다. 세 검사의 문자열 포함 조건으로 판정했다.
<!-- body:end -->
@@ -0,0 +1,76 @@
---
kind: CASE
slug: grpc-client-f04
title: 프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-client-f04
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-client-f04
file: ../../../final/evidence/rendered/grpc-client-f04.svg
evidence:
- ../../../final/evidence/raw/grpc-client-f04.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-client.md#L199 이다.
module: grpc-client
priority: P3
---
# 프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다
구현된 것은 첫째와 다른 것이다. 이름이 같은 프로파일이 두 번 선언된 경우를 잡는다.
## 문제
구현된 것은 첫째와 다른 것이다.
이름이 같은 프로파일이 두 번 선언된 경우를 잡는다.
## 결론
javadoc 이 든 둘째는 이름이 다르고 대상이 같은 경우인데, 그 검사가 없다.
지도는 이름을 키로 쓰므로 같은 대상을 가리키는 두 이름은 서로를 만나지 않는다.
그리고 둘째가 실제로 더 찾기 어려운 형태다 — 이름이 같으면 설정 결속이 먼저 실패하거나 나중 것이 이기지만, 이름이 다르면 조용히 두 채널이 생긴다.
수정은 대상과 설정을 키로 하는 두 번째 지도를 두고 역방향 중복을 보고하는 것이다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : javadoc 이 든 두 실수와 검증기에 실제로 구현된 검사의 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-client.md#L199 에 있다.
## 본문
<!-- body:start -->
구현된 것은 javadoc 이 든 첫째와 **다른 것**이다 — 이름이 같은 프로파일이 두 번 선언된 경우를 잡는다.
## 검증기가 실제로 잡는 실수
:::evidence key="grpc-client-f04" alt="분석 문서 analysis/grpc/grpc-client.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-client.md 발췌 — 15줄" zoom="true"
:::
## 둘째에 해당하는 검사가 없다
javadoc 이 든 둘째는 **이름이 다르고 대상이 같은** 경우인데, 지도는 이름을 키로 쓰므로 같은 대상을 가리키는 두 이름은 서로를 만나지 않는다.
## 둘째가 더 찾기 어려운 형태다
이름이 같으면 설정 결속이 먼저 실패하거나 나중 것이 이기지만, 이름이 다르면 조용히 두 채널이 생긴다. 수정은 대상과 설정을 키로 하는 두 번째 지도를 두고 역방향 중복을 보고하는 것이다.
## 확인하지 못한 것
검증되지 않는 둘째 실수를 담은 프로파일로 시작을 시도하지 않았다. 검증기 본문의 검사 목록으로 판정했다.
<!-- body:end -->
@@ -0,0 +1,74 @@
---
kind: CASE
slug: grpc-discovery-f02
title: 리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-discovery-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-discovery-f02
file: ../../../final/evidence/rendered/grpc-discovery-f02.svg
evidence:
- ../../../final/evidence/raw/grpc-discovery-f02.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-discovery.md#L177 이다.
module: grpc-discovery
priority: P3
---
# 리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다
javadoc 은 "Checks a discovery configuration for the things that look right and are not" 라고 적는다. 실제로 담긴 규칙은 균형 정책의 무의미함 하나다.
## 문제
javadoc 은 "Checks a discovery configuration for the things that look right and are not" 라고 적는다.
실제로 담긴 규칙은 균형 정책의 무의미함 하나다.
## 결론
나머지 위험 조합은 GrpcResolverProfile 정규 생성자가 이미 거부하므로 결과적으로 빈틈은 아니다.
다만 목록으로 보고하는 API 형태와 규칙 하나라는 내용이 어긋나 있어, 다음 사람이 여기에 규칙을 더할 자리로 읽거나 이미 여러 규칙이 있다고 읽는다.
§17.1 이 실제로 그 자리다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : GrpcResolverProfile 참조 16건 검색과 검증기에 실제로 담긴 규칙 수 확인
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-discovery.md#L177 에 있다.
## 본문
<!-- body:start -->
javadoc 은 "Checks a discovery configuration for **the things** that look right and are not" 라고 적는다. 실제로 담긴 규칙은 균형 정책의 무의미함 하나다.
## GrpcResolverProfile 참조 위치
:::evidence key="grpc-discovery-f02" alt="코드베이스에서 GrpcResolverProfile 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcResolverProfile 코드베이스 검색 — 16줄 · exit 0" zoom="true"
:::
## 결과적으로 빈틈은 아니다
나머지 위험 조합은 `GrpcResolverProfile` 정규 생성자가 이미 거부한다.
## 어긋난 것은 형태와 내용이다
목록으로 보고하는 API 형태와 규칙 하나라는 내용이 어긋나 있어, 다음 사람이 여기에 규칙을 더할 자리로 읽거나 이미 여러 규칙이 있다고 읽는다. §17.1 이 실제로 그 자리다.
## 확인하지 못한 것
시작 검증기를 통한 스킴 거부를 실행으로 확인하지 않았다. 그 검증기가 돌지 않는다.
<!-- body:end -->
@@ -0,0 +1,90 @@
---
kind: CASE
slug: grpc-observability-f02
title: deadlineRemaining 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-observability-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-observability-f02
file: ../../../final/evidence/rendered/grpc-observability-f02.svg
evidence:
- ../../../final/evidence/raw/grpc-observability-f02.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-observability.md#L213 이다.
module: grpc-observability
priority: P3
---
# deadlineRemaining 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다
§17.1 과 같은 형태가 GrpcRpcObservation 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다. 두 값을 함께 들면서 "기록된다"고 단언하는데, record(GrpcRpcObservation) 이 등록하는 meter 는 넷이다.
## 문제
§17.1 과 같은 형태가 GrpcRpcObservation 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다.
두 값을 함께 들면서 "기록된다"고 단언하는데, record(GrpcRpcObservation) 이 등록하는 meter 는 넷이다.
## 결론
queueWaitTime 은 QUEUE_WAIT 타이머로 나간다.
deadlineRemaining 은 나가는 곳이 없다 — meter 이름 상수 일곱 개 중에도 마감 잔량에 해당하는 것이 없고, tags() 에도 들어가지 않는다(태그로 넣으면 카디널리티가 터지므로 그것이 옳다).
그래서 이 성분을 읽는 코드는 unusedDeadline() 하나이고, 그 메서드의 production 호출자는 0 이다(§12.1).
§4.6 은 이 성분의 검증 비대칭(음수 허용)이 "마감을 넘긴 호출을 표현하기 위한 것" 이라고 읽었다.
그 해석은 그대로 유효하다 — 다만 그 표현이 도달하는 곳이 아직 없다.
관측값으로서는 §17.1 의 queueHighWatermark 와 같은 처지다.
queueWaitTime 과 같은 형태로 타이머를 하나 더 둔다(마감을 넘긴 경우는 unusedDeadline() 이 이미 빈 값으로 구분해 주므로 기록 대상에서 빼면 된다).
아니면 javadoc 의 "recorded" 를 "carried" 로 낮춘다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : GrpcRpcObservation 참조 5건 검색과 클래스 javadoc 의 기록 주장 대비 meter 넷 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-observability.md#L213 에 있다.
## 본문
<!-- body:start -->
§17.1 과 같은 형태가 `GrpcRpcObservation` 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다.
> "{@code deadlineRemaining} and {@code queueWaitTime} are **recorded** because they are the two numbers that explain a latency change without being latency."
## GrpcRpcObservation 참조 위치
:::evidence key="grpc-observability-f02" alt="코드베이스에서 GrpcRpcObservation 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcRpcObservation 코드베이스 검색 — 5줄 · exit 0" zoom="true"
:::
## 둘 중 하나만 나간다
`record(GrpcRpcObservation)` 이 등록하는 meter 는 넷이고, `queueWaitTime``QUEUE_WAIT` 타이머로 나간다. `deadlineRemaining` 은 나가는 곳이 없다 — meter 이름 상수 일곱 개 중에도 마감 잔량에 해당하는 것이 없고, `tags()` 에도 들어가지 않는다(태그로 넣으면 카디널리티가 터지므로 그것이 옳다).
## 읽는 코드가 하나이고 그 호출자가 0이다
`unusedDeadline()` 하나이고 production 호출자는 0 이다(§12.1). §4.6 은 이 성분의 검증 비대칭(음수 허용)이 "마감을 넘긴 호출을 표현하기 위한 것" 이라고 읽었다 — 그 해석은 그대로 유효하고, 다만 그 표현이 도달하는 곳이 아직 없다.
## 수정
`queueWaitTime` 과 같은 형태로 타이머를 하나 더 둔다(마감을 넘긴 경우는 `unusedDeadline()` 이 이미 빈 값으로 구분해 준다). 아니면 javadoc 의 "recorded" 를 "carried" 로 낮춘다.
## 확인하지 못한 것
meter 이름 상수 일곱 개와 두 record 오버로드 본문으로 판정했다. 다른 이름의 상수가 그 역할을 겸하는지는 이름만 보고 배제했다.
<!-- body:end -->
@@ -0,0 +1,92 @@
---
kind: CASE
slug: grpc-policy-f06
title: 오류 노출 거부 목록의 "호스트와 포트" 규칙이 IPv4 점표기만 본다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-policy-f06
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-policy-f06
file: ../../../final/evidence/rendered/grpc-policy-f06.svg
evidence:
- ../../../final/evidence/raw/grpc-policy-f06.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-policy.md#L305 이다.
module: grpc-policy
priority: P3
---
# 오류 노출 거부 목록의 "호스트와 포트" 규칙이 IPv4 점표기만 본다
아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 이 하나다. 클래스 javadoc 은 거부 대상을 "a stack frame, a SQL fragment, a JDBC URL, a bearer token, a host and port, a file path" 로 서술하는데, 실제로 걸리는 host 는 IPv4 점표기뿐이다.
## 문제
아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 이 하나다.
클래스 javadoc 은 거부 대상을 "a stack frame, a SQL fragment, a JDBC URL, a bearer token, a host and port, a file path" 로 서술하는데, 실제로 걸리는 host 는 IPv4 점표기뿐이다.
## 결론
IPv6 리터럴 — fe80::1, [2001:db8::1]:5432 DNS 이름과 포트 — documents-db.internal:5432, kafka-0.kafka-headless:9092 jdbc:postgresql://db/app 이 막히는 것은 host 규칙이 아니라 jdbc: 규칙 때문이다.
즉 이 구멍은 테스트에도 없다 — exposurePolicyRefusesLeakyStrings 의 아홉 사례 중 주소는 upstream 10.0.3.14:5432 refused 하나이고 IPv4 다.
닿는 경로는 mapUnknown 이다.
인식되지 않은 예외의 메시지를 safeToExpose 가 통과시키면 그대로 클라이언트로 간다.
IPv6 클러스터나 쿠버네티스 서비스 이름을 쓰는 배포에서 상류 좌표가 밖으로 나간다.
등급이 P3 인 이유는 두 가지다.
이 리프가 build-only 라 오늘 닿지 않고, 노출되는 것이 자격증명이 아니라 내부 좌표다.
다만 이 정책이 존재하는 이유 자체가 "부분 마스킹이 아니라 통째 교체" 이므로, 목록에 빠진 형태는 통째로 통과한다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : 거부 목록 아홉 패턴 전수 확인과 클래스 javadoc 의 거부 대상 서술 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-policy.md#L305 에 있다.
## 본문
<!-- body:start -->
아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 하나이고 그것이 IPv4 점표기만 본다. 클래스 javadoc 은 거부 대상을 "a stack frame, a SQL fragment, a JDBC URL, a bearer token, **a host and port**, a file path" 로 서술한다.
## 아홉 패턴 중 주소를 보는 하나
:::evidence key="grpc-policy-f06" alt="분석 문서 analysis/grpc/grpc-policy.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-policy.md 발췌 — 15줄" zoom="true"
:::
## 걸리지 않는 형태들
IPv6 리터럴(`fe80::1`, `[2001:db8::1]:5432`)과 DNS 이름과 포트(`documents-db.internal:5432`, `kafka-0.kafka-headless:9092`)다. `jdbc:postgresql://db/app` 이 막히는 것은 host 규칙이 아니라 `jdbc:` 규칙 때문이다.
## 테스트에도 없다
`exposurePolicyRefusesLeakyStrings` 의 아홉 사례 중 주소는 `upstream 10.0.3.14:5432 refused` 하나이고 IPv4 다.
## 닿는 경로
`mapUnknown` 이다. 인식되지 않은 예외의 메시지를 `safeToExpose` 가 통과시키면 그대로 클라이언트로 간다. IPv6 클러스터나 쿠버네티스 서비스 이름을 쓰는 배포에서 상류 좌표가 밖으로 나간다.
## 등급이 P3 인 이유 둘
이 리프가 build-only 라 오늘 닿지 않고, 노출되는 것이 자격증명이 아니라 내부 좌표다. 다만 이 정책이 존재하는 이유 자체가 "부분 마스킹이 아니라 통째 교체" 이므로, 목록에 빠진 형태는 통째로 통과한다.
## 확인하지 못한 것
IPv6 누출을 실제 예외 메시지로 재현하지 않았다. 아홉 패턴 중 IPv4 점표기 외에 주소 형태를 보는 것이 없음을 확인해 판정했다.
<!-- body:end -->
@@ -0,0 +1,68 @@
---
kind: CASE
slug: grpc-proto-contract-f02
title: 반환 목록이 자바독이 약속한 source order 가 아니다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-proto-contract-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-proto-contract-f02
file: ../../../final/evidence/rendered/grpc-proto-contract-f02.svg
evidence:
- ../../../final/evidence/raw/grpc-proto-contract-f02.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-proto-contract.md#L191 이다.
module: grpc-proto-contract
priority: P3
---
# 반환 목록이 자바독이 약속한 source order 가 아니다
validate 의 javadoc 은 "@return every violation found, in source order" 라고 적는다. 실제로는 파일 앞머리의 syntax·package 위반이 40번째 줄의 map 위반보다 뒤에 온다.
## 문제
validate 의 javadoc 은 "@return every violation found, in source order" 라고 적는다.
실제로는 파일 앞머리의 syntax·package 위반이 40번째 줄의 map 위반보다 뒤에 온다.
## 결론
describe() 가 file:line rule — detail 형태를 만들고 그 형태의 목적이 빌드 로그를 읽는 것이므로, 정렬이 어긋나면 리뷰 목록으로서의 값이 줄어든다.
수정은 반환 직전에 line 으로 안정 정렬하는 것이다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : validate 의 javadoc 이 약속한 정렬과 실제 위반 수집 순서의 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-proto-contract.md#L191 에 있다.
## 본문
<!-- body:start -->
`validate` 의 javadoc 은 "@return every violation found, **in source order**" 라고 적는다. 실제로는 파일 앞머리의 `syntax`·`package` 위반이 40번째 줄의 `map` 위반보다 뒤에 온다.
## javadoc 이 약속한 정렬
:::evidence key="grpc-proto-contract-f02" alt="분석 문서 analysis/grpc/grpc-proto-contract.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-proto-contract.md 발췌 — 15줄" zoom="true"
:::
## 그 형태의 목적이 빌드 로그를 읽는 것이다
`describe()``file:line rule — detail` 형태를 만들므로, 정렬이 어긋나면 리뷰 목록으로서의 값이 줄어든다. 수정은 반환 직전에 `line` 으로 안정 정렬하는 것이다.
## 확인하지 못한 것
실제 빌드 로그에서 순서 뒤바뀜을 관측하지 않았다. 수집 순서 코드로 판정했다.
<!-- body:end -->
@@ -0,0 +1,78 @@
---
kind: CASE
slug: grpc-server-f02
title: 원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:grpc-server-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: grpc-server-f02
file: ../../../final/evidence/rendered/grpc-server-f02.svg
evidence:
- ../../../final/evidence/raw/grpc-server-f02.txt
source:
- 원본 분석 절은 analysis/grpc/grpc-server.md#L167 이다.
module: grpc-server
priority: P3
---
# 원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다
이것이 가정에 그치지 않는 이유는 이 저장소 자신의 문체다. 같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다.
## 문제
이것이 가정에 그치지 않는 이유는 이 저장소 자신의 문체다.
같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다.
## 결론
즉 이 코드베이스에서 완전 수식 사용은 예외가 아니라 흔한 형태다.
규칙 클래스의 자바독은 "there is nothing to reach for" 를 목표로 든다.
지금 형태는 손이 닿는 경로 하나만 본다.
수정은 정규식을 타입 이름의 등장 자체로 넓히거나(오탐이 생기므로 주석·문자열 제거가 필요), 바이트코드 기반 검사로 옮기는 것이다.
후자가 이 저장소의 다른 아키텍처 게이트와 형태가 같다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : 규칙이 스캔하는 구문 범위와 같은 가족 파일들의 완전 수식 참조 사용 대조
소스 수정 : x
## 재현 조건
원문은 analysis/grpc/grpc-server.md#L167 에 있다.
## 본문
<!-- body:start -->
원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다.
## 규칙이 보는 범위
:::evidence key="grpc-server-f02" alt="분석 문서 analysis/grpc/grpc-server.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-server.md 발췌 — 15줄" zoom="true"
:::
## 가정에 그치지 않는 이유는 이 저장소 자신의 문체다
같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다. 즉 이 코드베이스에서 완전 수식 사용은 예외가 아니라 흔한 형태다.
## 규칙의 목표와 어긋난다
규칙 클래스의 자바독은 "there is nothing to reach for" 를 목표로 든다. 지금 형태는 손이 닿는 경로 하나만 본다. 수정은 정규식을 타입 이름의 등장 자체로 넓히거나(오탐이 생기므로 주석·문자열 제거가 필요), 바이트코드 기반 검사로 옮기는 것이다 — 후자가 이 저장소의 다른 아키텍처 게이트와 형태가 같다.
## 확인하지 못한 것
완전 수식 사용을 담은 파일로 규칙을 실행해 통과를 관측하지 않았다. 규칙이 import 문만 본다는 코드 형태로 판정했다.
<!-- body:end -->
@@ -0,0 +1,87 @@
---
kind: CASE
slug: messaging-kafka-f01
title: deduplicatedPublish 를 문서는 지원으로 적고 코드는 거짓으로 둔다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-kafka-f01
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-kafka-f01
file: ../../../final/evidence/rendered/messaging-kafka-f01.svg
- key: messaging-kafka-f01-diagram
file: ../../../final/assets/diagrams/messaging-kafka-f01.svg
evidence:
- ../../../final/evidence/raw/messaging-kafka-f01.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-kafka.md#L208 이다.
module: messaging-kafka
priority: P1
---
# deduplicatedPublish 를 문서는 지원으로 적고 코드는 거짓으로 둔다
코드의 판정이 옳고 그 근거가 javadoc 에 있다. docs/messaging/support-matrix.md:55 의 능력 표는 이 칸을 O 로 적는다.
## 문제
코드의 판정이 옳고 그 근거가 javadoc 에 있다.
docs/messaging/support-matrix.md:55 의 능력 표는 이 칸을 O 로 적는다.
## 결론
그 차이가 무거운 이유는 이 플랫폼에서 이 플래그가 특별하기 때문이다.
능력 열둘 중 부재가 예외를 만드는 유일한 플래그다.
그래서 표를 읽고 중복 제거를 전제한 목적지를 설계한 팀은 실행 시점에 능력 예외를 만난다.
반대로 표를 읽고 "중복 제거가 있으니 모호를 그냥 재시도해도 된다" 고 결론지으면, 실제로는 중복이 저장된다.
MessagingCapabilities 의 클래스 javadoc 이 그 피해를 미리 적는다 — "a silently weakened guarantee is indistinguishable from a working one until the incident." 수정은 문서 쪽이다.
코드가 이미 옳다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : MessagingCapabilities 참조 39건 검색과 지원 문서 능력 표의 해당 칸 대조
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-kafka.md#L208 에 있다.
## 본문
<!-- body:start -->
코드의 판정이 옳고 그 근거가 javadoc 에 있다. `docs/messaging/support-matrix.md:55` 의 능력 표는 이 칸을 `O` 로 적는다.
## 문서와 상수가 갈리는 칸
:::evidence key="messaging-kafka-f01-diagram" alt="지원 문서 쪽에 deduplicatedPublish 지원과 표를 읽은 설계가 놓이고 코드 상수 쪽에 거짓과 실행 시점 능력 예외가 빗금으로 놓인다" caption="문서와 상수가 갈리는 칸" zoom="false"
:::
## MessagingCapabilities 참조 위치
:::evidence key="messaging-kafka-f01" alt="코드베이스에서 MessagingCapabilities 를 검색한 출력 39줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingCapabilities 코드베이스 검색 — 39줄 · exit 0" zoom="true"
:::
## 이 플래그가 특별해서 차이가 무겁다
능력 열둘 중 **부재가 예외를 만드는 유일한 플래그**다. 그래서 표를 읽고 중복 제거를 전제한 목적지를 설계한 팀은 실행 시점에 능력 예외를 만난다. 반대로 표를 읽고 "중복 제거가 있으니 모호를 그냥 재시도해도 된다" 고 결론지으면, 실제로는 중복이 저장된다.
## 수정은 문서 쪽이다
`MessagingCapabilities` 의 클래스 javadoc 이 그 피해를 미리 적는다 — "a silently weakened guarantee is indistinguishable from a working one until the incident." 코드가 이미 옳다.
## 확인하지 못한 것
실제 브로커로 이 능력을 켠 소비자를 만들어 중복 도착을 관측하지 않았다. 문서와 코드 상수의 대조로 판정했다.
<!-- body:end -->
@@ -0,0 +1,94 @@
---
kind: CASE
slug: messaging-observability-f01
title: 태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-observability-f01
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-observability-f01
file: ../../../final/evidence/rendered/messaging-observability-f01.svg
- key: messaging-observability-f01-diagram
file: ../../../final/assets/diagrams/messaging-observability-f01.svg
evidence:
- ../../../final/evidence/raw/messaging-observability-f01.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-observability.md#L673 이다.
module: messaging-observability
priority: P2
---
# 태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다
DefaultMessagingObservationConvention은 소비자가 0이다. 유일한 production 호출부(DefaultMessagePublisher.observe:260-269)가 "publish" 리터럴과 4인자 MessagingTags.of(...)를 쓴다.
## 관계
- **타입이 문서화한 불변식은 타입이 강제한다**
같은 분석 리프에서 끌어낸 규칙이다.
## 문제
DefaultMessagingObservationConvention은 소비자가 0이다.
유일한 production 호출부(DefaultMessagePublisher.observe:260-269)가 "publish" 리터럴과 4인자 MessagingTags.of(...)를 쓴다.
## 결론
그 factory는 failureCategory와 retryStage를 NONE으로 고정한다.
convention의 publish(broker, dest, completion, Optional<FailureCategory>)는 정확히 failureCategory를 채우려고 존재한다.
convention javadoc이 "an adapter inventing its own spelling of 'rejected' silently breaks every alert that was watching for it"를 이유로 중앙화를 선언했고, 첫 호출부가 그것을 지나쳤다.
그리고 결과가 철자 문제에 그치지 않는다 — MessagingTags가 선언한 6차원 중 4개만 채워진다.
메트릭이 배선되더라도(§다음 항목) 실패한 발행이 failureCategory=none으로 기록되어, "왜 실패했는가"를 메트릭에서 나눌 수 없다.
PublishResult.failure()에 FailureDescriptor가 이미 있으므로 값은 손에 있다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : DefaultMessagingObservationConvention 참조 1건 검색과 유일한 production 호출부가 넘기는 인자 확인
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-observability.md#L673 에 있다.
## 본문
<!-- body:start -->
`DefaultMessagingObservationConvention`은 소비자가 0이다. 유일한 production 호출부(`DefaultMessagePublisher.observe:260-269`)가 `"publish"` 리터럴과 **4인자** `MessagingTags.of(...)`를 쓴다.
## 기록이 끊기는 자리
:::evidence key="messaging-observability-f01-diagram" alt="convention 이 채우는 것 쪽에 여섯 차원과 failureCategory 가 놓이고 호출부가 채우는 것 쪽에 네 차원과 NONE 고정이 빗금으로 놓인다" caption="기록이 끊기는 자리" zoom="false"
:::
그 factory는 `failureCategory``retryStage``NONE`으로 고정한다.
## DefaultMessagingObservationConvention 참조 위치
:::evidence key="messaging-observability-f01" alt="코드베이스에서 DefaultMessagingObservationConvention 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DefaultMessagingObservationConvention 코드베이스 검색 — 1줄 · exit 0" zoom="true"
:::
## convention 이 존재하는 이유가 그 필드다
`publish(broker, dest, completion, Optional<FailureCategory>)`는 정확히 `failureCategory`를 채우려고 존재한다. convention javadoc이 "an adapter inventing its own spelling of 'rejected' silently breaks every alert that was watching for it"를 이유로 중앙화를 선언했고, 첫 호출부가 그것을 지나쳤다.
## 철자 문제에 그치지 않는다
`MessagingTags`가 선언한 6차원 중 4개만 채워진다. 메트릭이 배선되더라도 실패한 발행이 `failureCategory=none`으로 기록되어, "왜 실패했는가"를 메트릭에서 나눌 수 없다. `PublishResult.failure()``FailureDescriptor`가 이미 있으므로 값은 손에 있다.
## 확인하지 못한 것
실제 MeterRegistry 에 붙여 태그가 어떻게 기록되는지 관측하지 않았다. 호출부의 인자 수와 리터럴로 판정했다.
<!-- body:end -->
@@ -0,0 +1,76 @@
---
kind: CASE
slug: messaging-outbox-jdbc-postgresql-f06
title: 백오프 지터가 인스턴스를 분산시키지 못한다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-outbox-jdbc-postgresql-f06
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-outbox-jdbc-postgresql-f06
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f06.svg
evidence:
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f06.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L923 이다.
module: messaging-outbox-jdbc-postgresql
priority: P3
---
# 백오프 지터가 인스턴스를 분산시키지 못한다
jittered = capped - (capped/8) * (exponent % 3) 는 exponent 만의 함수다. 같은 상태의 복제본들은 같은 값을 계산한다.
## 문제
jittered = capped - (capped/8) * (exponent % 3) 는 exponent 만의 함수다.
같은 상태의 복제본들은 같은 값을 계산한다.
## 결론
javadoc 이 약속하는 "thundering herd 방지" 가 성립하지 않는다.
OutboxRelay 가 이미 defaultOwner() 로 프로세스별 안정 식별자를 만든다(pid@uuid8).
그것의 해시를 지터에 섞으면 결정성(같은 프로세스에서 재현 가능)을 유지하면서 인스턴스 간 위상차가 생긴다.
javadoc 이 난수를 거부한 이유("a random source would make the schedule impossible to test")도 그대로 지켜진다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : 지터 계산식의 입력 변수 확인 — exponent 만의 함수임을 코드로 대조
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L923 에 있다.
## 본문
<!-- body:start -->
`jittered = capped - (capped/8) * (exponent % 3)``exponent` 만의 함수다. 같은 상태의 복제본들은 같은 값을 계산한다.
## OutboxRelay 참조 위치
:::evidence key="messaging-outbox-jdbc-postgresql-f06" alt="코드베이스에서 OutboxRelay 를 검색한 출력 27줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxRelay 코드베이스 검색 — 27줄 · exit 0" zoom="true"
:::
## javadoc 이 약속한 성질이 성립하지 않는다
"thundering herd 방지" 다.
## 재료가 이미 있다
`OutboxRelay``defaultOwner()` 로 프로세스별 안정 식별자를 만든다(`pid@uuid8`). 그것의 해시를 지터에 섞으면 결정성(같은 프로세스에서 재현 가능)을 유지하면서 인스턴스 간 위상차가 생긴다. javadoc 이 난수를 거부한 이유("a random source would make the schedule impossible to test")도 그대로 지켜진다.
## 확인하지 못한 것
지터 동기화를 다중 인스턴스로 재현하지 않았다. 함수가 exponent 만의 함수라는 것은 코드로 확인했다.
<!-- body:end -->
@@ -0,0 +1,72 @@
---
kind: CASE
slug: messaging-outbox-jdbc-postgresql-f07
title: 커넥션 획득 방식이 리프 안에서 갈린다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-outbox-jdbc-postgresql-f07
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-outbox-jdbc-postgresql-f07
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-f07.svg
evidence:
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-f07.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L929 이다.
module: messaging-outbox-jdbc-postgresql
priority: P3
---
# 커넥션 획득 방식이 리프 안에서 갈린다
JdbcOutboxRepository.append 는 DataSourceUtils, 나머지는 raw dataSource.getConnection(), JdbcAdminOperationJournal 은 전부 DataSourceUtils. 릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단은 타당하지만 어디에도 적혀 있지 않고, 같은 리프의 저널이 반대로 한다.
## 문제
JdbcOutboxRepository.append 는 DataSourceUtils, 나머지는 raw dataSource.getConnection(), JdbcAdminOperationJournal 은 전부 DataSourceUtils.
릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단은 타당하지만 어디에도 적혀 있지 않고, 같은 리프의 저널이 반대로 한다.
## 결론
withConnection 에 한 문장 — "릴레이 연산은 호출자 트랜잭션에 합류하지 않는다" — 을 붙이면 append 의 상세한 주석과 짝이 맞는다.
저널이 DataSourceUtils 를 쓰는 것이 의도인지도 확인이 필요하다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : JdbcOutboxRepository 참조 5건 검색과 리프 안 세 지점의 커넥션 획득 방식 대조
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L929 에 있다.
## 본문
<!-- body:start -->
`JdbcOutboxRepository.append``DataSourceUtils`, 나머지는 raw `dataSource.getConnection()`, `JdbcAdminOperationJournal` 은 전부 `DataSourceUtils` 를 쓴다.
## JdbcOutboxRepository 참조 위치
:::evidence key="messaging-outbox-jdbc-postgresql-f07" alt="코드베이스에서 JdbcOutboxRepository 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JdbcOutboxRepository 코드베이스 검색 — 5줄 · exit 0" zoom="true"
:::
## 판단은 타당하고 어디에도 적혀 있지 않다
릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단이다. 그리고 같은 리프의 저널이 반대로 한다.
## 수정
`withConnection` 에 한 문장 — "릴레이 연산은 호출자 트랜잭션에 합류하지 않는다" — 을 붙이면 `append` 의 상세한 주석과 짝이 맞는다. 저널이 `DataSourceUtils` 를 쓰는 것이 의도인지도 확인이 필요하다.
## 확인하지 못한 것
비즈니스 트랜잭션 합류 여부가 실제 동작에서 갈리는 것을 관측하지 않았다. 획득 방식의 코드 대조로 판정했다.
<!-- body:end -->
@@ -0,0 +1,86 @@
---
kind: CASE
slug: messaging-schema-avro-f01
title: CI에서 돈다고 선언한 게이트를 부르는 CI가 없다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-schema-avro-f01
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-schema-avro-f01
file: ../../../final/evidence/rendered/messaging-schema-avro-f01.svg
- key: messaging-schema-avro-f01-diagram
file: ../../../final/assets/diagrams/messaging-schema-avro-f01.svg
evidence:
- ../../../final/evidence/raw/messaging-schema-avro-f01.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L524 이다.
module: messaging-schema-avro
priority: P2
---
# CI에서 돈다고 선언한 게이트를 부르는 CI가 없다
AvroCompatibilityGate javadoc이 "Run in CI rather than at runtime"이라고 선언한다. 저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, src/build.gradle의 9개 verifyMessaging* task 중 스키마 진화를 검사하는 것이 없다.
## 관계
- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다**
같은 분석 리프에서 끌어낸 규칙이다.
- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다**
같은 분석 리프에서 끌어낸 규칙이다.
- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다**
같은 분석 리프에서 끌어낸 규칙이다.
## 문제
AvroCompatibilityGate javadoc이 "Run in CI rather than at runtime"이라고 선언한다.
저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, src/build.gradle의 9개 verifyMessaging* task 중 스키마 진화를 검사하는 것이 없다.
## 결론
게이트의 존재 이유가 "한 번 발행되면 보존 로그에 영구히 남는다"인데, 그 보호가 어느 파이프라인에도 붙어 있지 않다.
AvroMessageCodec의 미사용과 달리 이것은 membership으로 설명되지 않는다 — 런타임 편입 여부와 무관하게 CI 게이트는 붙었어야 한다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : AvroCompatibilityGate 참조 3건 검색과 이 게이트를 부르는 Gradle 태스크·CI 워크플로 검색
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-schema-avro.md#L524 에 있다.
## 본문
<!-- body:start -->
`AvroCompatibilityGate` javadoc이 "Run in CI rather than at runtime"이라고 선언한다.
## 선언과 호출의 거리
:::evidence key="messaging-schema-avro-f01-diagram" alt="자기 테스트만 게이트를 부르는 것 안에 놓이고 verifyMessaging 태스크와 CI 워크플로가 바깥에 빗금으로 놓인다" caption="선언과 호출의 거리" zoom="false"
:::
저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, `src/build.gradle`의 9개 `verifyMessaging*` task 중 스키마 진화를 검사하는 것이 없다.
## AvroCompatibilityGate 참조 위치
:::evidence key="messaging-schema-avro-f01" alt="코드베이스에서 AvroCompatibilityGate 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AvroCompatibilityGate 코드베이스 검색 — 3줄 · exit 0" zoom="true"
:::
## membership 으로 설명되지 않는다
게이트의 존재 이유가 "한 번 발행되면 보존 로그에 영구히 남는다"인데, 그 보호가 어느 파이프라인에도 붙어 있지 않다. `AvroMessageCodec`의 미사용과 달리 런타임 편입 여부와 무관하게 CI 게이트는 붙었어야 한다.
## 확인하지 못한 것
파생 프로젝트가 이 게이트를 자기 CI 에서 부르는지 확인할 수 없었다. 저장소 안에 확인 수단이 없다.
<!-- body:end -->
@@ -0,0 +1,102 @@
---
kind: CASE
slug: messaging-spring-boot-starter-f02
title: 출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-spring-boot-starter-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-spring-boot-starter-f02
file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f02.svg
- key: messaging-spring-boot-starter-f02-diagram
file: ../../../final/assets/diagrams/messaging-spring-boot-starter-f02.svg
evidence:
- ../../../final/evidence/raw/messaging-spring-boot-starter-f02.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-spring-boot-starter.md#L300 이다.
module: messaging-spring-boot-starter
priority: P2
---
# 출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다
여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다. 조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다.
## 관계
- **검증기는 발행이 아니라 주입이 강제다**
같은 분석 리프에서 끌어낸 규칙이다.
## 문제
여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다.
조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다.
## 결론
문제는 저장소 안에 그 타입을 공급하는 코드가 없다는 것이다.
messaging-outbox-jdbc-postgresql 의 JdbcOutboxRepository 는 스프링 스테레오타입도 @Bean 선언도 없고, new JdbcOutboxRepository 가 main 에 0 건이다.
그래서 이 스타터를 켠 배포는 발행 경로는 얻고 발신함 경로는 얻지 못하며, 그 사실이 시작 시점에 어떤 신호도 내지 않는다.
둘째와 셋째가 같은 설정 클래스 안에서 방금 선언된 빈의 존재를 조건으로 삼는다.
스프링은 @ConditionalOnBean 을 자동 설정 클래스에서만, 그리고 등록 순서에 의존하는 방식으로만 신뢰할 수 있다고 문서화한다.
지금은 첫 조건이 이미 거짓이라 결과가 드러나지 않는다.
발신함을 배선하는 순간 이 사슬이 실제로 평가된다.
같은 가족의 다른 결정과 대비된다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : JdbcOutboxRepository 참조 5건 검색과 여섯 빈의 @ConditionalOnBean 대상 타입 확인
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-spring-boot-starter.md#L300 에 있다.
## 본문
<!-- body:start -->
여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다. 조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다.
## 조건이 가리키는 대상
:::evidence key="messaging-spring-boot-starter-f02-diagram" alt="애플리케이션 공급 타입만 조건이 보는 것 안에 놓이고 같은 클래스에서 방금 선언된 빈이 바깥에 빗금으로 놓인다" caption="조건이 가리키는 대상" zoom="false"
:::
문제는 저장소 안에 그 타입을 공급하는 코드가 없다는 것이다. `messaging-outbox-jdbc-postgresql``JdbcOutboxRepository` 는 스프링 스테레오타입도 `@Bean` 선언도 없고, `new JdbcOutboxRepository` 가 main 에 0 건이다.
## JdbcOutboxRepository 참조 위치
:::evidence key="messaging-spring-boot-starter-f02" alt="코드베이스에서 JdbcOutboxRepository 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JdbcOutboxRepository 코드베이스 검색 — 5줄 · exit 0" zoom="true"
:::
## 그래서 발신함 경로만 조용히 빠진다
이 스타터를 켠 배포는 발행 경로는 얻고 발신함 경로는 얻지 못하며, 그 사실이 시작 시점에 어떤 신호도 내지 않는다.
## 사슬이 자기 클래스 안을 가리킨다
둘째와 셋째가 같은 설정 클래스 안에서 방금 선언된 빈의 존재를 조건으로 삼는다. 스프링은 `@ConditionalOnBean` 을 자동 설정 클래스에서만, 그리고 등록 순서에 의존하는 방식으로만 신뢰할 수 있다고 문서화한다. 지금은 첫 조건이 이미 거짓이라 결과가 드러나지 않고, 발신함을 배선하는 순간 이 사슬이 실제로 평가된다.
## 같은 가족의 다른 결정과 대비된다
관리 평면은 스위치가 켜졌을 때 만들어지지 **않는** 타입의 부재를 javadoc 에 명시한다(`DestructiveMessagingAdmin` 하나). 이쪽은 여섯이 조용히 빠진다. 수정은 둘이다 — 발신함을 요구하는 설정에서 저장소 빈이 없으면 시작을 거부하는 검증(`StartupProfileValidation` 형태), 그리고 중계·작업자·수명을 하나의 `@Bean` 으로 합치거나 조건을 전부 최초 두 타입으로 표현하는 것.
## 확인하지 못한 것
애플리케이션이 저장소 빈을 공급한 상태로 컨텍스트를 세우지 않았다. 저장소에 그런 애플리케이션이 없다.
<!-- body:end -->
@@ -0,0 +1,85 @@
---
kind: CASE
slug: messaging-spring-boot-starter-f04
title: 설정 경로의 재시도가 예외 분류를 표현할 수 없다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-spring-boot-starter-f04
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-spring-boot-starter-f04
file: ../../../final/evidence/rendered/messaging-spring-boot-starter-f04.svg
evidence:
- ../../../final/evidence/raw/messaging-spring-boot-starter-f04.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-spring-boot-starter.md#L342 이다.
module: messaging-spring-boot-starter
priority: P3
---
# 설정 경로의 재시도가 예외 분류를 표현할 수 없다
RetryPolicy 는 성분 열이고 그중 둘이 분류 집합이다. DestinationSettings.Retry 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다.
## 관계
- **검증기는 발행이 아니라 주입이 강제다**
같은 분석 리프에서 끌어낸 규칙이다.
## 문제
RetryPolicy 는 성분 열이고 그중 둘이 분류 집합이다.
DestinationSettings.Retry 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다.
## 결론
빈 집합은 "기본 분류 그대로" 라는 중립값이므로 오동작은 아니다.
문제는 비대칭이다.
DestinationProfile 을 자바로 선언한 배포는 두 집합을 조정할 수 있고, 문서대로 YAML 로 설정한 배포는 할 수 없다.
이 리프가 반복해서 근거로 든 규칙이 정확히 그 비대칭을 금지한다.
수정은 Retry 에 두 키를 더하는 것이다.
FailureCategory 는 열거이므로 relaxed binding 이 그대로 처리한다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : RetryPolicy 참조 36건 검색과 설정 record 의 키 목록 대조
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-spring-boot-starter.md#L342 에 있다.
## 본문
<!-- body:start -->
`RetryPolicy` 는 성분 열이고 그중 둘이 분류 집합이다. `DestinationSettings.Retry` 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다.
## RetryPolicy 참조 위치
:::evidence key="messaging-spring-boot-starter-f04" alt="코드베이스에서 RetryPolicy 를 검색한 출력 36줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RetryPolicy 코드베이스 검색 — 36줄 · exit 0" zoom="true"
:::
## 오동작은 아니다
빈 집합은 "기본 분류 그대로" 라는 중립값이다.
## 문제는 비대칭이다
`DestinationProfile` 을 자바로 선언한 배포는 두 집합을 조정할 수 있고, 문서대로 YAML 로 설정한 배포는 할 수 없다. 이 리프가 반복해서 근거로 든 규칙이 정확히 그 비대칭을 금지한다. 수정은 `Retry` 에 두 키를 더하는 것이다 — `FailureCategory` 는 열거이므로 relaxed binding 이 그대로 처리한다.
## 확인하지 못한 것
설정으로 분류를 지정해 무시되는 것을 실행으로 확인하지 않았다. 대응 키가 없어 컴파일러가 상수로 채운다는 것으로 판정했다.
<!-- body:end -->
@@ -0,0 +1,93 @@
---
kind: CASE
slug: messaging-testkit-f02
title: 클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-testkit-f02
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-testkit-f02
file: ../../../final/evidence/rendered/messaging-testkit-f02.svg
- key: messaging-testkit-f02-diagram
file: ../../../final/assets/diagrams/messaging-testkit-f02.svg
evidence:
- ../../../final/evidence/raw/messaging-testkit-f02.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L950 이다.
module: messaging-testkit
priority: P2
---
# 클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다
BrokerFailureMatrix.java:18-20 이 "A Stable adapter must cover every scenario. That rule is enforced by a test rather than documented" 라고 쓰고 있으나, isComplete 를 Stable 어댑터에 거는 테스트는 없다(EVD-300).
## 문제
BrokerFailureMatrix.java:18-20 이 "A Stable adapter must cover every scenario.
That rule is enforced by a test rather than documented" 라고 쓰고 있으나, isComplete 를 Stable 어댑터에 거는 테스트는 없다(EVD-300).
## 결론
유일한 호출부는 Experimental 어댑터가 불완전함을 단언한다.
실제 Stable 인 messaging-kafka 는 connection-refused gap 을 가진 채 통과하며, 그 gap 은 같은 모듈이 명시적으로 단언한다.
코드 쪽 결정("gap 을 열거하되 비어 있음을 단언하지 않는다")은 옳고, 그 이유도 CrossBrokerContractSuite.java:45-47 에 적혀 있다.
문제는 javadoc 이 갱신되지 않은 것이다.
이 리프의 다른 javadoc 여섯 곳이 자기 이력을 정확히 남긴 것과 대비되어 더 눈에 띈다.
같은 이유로 테스트 메서드 이름 everyStableAdapterCoversEveryFaultScenario 도 본문과 맞지 않는다.
everyStableAdaptersGapsAreExactlyWhatTheEvidenceShows 같은 이름이 본문을 정확히 기술한다.
수정 방향: javadoc 을 현재 규칙("Stable 은 live-broker 증거를 하나 이상 요구한다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : isComplete 호출부 1건 확인과 그 호출이 Stable 어댑터를 대상으로 하는지 대조
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-testkit.md#L950 에 있다.
## 본문
<!-- body:start -->
`BrokerFailureMatrix.java:18-20` 이 "A Stable adapter must cover every scenario. That rule is enforced by a test rather than documented" 라고 쓰고 있으나, `isComplete` 를 Stable 어댑터에 거는 테스트는 없다(`EVD-300`).
## 강제가 없는 규칙
:::evidence key="messaging-testkit-f02-diagram" alt="클래스 javadoc 만 규칙을 표현하는 것 안에 놓이고 Stable 에 거는 테스트가 바깥에 빗금으로 놓인다" caption="강제가 없는 규칙" zoom="false"
:::
유일한 호출부는 Experimental 어댑터가 불완전함을 단언한다. 실제 Stable 인 `messaging-kafka``connection-refused` gap 을 가진 채 통과하며, 그 gap 은 같은 모듈이 명시적으로 단언한다.
## javadoc 이 강제된다고 말한 규칙
:::evidence key="messaging-testkit-f02" alt="분석 문서 analysis/messaging/messaging-testkit.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-testkit.md 발췌 — 15줄" zoom="true"
:::
## 코드 쪽 결정은 옳다
"gap 을 열거하되 비어 있음을 단언하지 않는다" 이고 그 이유도 `CrossBrokerContractSuite.java:45-47` 에 적혀 있다. 문제는 **javadoc 이 갱신되지 않은 것**이다. 이 리프의 다른 javadoc 여섯 곳이 자기 이력을 정확히 남긴 것과 대비되어 더 눈에 띈다.
## 테스트 이름도 본문과 맞지 않는다
`everyStableAdapterCoversEveryFaultScenario` 대신 `everyStableAdaptersGapsAreExactlyWhatTheEvidenceShows` 같은 이름이 본문을 정확히 기술한다. 수정 방향은 javadoc 을 현재 규칙("Stable 은 live-broker 증거를 하나 이상 요구한다. 전 시나리오 커버리지는 목표이지 게이트가 아니며, gap 은 `knownGaps` 로 명명된다")으로 바꾸는 것이다.
## 확인하지 못한 것
매니페스트를 손으로 고쳐 게이트가 실패하는 것은 확인하지 않았다. 그것은 애플리케이션 소스 수정에 해당해 하지 않았다.
<!-- body:end -->
@@ -0,0 +1,70 @@
---
kind: CASE
slug: messaging-testkit-f08
title: gitCommit 은 기록되지만 읽혀 판정되지 않는다
topic: declaration-and-document-drift
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:messaging-testkit-f08
evidenceCapturedOn: 2026-09-01
assets:
- key: messaging-testkit-f08
file: ../../../final/evidence/rendered/messaging-testkit-f08.svg
evidence:
- ../../../final/evidence/raw/messaging-testkit-f08.txt
source:
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L984 이다.
module: messaging-testkit
priority: P3
---
# gitCommit 은 기록되지만 읽혀 판정되지 않는다
BrokerCertificationEvidence javadoc 이 "without them the evidence cannot be checked against anything later" 라고 쓰지만, 실제로 gitCommit 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)). 현재 매니페스트의 커밋은 HEAD 가 아니다(e98b56eb vs 21234e38).
## 문제
BrokerCertificationEvidence javadoc 이 "without them the evidence cannot be checked against anything later" 라고 쓰지만, 실제로 gitCommit 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)).
현재 매니페스트의 커밋은 HEAD 가 아니다(e98b56eb vs 21234e38).
## 결론
"증거가 얼마나 오래된 트리에서 나왔는가" 를 보고하는 것은 유용한 진단이 될 수 있다 — 게이트로 만들 필요는 없고, knownGaps 처럼 사실로 노출하면 이 리프의 나머지 설계와 결이 맞는다.
## 검증 환경
OpenJDK : 21.0.12 java -version 으로 확인
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
확인 방식 : BrokerCertificationEvidence 참조 26건 검색과 게이트가 비교에서 제외하는 필드 확인
소스 수정 : x
## 재현 조건
원문은 analysis/messaging/messaging-testkit.md#L984 에 있다.
## 본문
<!-- body:start -->
`BrokerCertificationEvidence` javadoc 이 "without them the evidence cannot be checked against anything later" 라고 쓰지만, 실제로 `gitCommit` 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)).
## BrokerCertificationEvidence 참조 위치
:::evidence key="messaging-testkit-f08" alt="코드베이스에서 BrokerCertificationEvidence 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BrokerCertificationEvidence 코드베이스 검색 — 26줄 · exit 0" zoom="true"
:::
## 현재 매니페스트의 커밋은 HEAD 가 아니다
`e98b56eb` vs `21234e38`.
## 게이트로 만들 필요는 없다
"증거가 얼마나 오래된 트리에서 나왔는가" 를 보고하는 것은 유용한 진단이 될 수 있다. `knownGaps` 처럼 사실로 노출하면 이 리프의 나머지 설계와 결이 맞는다.
## 확인하지 못한 것
게이트를 다른 커밋의 매니페스트로 돌려 보지 않았다. 비교 대상 필드 목록으로 판정했다.
<!-- body:end -->