- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다 - 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5 (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를 techviz 로 만들었다 - 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs 돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다. Concept 이 인용한 코드가 SSOT 에 없어 뺐다 - candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
10 KiB
kind, slug, title, topic, project, status, sourceRevision, evidenceCapturedOn, rootTreeNode, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | evidenceCapturedOn | rootTreeNode | body | assets | evidence | source | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a16-f002-oneof | 플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다 | graphql-surface | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | 2026-09-03 | case:a16-f002-oneof | case-a16-f002-oneof.body.md |
|
|
|
플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다
단일 선택 입력 정책의 자바독이 플랫폼이 강제하는 규칙이라고 적었다. 강제하는 두 코드에 프로덕션 호출자가 없다. 그리고 스키마 게이트가 거부하는 잘못된 선언은 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 절이다.
- 정책 자바독 첫 문장이 강제 주체를 무엇으로 적었는지 읽는다.
- 스키마 게이트와 실행 시점 검증기를 자기 파일 밖에서 부르는 자리를 main·test·testFixtures 로 갈라 세고, 두 클래스의 생성자 접근 지정자를 읽는다.
- 잠금 파일에서 graphql-java 판본을 확인한다.
- 저장소의 스키마 파일을 찾아 그 지시자 선언이 있는지 본다.
- 규칙에 맞는 선언과 어긴 선언을 각각 graphql-java 에 넣어 스키마가 만들어지는지 본다.
- 같은 두 선언을 스키마 게이트에 넣어 결과를 대조한다.
- 규칙에 맞는 스키마에 구성원 하나, 구성원 둘, 아무것도 없는 값을 차례로 실어 보낸다.
- 이 계열 타입을 API 표면 목록에 적어 둔 문서 줄을 찾는다.
본문
정책 클래스 자바독의 첫 문장이 강제 주체를 밝힌다. 2025년 9월 판 단일 선택 입력 규칙을 플랫폼이 강제한다는 것이다.
강제하는 두 코드를 부르는 자리
:::evidence key="a16-f002-oneof" alt="저장소 루트에서 돌린 정적 검색 출력 69줄. 정책 클래스 자바독 전체와 지시자 상수 선언, 스키마 게이트와 실행 시점 검증기를 자기 파일 밖에서 부르는 자리가 각각 나오고 게이트 쪽에는 testFixtures 의 계약 스위트가 한 줄 섞여 있다. 이어서 정책을 참조하는 main 네 자리와 test 다섯 자리, 잠금 파일의 graphql-java 판본, 저장소의 스키마 파일 셋과 그중 지시자를 선언한 파일이 없다는 확인, 그 계약 스위트를 부르는 시험 세 자리와 그 시험의 태그와 기본 레인이 배제하는 태그, 두 강제 코드가 각각 비공개 생성자를 두어 인스턴스를 갖지 않는다는 줄, 그리고 이 계열 타입 넷을 API 표면 목록에 적어 둔 문서 네 줄이 보인다." caption="두 강제 코드의 호출자와 계약 스위트 경로·스키마·판본 대조 — 69줄 · exit 0" zoom="true" :::
두 강제 코드는 비공개 생성자만 두고 정적 메서드로 되어 있다. 컨테이너가 발행할 대상이 아니라 호출자가 직접 부르는 규칙 객체다. 그래서 어디에서 부르는지가 전부다.
실행 시점 검증기는 자기 시험에서만 불린다. 스키마 게이트는 같은 시험 파일 다섯 자리와, GraphQlSchemaContractSuite:41 한 자리에서 불린다.
// 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 는 기본값을 둘 수 없다고 따로 지목한다. 같은 선언을 스키마 게이트에 넣으면 같은 두 구성원을 같은 순서로 지목한다.
값 쪽도 라이브러리가 본다. 구성원 하나만 채우면 통과하고, 둘을 채우거나 아무것도 채우지 않으면 정확히 하나여야 한다며 거부한다.
원문과 갈리는 자리
원문은 게이트가 확인하는 선언 시점 규칙을 라이브러리가 확인하지 않는 부분이라고 적었고, 그래서 잘못된 선언이 첫 요청에서 드러난다고 했다. 위 실행이 둘 다 뒤집는다. 라이브러리가 같은 규칙을 스키마 만들 때 건다.
규칙 자체는 지켜진다. 지키는 주체가 자바독이 적은 주체와 다를 뿐이다.
확인하지 못한 것
계약 스위트를 실제로 돌려 보지 않았다. 확인한 것은 스위트가 게이트를 부르는 자리와, 이 저장소의 어떤 시험이 그 스위트를 부르며 어느 레인이 그 시험을 배제하지 않는지까지다.
애플리케이션 기동 경로 전체를 띄워 두 코드가 불리지 않는 것을 확인하지 않았다. 호출자를 전수로 세었을 뿐이다.
라이브러리가 선언을 거부하는 것은 스키마를 만드는 시점이다. 이 저장소의 배포가 스키마를 언제 만드는지, 그 실패가 기동 실패로 이어지는지는 보지 않았다.
프로브가 쓴 스키마는 두 구성원짜리 최소 예제다. 라이브러리가 더 복잡한 선언에서도 같은 검사를 하는지는 보지 않았다.