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>
133 lines
10 KiB
Markdown
133 lines
10 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a16-f002-oneof
|
|
title: 플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다
|
|
topic: graphql-surface
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
evidenceCapturedOn: 2026-09-03
|
|
rootTreeNode: case:a16-f002-oneof
|
|
body: case-a16-f002-oneof.body.md
|
|
assets:
|
|
- key: a16-f002-oneof
|
|
file: ../../../final/evidence/rendered/a16-f002-oneof.svg
|
|
- key: a16-f002-oneof-run
|
|
file: ../../../final/evidence/rendered/a16-f002-oneof-run.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a16-f002-oneof.txt
|
|
- ../../../final/evidence/raw/a16-f002-oneof-run.txt
|
|
source:
|
|
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L281 이다.
|
|
---
|
|
|
|
# 플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다
|
|
|
|
단일 선택 입력 정책의 자바독이 플랫폼이 강제하는 규칙이라고 적었다. 강제하는 두 코드에 프로덕션 호출자가 없다. 그리고 스키마 게이트가 거부하는 잘못된 선언은 graphql-java 25.0 이 이미 스키마를 만들 때 거부한다.
|
|
|
|
## 관계
|
|
|
|
- **크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다**
|
|
둘 다 게이트가 도는 자리가 시험 쪽이라서 배포에 대해 아무 말도 하지 않는다. 저쪽은 게이트가 보는 조립이 픽스처의 것이고, 여기는 게이트가 보는 규칙을 라이브러리가 이미 본다.
|
|
- **검증기는 발행이 아니라 주입이 강제다**
|
|
저쪽 규칙의 예외 절에 걸리는 사례다. 순수 함수형 규칙 객체는 호출자가 어디인지를 자바독이 대야 한다. 정책 자바독은 스키마 게이트와 실행 시점 검증기를 호출자로 대지만 그 둘의 자바독은 자기 호출자를 대지 않고, 그 둘을 실제로 부르는 자리는 시험과 픽스처뿐이다. 자바독이 댄 사슬이 프로덕션에서 끊긴다.
|
|
- **설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다**
|
|
둘 다 서술과 실제 적용 지점이 어긋난다. 저쪽은 스위치 밖이고 여기는 계층 밖이다.
|
|
|
|
## 문제
|
|
|
|
정책 자바독이 이 규칙을 플랫폼이 강제한다고 적었다. 강제하는 코드가 실제로 불리는지, 그리고 그 코드가 없으면 무엇이 달라지는지 확인했다.
|
|
|
|
## 결론
|
|
|
|
프로덕션에서는 불리지 않는다.
|
|
|
|
강제하는 코드는 둘이다. 선언을 보는 스키마 게이트와 값을 보는 실행 시점 검증기다. 실행 시점 검증기는 부르는 자리가 시험뿐이다. 스키마 게이트는 시험 다섯 자리와 계약 스위트 한 자리에서 불린다. 그 스위트는 testFixtures 에 있어 채택자도 쓸 수 있지만, 이 저장소에서도 GraphQlCrossModuleContractSuiteTest 가 세 자리에서 부르고, 그 시험의 태그를 기본 test 레인이 배제하지 않는다. 애플리케이션 기동 경로에는 둘 다 없다.
|
|
|
|
노출은 없다. 현재 세 스키마 파일 어디에도 그 지시자 선언이 없고, 라이브러리가 값 검사를 스스로 한다. 규칙에 맞게 선언한 스키마에 구성원 둘을 채워 보내면 라이브러리가 거부하고, 아무것도 채우지 않아도 거부한다.
|
|
|
|
여기서 원문과 갈린다.
|
|
|
|
원문은 게이트가 라이브러리의 사각을 메운다고 적고, 그래서 잘못된 타입이 첫 요청에서 드러난다고 했다. 돌려 보니 둘 다 아니다. graphql-java 25.0 이 같은 선언을 스키마 만들 때 거부하고, 게이트와 거의 같은 문장으로 같은 두 구성원을 지목한다. 드러나는 시점은 첫 요청이 아니라 스키마 조립이다.
|
|
|
|
남는 것은 하나다. 강제하는 계층이 자바독이 적은 계층과 다르다. 게이트가 계약 스위트에서 도는 것은 사실이지만 그것은 시험을 돌릴 때이지 배포가 뜰 때가 아니다.
|
|
|
|
판정은 P3 이고 원문과 같다. 노출이 없다는 결론도, 그 두 근거인 스키마에 지시자가 없다는 것과 라이브러리가 값을 본다는 것도 원문과 같다. 갈리는 것은 원문이 따로 적어 둔 선언 시점 주장뿐이다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
graphql-java : 25.0
|
|
확인 방식 : 두 강제 코드의 호출자 전수, 스키마 파일의 지시자 선언 검색, graphql-java 25.0 에 규칙에 맞는 선언과 어긴 선언을 각각 넣어 스키마 생성, 같은 두 선언을 스키마 게이트에 투입, 규칙에 맞는 스키마에 값 세 가지를 실어 실행
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
원문 근거는 분석 문서의 #L281 절이다.
|
|
|
|
1. 정책 자바독 첫 문장이 강제 주체를 무엇으로 적었는지 읽는다.
|
|
2. 스키마 게이트와 실행 시점 검증기를 자기 파일 밖에서 부르는 자리를 main·test·testFixtures 로 갈라 세고, 두 클래스의 생성자 접근 지정자를 읽는다.
|
|
3. 잠금 파일에서 graphql-java 판본을 확인한다.
|
|
4. 저장소의 스키마 파일을 찾아 그 지시자 선언이 있는지 본다.
|
|
5. 규칙에 맞는 선언과 어긴 선언을 각각 graphql-java 에 넣어 스키마가 만들어지는지 본다.
|
|
6. 같은 두 선언을 스키마 게이트에 넣어 결과를 대조한다.
|
|
7. 규칙에 맞는 스키마에 구성원 하나, 구성원 둘, 아무것도 없는 값을 차례로 실어 보낸다.
|
|
8. 이 계열 타입을 API 표면 목록에 적어 둔 문서 줄을 찾는다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
정책 클래스 자바독의 첫 문장이 강제 주체를 밝힌다. 2025년 9월 판 단일 선택 입력 규칙을 플랫폼이 강제한다는 것이다.
|
|
|
|
## 강제하는 두 코드를 부르는 자리
|
|
|
|
:::evidence key="a16-f002-oneof" alt="저장소 루트에서 돌린 정적 검색 출력 69줄. 정책 클래스 자바독 전체와 지시자 상수 선언, 스키마 게이트와 실행 시점 검증기를 자기 파일 밖에서 부르는 자리가 각각 나오고 게이트 쪽에는 testFixtures 의 계약 스위트가 한 줄 섞여 있다. 이어서 정책을 참조하는 main 네 자리와 test 다섯 자리, 잠금 파일의 graphql-java 판본, 저장소의 스키마 파일 셋과 그중 지시자를 선언한 파일이 없다는 확인, 그 계약 스위트를 부르는 시험 세 자리와 그 시험의 태그와 기본 레인이 배제하는 태그, 두 강제 코드가 각각 비공개 생성자를 두어 인스턴스를 갖지 않는다는 줄, 그리고 이 계열 타입 넷을 API 표면 목록에 적어 둔 문서 네 줄이 보인다." caption="두 강제 코드의 호출자와 계약 스위트 경로·스키마·판본 대조 — 69줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
두 강제 코드는 비공개 생성자만 두고 정적 메서드로 되어 있다. 컨테이너가 발행할 대상이 아니라 호출자가 직접 부르는 규칙 객체다. 그래서 어디에서 부르는지가 전부다.
|
|
|
|
실행 시점 검증기는 자기 시험에서만 불린다. 스키마 게이트는 같은 시험 파일 다섯 자리와, `GraphQlSchemaContractSuite:41` 한 자리에서 불린다.
|
|
|
|
```java
|
|
// GraphQlSchemaContractSuite.java:38-44
|
|
GraphQlSchemaAssemblyResult assembled = GraphQlSchemaAssembler.defaults().assemble(resources);
|
|
|
|
try {
|
|
GraphQlOneOfSchemaGate.verify(assembled.registry());
|
|
} catch (RuntimeException ex) {
|
|
violations.add("@oneOf declaration is invalid: " + ex.getMessage());
|
|
}
|
|
```
|
|
|
|
그 스위트는 `testFixtures` 소스 세트에 있어 채택자가 자기 스키마를 검사할 때 쓸 수 있고, 이 저장소에서는 `GraphQlCrossModuleContractSuiteTest` 가 세 자리에서 부른다. 그 시험은 `@Tag("graphql-contract")` 를 달았고 기본 `test` 레인이 그 태그를 배제하지 않는다. 시험을 돌리면 여기서도 돈다. 어느 쪽이든 시험이지 배포가 뜰 때 도는 코드가 아니다.
|
|
|
|
스키마 파일은 셋이고 어디에도 그 지시자 선언이 없다.
|
|
|
|
## 라이브러리가 어디까지 하는가
|
|
|
|
:::evidence key="a16-f002-oneof-run" alt="JVM 프로브 출력 16줄. 맨 첫 줄에 OpenJDK 판이 찍히고, graphql-java 25.0 에 규칙에 맞는 선언과 어긴 선언을 각각 넣은 결과가 먼저 나오고, 어긴 쪽은 스키마 생성 단계에서 예외가 나며 비널 구성원과 기본값 구성원을 각각 지목한다. 이어서 같은 두 선언을 스키마 게이트에 넣은 결과가 같은 모양으로 갈린다. 끝으로 규칙에 맞는 스키마에 구성원 하나만 채운 값, 구성원 둘을 채운 값, 아무것도 채우지 않은 값을 실어 보낸 세 결과가 보인다." caption="같은 두 선언에 대한 라이브러리와 게이트의 판정, 그리고 값 세 가지의 실행 결과 — 16줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
규칙을 어긴 선언을 라이브러리에 넣으면 스키마가 만들어지지 않는다. `id` 는 널 허용이어야 한다고, `number` 는 기본값을 둘 수 없다고 따로 지목한다. 같은 선언을 스키마 게이트에 넣으면 같은 두 구성원을 같은 순서로 지목한다.
|
|
|
|
값 쪽도 라이브러리가 본다. 구성원 하나만 채우면 통과하고, 둘을 채우거나 아무것도 채우지 않으면 정확히 하나여야 한다며 거부한다.
|
|
|
|
## 원문과 갈리는 자리
|
|
|
|
원문은 게이트가 확인하는 선언 시점 규칙을 라이브러리가 확인하지 않는 부분이라고 적었고, 그래서 잘못된 선언이 첫 요청에서 드러난다고 했다. 위 실행이 둘 다 뒤집는다. 라이브러리가 같은 규칙을 스키마 만들 때 건다.
|
|
|
|
규칙 자체는 지켜진다. 지키는 주체가 자바독이 적은 주체와 다를 뿐이다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
계약 스위트를 실제로 돌려 보지 않았다. 확인한 것은 스위트가 게이트를 부르는 자리와, 이 저장소의 어떤 시험이 그 스위트를 부르며 어느 레인이 그 시험을 배제하지 않는지까지다.
|
|
|
|
애플리케이션 기동 경로 전체를 띄워 두 코드가 불리지 않는 것을 확인하지 않았다. 호출자를 전수로 세었을 뿐이다.
|
|
|
|
라이브러리가 선언을 거부하는 것은 스키마를 만드는 시점이다. 이 저장소의 배포가 스키마를 언제 만드는지, 그 실패가 기동 실패로 이어지는지는 보지 않았다.
|
|
|
|
프로브가 쓴 스키마는 두 구성원짜리 최소 예제다. 라이브러리가 더 복잡한 선언에서도 같은 검사를 하는지는 보지 않았다.
|
|
|
|
<!-- body:end -->
|