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>
135 lines
8.3 KiB
Markdown
135 lines
8.3 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a-test-that-passed-on-the-wrong-guard
|
|
title: 다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다
|
|
topic: what-a-gate-does-not-prove
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a-test-that-passed-on-the-wrong-guard
|
|
evidenceCapturedOn: 2026-09-02
|
|
assets:
|
|
- key: a-test-that-passed-on-the-wrong-guard
|
|
file: ../../../final/evidence/rendered/a-test-that-passed-on-the-wrong-guard.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a-test-that-passed-on-the-wrong-guard.txt
|
|
source:
|
|
- 분석 문서는 notification 어댑터 편 §9 다. 그 절이 같은 메서드가 전략을 두 번 본다는 것을 코드로 보이고, SINGLE 을 뺀 세 갈래를 도달 불가로 표시하며, 이름이 약속한 두 성질 중 하나만 그것도 두 번 확인된다고 적는다. 권고는 단언을 좁히는 것이다.
|
|
---
|
|
|
|
# 다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다
|
|
|
|
경로 형태 검증기의 첫 가드가 SINGLE 이 아닌 전략을 전부 거부한다. 그 뒤 switch 의 나머지 세 갈래와 폴백 그래프 검사는 도달할 수 없고, 그것을 확인한다는 테스트는 앞 가드의 예외로 통과한다.
|
|
|
|
## 관계
|
|
|
|
- **빠뜨림이 통과가 되는 게이트는 게이트가 아니다**
|
|
통과의 이유가 의도한 것과 다른 형태다.
|
|
- **계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다**
|
|
계약 테스트가 어댑터의 실제 statement 를 돌려야 한다는 규칙이다.
|
|
|
|
## 문제
|
|
|
|
경로 형태 검증기 안에서 전략을 보는 자리가 한 메서드에 둘이다.
|
|
|
|
첫 번째는 가드다. 전략이 SINGLE 이 아니면 초기 카탈로그는 SINGLE 만 지원한다는 예외를 던진다.
|
|
|
|
두 번째는 switch 다. 네 갈래를 갖고, 그 뒤에 폴백 그래프 검사가 온다.
|
|
|
|
## 결론
|
|
|
|
첫 가드를 통과한 경로는 전략이 SINGLE 뿐이다. switch 의 나머지 세 갈래 — 팬아웃, 순서 폴백, 기본 — 에 들어갈 값이 없다. SINGLE 갈래가 폴백을 금지하므로 그 뒤 폴백 그래프 검사의 두 문구도 도달할 수 없다.
|
|
|
|
그것을 확인한다는 테스트가 있다. 이름은 폴백 범위와 순환이 거부된다고 말하고, 순환 경로와 팬아웃 경로를 각각 만든다. 단언 둘 다 타입을 확인한 뒤 메시지에서 전략이라는 단어를 찾는다.
|
|
|
|
두 경로 다 첫 가드에 걸린다. 그 문구에 그 단어가 있으므로 단언이 통과한다. 테스트가 실제로 확인하는 것은 SINGLE 이 아닌 전략이 거부된다는 성질 하나이고, 그것을 두 번 확인한다.
|
|
|
|
이름이 약속한 두 성질 중 하나는 확인되지 않는다.
|
|
|
|
첫 가드와 switch 사이에는 전략을 보지 않는 검사가 둘 있다. 타깃 수 일치와 증폭 한계다. 도달 불가를 만든 것은 위치가 아니라 switch 갈래 안에 들어갔다는 사실이다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
Gradle : 9.0.0
|
|
확인 방식 : 코드 경로와 테스트 단언 비교, 해당 테스트 실행
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 경로 형태 검증기의 첫 가드와 그 뒤 전략을 보지 않는 검사 둘을 읽는다.
|
|
2. switch 네 갈래와 그 뒤 폴백 그래프 검사가 던지는 두 문구를 확인한다.
|
|
3. 테스트가 만든 두 라우트의 전략과 범위 값을 읽는다.
|
|
4. 두 단언이 기대하는 문자열과 각 예외 문구를 대조한다.
|
|
5. 이 두 갈래를 태울 라우트를 만드는 곳을 저장소 전체에서 센다.
|
|
6. 그 테스트를 실행한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
경로 형태 검증기가 같은 메서드 안에서 전략을 두 번 본다. 앞은 가드이고 뒤는 switch 다.
|
|
|
|
## 앞의 가드가 뒤의 갈래들을 비운다
|
|
|
|
:::evidence key="a-test-that-passed-on-the-wrong-guard" alt="코드베이스에서 경로 형태 검증기의 첫 가드와 그 뒤 전략을 보지 않는 검사 둘, switch 네 갈래 전체와 기본 갈래의 문구, 폴백 그래프 검사가 던질 수 있는 두 문구의 위치, 순환을 거부한다는 테스트의 이름과 그 테스트가 만든 팬아웃 라우트와 순환 라우트의 전략과 범위 값과 두 단언, 이 두 분기를 태울 라우트를 만드는 곳의 저장소 전수 목록과 설정 파일의 비-SINGLE 전략 수, 그리고 그 테스트를 돌린 결과를 뽑은 출력 106줄. 순환 라우트의 범위가 그 갈래의 검사를 통과하는 값이라는 것이 그 출력에 보인다." caption="첫 가드와 전략을 보지 않는 검사 둘 · switch 네 갈래 · 폴백 그래프의 두 문구 · 두 라우트의 전략과 범위 · 전수 목록 · 5건 통과 — 106줄" zoom="true"
|
|
:::
|
|
|
|
첫 가드는 전략이 SINGLE 이 아니면 예외를 던진다. 문구는 초기 카탈로그가 SINGLE 만 지원한다는 것이다.
|
|
|
|
그 뒤 switch 에 네 갈래가 있다. SINGLE 은 타깃 하나와 폴백 없음을 요구하고, 팬아웃은 폴백 정의를 금지하고, 순서 폴백은 타깃 둘 이상과 활성화 범위를 요구하고, 기본 갈래는 알 수 없는 전략을 거부한다.
|
|
|
|
가드를 통과한 값은 SINGLE 뿐이다. 나머지 세 갈래에 들어갈 값이 없다.
|
|
|
|
## 폴백 그래프 검사도 그 뒤에 있다
|
|
|
|
switch 가 끝나면 폴백 그래프 검사를 부른다. 던질 수 있는 문구가 둘이다. 폴백이 없는 타깃을 가리킬 때와, 순환일 때다.
|
|
|
|
SINGLE 갈래는 폴백이 하나라도 있으면 그 앞에서 거부한다. 그래서 이 검사에 도달하는 경로에는 폴백 간선이 없고, 두 문구 다 나올 수 없다.
|
|
|
|
## 테스트는 두 라우트를 만들고 같은 것을 묻는다
|
|
|
|
테스트 이름이 폴백 범위와 순환이 거부된다고 말한다.
|
|
|
|
하나는 팬아웃 라우트다. 다른 하나는 순서 폴백 라우트이고, 두 타깃이 서로를 폴백으로 가리키는 순환이다.
|
|
|
|
두 단언 모두 예외 타입과 메시지에 전략이라는 단어가 들어 있는지를 본다. 두 라우트 다 첫 가드에 걸리고, 그 문구에 그 단어가 있다.
|
|
|
|
그래서 이 테스트가 실제로 확인하는 것은 하나다. SINGLE 이 아닌 전략이 거부된다는 것. 그것을 두 번 확인한다.
|
|
|
|
## 가드가 사라지는 날 두 단언이 갈린다
|
|
|
|
순환 라우트는 타깃 둘과 폴백 활성화 하나를 갖는다. 순서 폴백 갈래의 범위 검사는 타깃 둘 미만이거나, 활성화가 하나 미만이거나, 활성화가 타깃 수보다 하나 이상 적지 않으면 거부한다. 이 값들은 셋 다 통과한다.
|
|
|
|
그러면 폴백 그래프 검사에 닿고 순환 문구를 던진다. 그 문구에는 전략이라는 단어가 없다. 이 단언은 그날 실패한다.
|
|
|
|
팬아웃 라우트는 다르다. 공급자 능력 카드의 전략이 라우트와 어긋나면 그것을 지목하는 예외가 나오는데, 그 문구에도 전략이라는 단어가 있다. 이 단언은 그날도 통과한다.
|
|
|
|
한쪽은 실패로 존재를 알리고, 다른 한쪽은 계속 조용하다.
|
|
|
|
## 이 갈래들을 태우는 라우트는 이 테스트뿐이다
|
|
|
|
저장소 전체에서 비 SINGLE 전략으로 라우트를 만드는 자바 코드를 찾으면 두 곳이다. 이 테스트의 두 라우트다.
|
|
|
|
같은 열거형을 쓰는 다른 파일들은 종류 정책 클래스와 그 테스트인데, 그 두 파일은 자기 클래스의 검증을 다룬다.
|
|
|
|
설정 파일에서 비 SINGLE 전략을 찾으면 0 이다.
|
|
|
|
## 도달 불가를 만든 것은 위치가 아니다
|
|
|
|
첫 가드와 switch 사이에도 검사가 둘 있다. 타깃 수가 상한과 정확히 같은지 보는 것과, 최악의 경우 호출 수를 계산해 총 한계와 비교하는 것이다.
|
|
|
|
둘 다 전략을 보지 않으므로 SINGLE 경로에서도 돈다. 갈라놓은 것은 switch 갈래 안에 들어갔는지 여부다.
|
|
|
|
## 고치는 방법
|
|
|
|
단언을 메시지 전문이나 그 갈래에만 있는 부분 문자열로 좁히면 된다. 순환이라는 단어나 잘못된 범위라는 문구가 그것이다.
|
|
|
|
지금 그 테스트를 돌리면 다섯 건이 통과한다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
첫 가드를 지운 사본을 만들어 돌려 보지는 않았다. 가드를 뺐을 때 각 라우트가 어디에 걸리는지는 실행 대신 범위 값과 갈래 조건으로 계산했다.
|
|
|
|
<!-- body:end -->
|