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>
113 lines
6.0 KiB
Markdown
113 lines
6.0 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: analysis-finding-a06-f026
|
|
title: 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다
|
|
topic: multitenancy-isolation
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:analysis-finding-a06-f026
|
|
evidenceCapturedOn: 2026-09-01
|
|
body: case-analysis-finding-a06-f026.body.md
|
|
assets:
|
|
- key: analysis-finding-a06-f026
|
|
file: ../../../final/evidence/rendered/analysis-finding-a06-f026.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/analysis-finding-a06-f026.txt
|
|
source:
|
|
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L1383 이다.
|
|
---
|
|
|
|
# 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다
|
|
|
|
계약 묶음의 javadoc 은 실패한 계약과 한 번도 돌지 않은 계약을 구분한다고 적는다. 구현에서 실행 집합이 전체와 항상 같으므로 누락 항목이 어떤 입력으로도 생성되지 않는다. 같은 테스트킷에 옳게 구현된 형제가 있다.
|
|
|
|
## 관계
|
|
|
|
- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다**
|
|
이 사례가 그 규칙의 형태다.
|
|
- **아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다**
|
|
같은 원칙의 레인 판이다.
|
|
- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다**
|
|
형제 구현과 비교하는 방법이다.
|
|
|
|
## 문제
|
|
|
|
계약 묶음의 javadoc 이 존재 이유를 적는다.
|
|
|
|
보고서는 실패한 계약과 한 번도 돌지 않은 계약을 구분한다. 절반이 건너뛰어졌기 때문에 실패 없음을 보고하는 묶음은 아무것도 인증하지 않는 초록 빌드의 전형이므로, 누락된 계약은 여기서 실패다.
|
|
|
|
## 결론
|
|
|
|
구현이 그 구분을 만들 수 없다.
|
|
|
|
루프가 실행 집합을 무조건 채운다. 각 계약을 실행 집합에 넣고 그다음 검사를 수행한다.
|
|
|
|
그러므로 실행 집합은 전체 집합과 언제나 같고, 누락 집합은 언제나 비며, 실행되지 않음 항목은 어떤 입력으로도 생성되지 않는다.
|
|
|
|
인증 여부를 판정하는 조건도 마찬가지로 항상 참이다.
|
|
|
|
조건부 형제가 같은 테스트킷 안에 있다. 카오스 게이트의 보고 메서드는 같은 일을 옳게 한다. 실행 집합이 명시적 기록 호출로만 채워지는 맵이고, 누락은 전체에서 기록되지 않은 것을 뺀 것이다.
|
|
|
|
그리고 그 형제의 테스트가 그것을 증명한다. 열세 시나리오 중 하나만 기록하고 나머지가 실행되지 않음으로 나타나는지 단언한다.
|
|
|
|
계약 묶음의 대응 테스트는 그렇게 하지 않는다. 모든 계약에 통과를 주고 나서 실행 집합이 전부를 담는지 단언한다. 구조상 항상 참인 것을 단언하는 것이다.
|
|
|
|
판정은 두 인증 레인의 커버리지 주장이 무효라는 것이다.
|
|
|
|
수정은 형제를 따르면 된다. 실행 메서드가 실행할 계약 집합을 인자로 받거나, 실제로 호출된 것만 실행 집합에 넣는 것이다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
확인 방식 : 구현 코드 확인과 형제 구현 비교
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
원문은 final/evidence/raw/138 계열에 있다.
|
|
|
|
1. 계약 묶음의 javadoc 을 읽는다.
|
|
2. 보고 메서드의 루프에서 실행 집합이 어떻게 채워지는지 확인한다.
|
|
3. 누락 집합이 어떻게 계산되는지 확인한다.
|
|
4. 카오스 게이트의 같은 메서드와 비교한다.
|
|
5. 두 테스트가 각각 무엇을 단언하는지 비교한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
`MongoStableContractSuite`의 javadoc이 존재 이유를 적는다.
|
|
|
|
> The report distinguishes a failed contract from a contract that never ran. A suite that reports "no failures" because half of it was skipped is exactly the shape of green build that certifies nothing, **so a missing contract is a failure here.**
|
|
|
|
## suite javadoc 이 적은 존재 이유
|
|
|
|
:::evidence key="analysis-finding-a06-f026" alt="분석 문서 analysis/06-adapter-outbound-persistence-mongo.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/06-adapter-outbound-persistence-mongo.md 발췌 — 15줄" zoom="true"
|
|
:::
|
|
|
|
## 구현은 그 구분을 만들 수 없다
|
|
|
|
```java
|
|
Set<MongoReplicaSetContract> executed = new LinkedHashSet<>();
|
|
for (MongoReplicaSetContract contract : MongoReplicaSetContract.all()) {
|
|
executed.add(contract); // ← 루프가 무조건 채운다
|
|
if (!contractRunner.test(contract)) { failures.add(...); }
|
|
}
|
|
Set<MongoReplicaSetContract> missing = new LinkedHashSet<>(MongoReplicaSetContract.all());
|
|
missing.removeAll(executed); // ← 항상 비어 있다
|
|
missing.forEach(contract -> failures.add(... + " (not executed)"));
|
|
```
|
|
|
|
`executed`는 `all()`과 언제나 같으므로 `missing`은 언제나 비고, `(not executed)` 항목은 **어떤 입력으로도 생성되지 않는다**. `certified()`의 `executed.containsAll(all())`(78행)도 항상 참이다. 두 인증 lane(7.0/8.0)의 커버리지 주장이 무효다. P2.
|
|
|
|
## 조건부 형제가 같은 testkit 안에 있다
|
|
|
|
`MongoChaosGate.report()`는 같은 일을 옳게 한다 — `executed`는 명시적 `record(scenario, passed)` 호출로만 채워지는 map이고, `missing`은 `all()`에서 기록되지 않은 것을 뺀 것이다. 그 test가 그것을 증명한다: `aScenarioThatNeverRanIsAFailureRatherThanASilence`는 13개 시나리오 중 **하나만** 기록하고 나머지가 `(not executed)`로 나타나는지 단언한다. 수정은 형제를 따르면 된다 — `run(...)`이 실행할 contract 집합을 인자로 받거나, runner가 실제로 호출된 것만 `executed`에 넣는 것.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
계약 하나를 실제로 건너뛰게 만들어 보고서가 여전히 깨끗한지 재현하지 않았다. 코드 구조상 그 결과가 나온다.
|
|
|
|
<!-- body:end -->
|