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:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+132
@@ -0,0 +1,132 @@
|
||||
---
|
||||
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 -->
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a16-f004-graphqloperationnamepolicy
|
||||
title: 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 GraphQlOperationNamePolicy를 쓴다
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a16-f004-graphqloperationnamepolicy
|
||||
evidenceCapturedOn: 2026-09-03
|
||||
assets:
|
||||
- key: a16-f004-graphqloperationnamepolicy
|
||||
file: ../../../final/evidence/rendered/a16-f004-graphqloperationnamepolicy.svg
|
||||
- key: a16-f004-graphqloperationnamepolicy-run
|
||||
file: ../../../final/evidence/rendered/a16-f004-graphqloperationnamepolicy-run.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a16-f004-graphqloperationnamepolicy.txt
|
||||
- ../../../final/evidence/raw/a16-f004-graphqloperationnamepolicy-run.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L403 이다. 세 구현의 참조 계수는 정적 검색으로, 두 구현의 판정 차이는 프로브 실행으로 확인했다.
|
||||
---
|
||||
|
||||
# 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 GraphQlOperationNamePolicy를 쓴다
|
||||
|
||||
이름 없는 연산을 운영에서 거부하는 규칙이 `GraphQlOperationNamePolicy` 와 `GraphQlOperationSelectionHandler` 와 `GraphQlRequestEnvelopeValidator` 셋에 나뉘어 있고, 자동설정이 조립하는 것은 `GraphQlOperationSelectionHandler` 하나다. 조립되는 `GraphQlClientPolicy` 는 `defaults(...)` 가 만드는 것 하나뿐이고 그것이 `namedOperationRequired` 에 상수 `false` 를 넣으므로, `GraphQlOperationSelectionHandler` 의 이름 요구 분기는 출하 상태에서 실행되지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다**
|
||||
둘 다 프로파일마다 다른 값을 고르도록 타입을 만들어 두고, 조립은 기본값 하나만 넣는다.
|
||||
- **플랫폼이 강제한다고 적었지만 그 규칙을 실제로 거는 것은 라이브러리다**
|
||||
둘 다 자바독이 적은 강제 주체와 그 검사를 실제로 도는 코드가 어긋난다.
|
||||
- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다**
|
||||
이름을 요구하는 코드가 셋인데, 어느 것이 조립되는지 보기 전에는 규칙이 걸린다고 읽게 된다.
|
||||
|
||||
## 문제
|
||||
|
||||
익명 연산을 거부하는 규칙이 자바독에 적혀 있다.
|
||||
|
||||
그 규칙을 거는 코드가 몇 개이고 그중 무엇이 조립되는지, 조립된 것이 실제로 그 검사를 도는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
거는 코드는 셋이고, 조립되는 것은 하나다.
|
||||
|
||||
GraphQlOperationNamePolicy 는 미배선 인터셉터의 필드와 생성자에서만 참조되고, 그 인터셉터를 만드는 코드는 GraphQlOperationNamePolicyTest:94 한 줄이다. GraphQlRequestEnvelopeValidator 도 같은 불리언을 읽지만 생성 지점 아홉이 전부 test 와 testFixtures 다. 남는 GraphQlOperationSelectionHandler 는 GraphQlExecutionChain 과 GraphQlPlatformInstrumentation 을 거쳐 요청마다 돈다.
|
||||
|
||||
그런데 그 핸들러가 읽는 namedOperationRequired 가 조립되는 정책에서 거짓이다.
|
||||
|
||||
GraphQlClientPolicy 를 만드는 프로덕션 코드가 defaults(...) 하나이고, 열여섯 번째 성분에 상수 false 가 들어간다. 프로브로 돌려 보니 익명 단일 연산이 통과하고 operationId 가 anonymous 로 찍힌다. 같은 핸들러에 그 성분만 참으로 바꾸면 같은 문서가 거부된다.
|
||||
|
||||
배선된 경로가 언제나 거는 것은 따로 있다. 문서에 연산이 하나도 없을 때, 요청한 이름과 맞는 연산이 문서에 없을 때, 그리고 연산이 여럿인데 어느 것을 돌릴지 지정하지 않았을 때다. 이름 요구와 달리 이 셋은 클라이언트 프로파일을 보지 않는다.
|
||||
|
||||
두 구현은 같은 입력에 반대로 답하기도 한다. 두 글자짜리 이름을 미배선 정책에 넣으면 IllegalArgumentException 이 나오고, 배선된 핸들러는 통과시킨 뒤 식별자를 anonymous 로 정규화한다. ADMIN 프로파일을 요구에서 빼는 예외도 미배선 쪽에만 있다.
|
||||
|
||||
판정은 원문과 같은 P3 이고, 노출이 없다는 것이 근거다. 근거는 셋으로 갈린다. 미배선 정책에는 실행 경로가 없다. 어댑터 자체가 backend.graphql.enabled 로 꺼져 있는 것이 출하 기본값이다. 그리고 배선된 핸들러가 심는 operationId 를 읽는 프로덕션 코드가 이 모듈에 없어서, anonymous 로 뭉개진 식별자가 지금 아무 데로도 흘러가지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : 세 구현의 바깥 참조 전수 계수, 조립되는 정책 빈과 defaults 의 성분 확인, 배선 사슬과 어댑터 프로퍼티 조건 확인, GraphQlOperationSelectionHandler.handle 과 GraphQlOperationNamePolicy.verify 를 문서 넷과 프로파일 둘로 직접 실행
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. GraphQlOperationNamePolicy 의 자바독에서 이름을 요구하는 이유를 읽는다.
|
||||
2. GraphQlOperationNamePolicy 를 언급하는 파일을 소스 세트별로 모아 센다.
|
||||
3. 그것을 쓰는 인터셉터를 생성하는 자리가 어느 소스 세트에 있는지 확인한다.
|
||||
4. namedOperationRequired 를 읽는 프로덕션 코드를 전부 찾고, 그 클래스들을 만드는 자리도 함께 센다.
|
||||
5. GraphQlClientPolicy 를 만드는 프로덕션 코드와 그 빈에 붙은 조건 애너테이션을 확인한다.
|
||||
6. defaults 가 마지막 세 성분에 넣는 값과 그 성분의 이름을 대조한다.
|
||||
7. 배선된 핸들러가 실행 사슬과 Instrumentation 을 거쳐 요청 경로에 오르는 자리와, 어댑터 자체의 프로퍼티 조건을 읽는다.
|
||||
8. 조립되는 값 그대로 GraphQlOperationSelectionHandler.handle 에 익명 단일 연산, 이름이 Ab 인 연산, 이름이 HealthQuery 인 연산, 연산 둘짜리 문서를 넣는다.
|
||||
9. 같은 핸들러에 namedOperationRequired 만 참으로 바꿔 익명 단일 연산을 다시 넣는다.
|
||||
10. GraphQlOperationNamePolicy.production().verify 에 PUBLIC 의 이름 없는 선택과 ADMIN 의 이름 없는 선택, 그리고 PUBLIC 의 Ab 와 HealthQuery 를 넣는다.
|
||||
11. GraphQlOperationName.parse 의 본문에서 빈 Optional 이 나오는 조건을 읽는다.
|
||||
12. GraphQlRequestContext 의 operationId 를 읽는 프로덕션 코드를 센다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GraphQlOperationNamePolicy` 의 자바독에는 운영에서 연산에 이름을 요구하는 이유가 적혀 있다. 규격은 익명 연산 하나를 허용하고 로컬에서는 그게 편하지만, 운영에서는 추적과 비용 예외와 persisted operation 레지스트리와 사용량 분석이 공통으로 키에 쓰는 연산 이름이 없어진다. 이름이 없으면 대신 쓸 수 있는 키는 원본 문서 문자열뿐인데, 그 문자열은 길이 상한이 없고 변수까지 들어 있다.
|
||||
|
||||
## 이름 요구를 거는 코드 셋과 각각의 바깥 참조
|
||||
|
||||
:::evidence key="a16-f004-graphqloperationnamepolicy" alt="저장소 루트에서 돌린 정적 검색 출력 89줄. 같은 불리언을 읽는 세 클래스의 줄 수가 먼저 나오고, GraphQlOperationNamePolicy 를 자기 파일 밖에서 쓰는 자리가 main 두 줄과 test 여덟 줄로 갈려 보인다. GraphQlOperationNameInterceptor 의 바깥 참조는 시험 한 줄뿐이고, GraphQlRequestEnvelopeValidator 를 실제로 만드는 아홉 자리는 전부 test 와 testFixtures 다. 이어서 namedOperationRequired 를 읽는 프로덕션 세 줄, GraphQlClientPolicy 인스턴스를 만드는 프로덕션 코드 전부와 그 빈을 내놓는 메서드의 조건 애너테이션, defaults 가 마지막 세 성분에 넣는 값 셋과 그 성분의 이름 셋이 나란히 나온다. 그다음 배선된 핸들러가 실행 사슬과 Instrumentation 을 거쳐 요청 경로에 오르는 자리들과 어댑터 자체의 프로퍼티 조건, GraphQlRequestContext 의 operationId 를 읽는 프로덕션 코드가 0 건이라는 확인, 그리고 GraphQlOperationNamePolicy 54행부터 61행까지와 GraphQlOperationName.parse 의 본문이 보인다." caption="세 구현의 바깥 참조 · namedOperationRequired 소비자 셋 · defaults 의 마지막 세 값 · 배선 사슬과 어댑터 조건 · operationId 소비자 0 — 89줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
`GraphQlOperationNamePolicy`(85줄)를 자기 파일 밖에서 쓰는 프로덕션 코드는 `GraphQlOperationNameInterceptor` 의 필드 선언(\:17)과 생성자(\:24) 두 줄이다. 그 인터셉터를 만드는 자리는 저장소 전체에서 `GraphQlOperationNamePolicyTest:94` 하나이고, 자동설정에는 없다.
|
||||
|
||||
`GraphQlRequestEnvelopeValidator`(152줄)의 `validateEnvelope` 도 같은 불리언을 읽어 이름 없는 요청을 거부한다(\:134). 이 클래스를 만드는 자리는 아홉인데 여덟은 자기 시험이고 나머지 하나가 `GraphQlContractFixture:84` 다. 프로덕션 조립에는 없다.
|
||||
|
||||
남는 것이 `GraphQlOperationSelectionHandler`(125줄)다. `GraphQlPlatformAutoConfiguration:404` 가 `new GraphQlOperationSelectionHandler(clientPolicy)` 로 만들어 `:403` 의 `GraphQlExecutionChain.stable` 에 넣고, `:420` 이 그 사슬을 `GraphQlPlatformInstrumentation` 으로 감싼다. Spring for GraphQL 이 `Instrumentation` 빈을 집어가므로 `GraphQlPlatformInstrumentation:81` 의 `chain.run` 이 실제 요청마다 돈다. 이 핸들러의 생성자가 받는 것은 `GraphQlOperationNamePolicy` 가 아니라 `GraphQlClientPolicy` 다.
|
||||
|
||||
어댑터 자체는 기본으로 꺼져 있다. `GraphQlRootAutoConfiguration:21` 이 `@ConditionalOnProperty(prefix = "backend.graphql", name = "enabled", havingValue = "true")` 를 달고 있어서, 아래의 통과와 거부는 그 프로퍼티를 켠 배포에서만 일어난다.
|
||||
|
||||
## 조립되는 GraphQlClientPolicy 의 namedOperationRequired 는 false 다
|
||||
|
||||
`GraphQlClientPolicy` 인스턴스를 만드는 프로덕션 코드는 `GraphQlClientPolicy.defaults` 와 그 안의 생성자 호출뿐이고, 그 `defaults` 를 부르는 자리는 `GraphQlPlatformAutoConfiguration:303` 하나다. 그 메서드에 `@ConditionalOnMissingBean` 이 붙어 있어 채택자가 자기 빈을 내놓지 않으면 이것이 쓰인다.
|
||||
|
||||
```java
|
||||
// GraphQlClientPolicy.java:85-104 (마지막 세 성분)
|
||||
public static GraphQlClientPolicy defaults(
|
||||
int maximumPageSize, long maximumComplexity, boolean introspectionAllowed) {
|
||||
return new GraphQlClientPolicy(
|
||||
…,
|
||||
introspectionAllowed, // :101 → :49 introspectionAllowed
|
||||
false, // :102 → :50 persistedOperationOnly
|
||||
false); // :103 → :51 namedOperationRequired
|
||||
}
|
||||
```
|
||||
|
||||
레코드 성분 순서에서 `namedOperationRequired` 는 열여섯 번째이자 마지막(\:51)이고, `defaults` 가 그 자리에 넣는 값은 상수 `false` 다. 이 값을 프로퍼티로 바꿀 수 있는 경로는 없다.
|
||||
|
||||
## 같은 문서를 배선된 핸들러는 통과시키고 미배선 정책은 거부한다
|
||||
|
||||
:::evidence key="a16-f004-graphqloperationnamepolicy-run" alt="JVM 프로브 출력 19줄. 맨 앞에 OpenJDK 판이 찍히고, 조립되는 defaults 가 namedOperationRequired 에 false 를 넣는다는 것이 먼저 나온다. 이어서 배선된 GraphQlOperationSelectionHandler 에 그 값을 그대로 넣었을 때 익명 단일 연산과 이름이 Ab 인 연산이 모두 통과하고 둘 다 operationId 가 anonymous 로 찍히며, 이름이 HealthQuery 인 연산은 통과하면서 operationId 가 healthquery 로 정규화되고, 연산 둘에 operationName 이 없는 문서만 거부된다. 같은 핸들러에 namedOperationRequired 만 true 로 바꾸면 익명 단일 연산이 거부된다. 끝으로 미배선 GraphQlOperationNamePolicy.production().verify 가 PUBLIC 의 이름 없는 연산은 GraphQlAnonymousOperationException 으로 거부하고, ADMIN 의 이름 없는 연산은 통과시키며, 이름이 Ab 인 연산에서는 IllegalArgumentException 을 던지고, 이름이 HealthQuery 인 연산은 통과시키는 것이 보인다." caption="조립되는 값과 두 구현의 판정 · 같은 이름에 통과와 예외가 갈린다 — 19줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
조립되는 값 그대로 배선된 핸들러에 문서를 넣으면 익명 단일 연산이 통과한다. `:74` 의 `policy.namedOperationRequired() && selected.getName() == null` 에서 앞쪽이 거짓이라 뒤를 보지 않는다. 같은 핸들러에 그 성분만 `true` 로 바꾸면 같은 문서가 `GraphQlAnonymousOperationException` 으로 거부된다.
|
||||
|
||||
거부가 남아 있는 것은 셋이다. 연산이 하나도 없는 문서(\:53), `operationName` 이 가리키는 연산이 문서에 없는 경우(\:65), 그리고 연산이 둘 이상인데 `operationName` 이 없는 경우(\:68)다. 마지막 것을 거절이 아니라 기본 선택으로 두면 클라이언트가 문서 순서를 바꿔 실행 대상을 바꿀 수 있고 그것이 인가 우회가 된다고 이 핸들러의 클래스 자바독이 적는다. 이 셋은 클라이언트 프로파일과 무관하게 항상 건다.
|
||||
|
||||
두 구현이 반대로 답하는 입력도 있다. 연산 이름이 `Ab` 인 문서에서 배선된 핸들러는 통과시키고 `operationId` 를 `anonymous` 로 정규화한다. `MINIMUM_OPERATION_ID_LENGTH` 가 3 이라 두 글자는 식별자가 되지 못한다. 정규화 메서드의 자바독에는 이름 규칙 때문에 유효한 요청을 거절하지는 않는다고 적혀 있다. 미배선 정책에 같은 이름을 넣으면 `IllegalArgumentException : invalid GraphQL operation name` 이 나온다.
|
||||
|
||||
프로파일 예외도 한쪽에만 있다. `GraphQlOperationNamePolicy:49` 는 `production && !ADMIN.equals(client.value())` 로 ADMIN 을 요구에서 뺀다. 배선된 경로에는 프로파일별 분기가 없고 불리언 하나를 읽는다.
|
||||
|
||||
## GraphQlOperationNamePolicy\:57 은 실행되지 않는다
|
||||
|
||||
이름이 규칙을 어겼을 때 `GraphQlAnonymousOperationException` 을 던지려고 쓴 검사인데, 그 예외는 나오지 않는다.
|
||||
|
||||
```java
|
||||
// GraphQlOperationNamePolicy.java:54-61
|
||||
if (namedRequired && !selection.named()) {
|
||||
throw new GraphQlAnonymousOperationException("named operation required");
|
||||
}
|
||||
if (selection.named() && GraphQlOperationName.parse(selection.operationName()).isEmpty()) {
|
||||
// Validates the bounded naming pattern; an unbounded name would defeat the reason for
|
||||
// requiring one at all.
|
||||
throw new GraphQlAnonymousOperationException("named operation required");
|
||||
}
|
||||
```
|
||||
|
||||
`selection.named()` 가 참이면 `operationName` 은 널도 공백도 아니다. 그 값을 받은 `GraphQlOperationName.parse` 는 빈 `Optional` 을 돌려주는 분기(널이거나 공백)에 들어가지 않고 `new GraphQlOperationName(candidate)` 로 간다. 그 생성자는 `[A-Za-z][_0-9A-Za-z]{2,127}` 에 맞지 않으면 `IllegalArgumentException` 을 던진다. 그래서 `isEmpty()` 는 이 자리에서 참이 될 수 없고, 60행의 `throw` 는 실행되지 않는다.
|
||||
|
||||
`GraphQlRequestEnvelopeValidator:134` 에도 `parse(...).isEmpty()` 를 보는 같은 검사가 있지만 결과가 다르다. `envelope.operationName()` 은 클라이언트가 보내지 않으면 널이므로 `parse` 가 빈 `Optional` 을 돌려주고, 의도한 `GraphQlRequestFormatException` 이 나온다.
|
||||
|
||||
## 원문과 갈리는 자리
|
||||
|
||||
원문 §12.2 는 익명 연산 거부를 배선된 `GraphQlOperationSelectionHandler` 가 수행하므로 강제 자체는 존재한다고 적고, 기록할 것은 정책 객체의 이원화라고 했다. 위 실행은 그중 강제가 존재한다는 부분을 뒤집는다. 조립되는 `GraphQlClientPolicy` 가 `namedOperationRequired` 에 `false` 를 넣으므로, 이름을 요구하는 강제는 출하 상태에서 어느 코드도 걸지 않는다. 배선된 핸들러가 항상 거는 것은 연산이 없는 문서, 지정한 이름의 연산이 없는 문서, 연산이 여럿인데 `operationName` 이 없는 문서 세 가지이지 이름 요구가 아니다.
|
||||
|
||||
원문 §11.2 가 미배선 인터셉터를 누락이 아니라 중복이라고 판정한 것도 함께 흔들린다. `GraphQlOperationNamePolicy.production()` 은 ADMIN 이 아닌 프로파일에 이름을 강제하는데 배선된 핸들러는 지금 그것을 강제하지 않으므로, 두 구현은 같은 일을 하지 않는다.
|
||||
|
||||
원문의 인용에도 어긋난 곳이 둘 있다. 배선된 검사를 `:75` 로 적었는데 실제는 `:74` 다. 그리고 `GraphQlOperationNamePolicy` 의 참조자를 미배선 인터셉터와 자기 자신뿐이라고 적었는데 자기 시험 여덟 줄이 빠졌다.
|
||||
|
||||
이원화 판정과 P3 등급은 원문과 같다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
프로브는 실행 사슬의 한 단계만 직접 불렀다. 요청을 엔드포인트로 쏘아 응답을 받아 본 것은 아니다.
|
||||
|
||||
채택자가 `GraphQlClientPolicy` 빈을 직접 등록하면 `@ConditionalOnMissingBean` 이라 `namedOperationRequired` 가 참이 될 수 있다. 그런 배포를 띄워 보지는 않았다.
|
||||
|
||||
브라우저나 HTTP 클라이언트로 실제 GraphQL 요청을 보내지 않았다. 프로브는 `GraphQlOperationSelectionHandler.handle` 을 직접 불렀고, 그 앞뒤에 있는 전송 계층과 인증 계층은 거치지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a16-f001
|
||||
title: 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a16-f001
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a16-f001.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a16-f001
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a16-f001.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a16-f001.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L258 이다.
|
||||
---
|
||||
|
||||
# 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다
|
||||
|
||||
조립기부터 보고서까지 사슬 전체가 끊겨 있다. 각 단계의 호출자와 생산자와 빈 선언이 모두 0 이다. 그 결과 결정적 병합 순서와 네 부분 계약 정체성과 운영 가시성이 함께 사라진다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **시작 검증기가 시작 시 실행되지 않는다**
|
||||
다른 리프의 같은 형태다.
|
||||
- **5계층 예산 모델에서 요청 계층만 강제되고 나머지 파생이 전부 미배선이다**
|
||||
같은 리프의 다른 미배선 사슬이다.
|
||||
- **만들어졌으나 아무도 만들지 않는 타입은 계약이 아니다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프는 스키마 조각을 병합해 스키마를 만들고, 그 결과에서 해시를 얻고, 해시를 포함한 계약 정체성을 만들고, 그것을 운영 보고서로 발행하도록 설계되어 있다.
|
||||
|
||||
각 단계가 실제로 불리는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
전 단계가 끊겨 있다.
|
||||
|
||||
조립 메서드의 호출자가 0 이고, 조립 결과 타입의 생산자가 0 이고, 해시 타입의 생산자가 0 이다.
|
||||
|
||||
보고 엔드포인트의 빈 선언이 0 이고, 그 보고서의 발행 경로가 0 이다.
|
||||
|
||||
네 부분 계약 정체성 타입의 생성자 호출도 0 이다.
|
||||
|
||||
세 가지가 함께 사라진다.
|
||||
|
||||
첫째는 결정적 병합 순서다.
|
||||
|
||||
스키마 조각의 정렬을 조립기가 강제하도록 설계되어 있는데, 실제로는 프레임워크의 탐색 순서를 그대로 쓴다.
|
||||
|
||||
조각이 하나뿐인 지금은 무해하다. 그러나 채택자가 자기 조각을 추가하는 순간 달라진다. 그것이 이 리프의 문서화된 확장 방식이다.
|
||||
|
||||
충돌 선언의 승자와 스키마 해시가 포장 방식에 따라 달라질 수 있다.
|
||||
|
||||
둘째는 네 부분 계약 정체성이다.
|
||||
|
||||
해시만으로는 호환성을 판정할 수 없다는 판단을 타입으로 만들었는데, 그 타입을 만드는 코드가 없다.
|
||||
|
||||
호환성 판정이 필요한 릴리스 게이트는 별도의 비교기를 직접 쓴다.
|
||||
|
||||
셋째는 운영 가시성이다.
|
||||
|
||||
보고 메서드가 배포된 스키마 해시와 실행 프로파일과 배포 모드와 활성 능력과 등록된 연산 및 페치 프로파일 수를 하나의 보고서로 낸다.
|
||||
|
||||
등록되지 않으므로 운영자가 이 배포가 무엇을 켜고 있는가를 물을 표면이 없다.
|
||||
|
||||
다른 리프의 시작 검증기 사례와 같은 형태다.
|
||||
|
||||
거기서는 시작 검증기가 시작 시 실행되지 않았고, 여기서는 보고 엔드포인트가 등록되지 않는다.
|
||||
|
||||
두 모듈 모두 파일 서버 하위 트리와 대조된다. 그쪽의 증명 메서드는 부트스트랩에서 실제로 호출된다.
|
||||
|
||||
권고는 이렇다.
|
||||
|
||||
플랫폼 자동 설정이 이미 서른아홉 빈을 만들고 해시 타입을 세 곳에서 참조하므로 자리는 있다.
|
||||
|
||||
스키마 원본이 확정된 뒤 그 정의로 조립기를 돌려 해시를 얻고, 그것으로 엔드포인트를 등록하면 된다.
|
||||
|
||||
판정은 P2 다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 사슬 각 단계의 호출자와 생산자 계수
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/182 계열에 있다.
|
||||
|
||||
1. 조립 메서드의 호출자를 센다.
|
||||
2. 조립 결과와 해시 타입의 생산자를 센다.
|
||||
3. 보고 엔드포인트의 빈 선언을 검색한다.
|
||||
4. 계약 정체성 타입의 생성자 호출을 센다.
|
||||
5. 릴리스 게이트가 호환성을 어떻게 판정하는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
세 가지가 통째로 미배선이다.
|
||||
|
||||
## GraphQlSchemaContract 참조 위치
|
||||
|
||||
:::evidence key="analysis-finding-a16-f001" alt="코드베이스에서 GraphQlSchemaContract 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlSchemaContract 코드베이스 검색 — 4줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 1. 결정적 병합 순서
|
||||
|
||||
SDL 조각의 정렬을 조립기가 강제하도록 설계돼 있고(§7.4), 실제로는 Spring GraphQL의 탐색 순서를 그대로 쓴다. 조각이 하나(`skeleton.graphqls`)뿐인 지금은 무해하지만, adopter가 자기 `.graphqls`를 추가하는 순간 — 그것이 이 leaf의 문서화된 확장 방식이다 — 충돌 선언의 승자와 스키마 해시가 패키징 방식에 따라 달라질 수 있다.
|
||||
|
||||
## 2. 네 부분 계약 정체성
|
||||
|
||||
`GraphQlSchemaContract`가 "해시만으로는 호환성을 판정할 수 없다"는 판단을 타입으로 만들었는데, 그 타입을 만드는 코드가 없다. 호환성 판정이 필요한 곳(릴리스 게이트)은 `compat`의 비교기를 직접 쓴다.
|
||||
|
||||
## 3. 운영 가시성
|
||||
|
||||
`GraphQlPlatformActuatorEndpoint.report()`가 배포된 스키마 해시 · 실행 프로파일 · 배포 모드 · 활성 능력 · 등록된 연산/페치 프로파일 수를 하나의 보고서로 낸다. 등록되지 않으므로 운영자가 "이 배포가 무엇을 켜고 있는가"를 물을 표면이 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
조각을 추가해 병합 순서가 달라지는 것을 재현하지 않았다. 조각이 하나뿐이라 현재 형상에서는 드러나지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a16-f003
|
||||
title: 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a16-f003
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a16-f003.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a16-f003
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a16-f003.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a16-f003.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L382 이다.
|
||||
---
|
||||
|
||||
# 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다
|
||||
|
||||
마감 전파기의 자바독이 다섯 계층이 왜 분리되는지 정확히 적는다. 그 파생을 수행하는 다섯 메서드 전부 프로덕션 호출자가 0 이다. 요청 전체 마감만 작동한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **스키마 조립과 계약 정체성과 해시 사슬이 통째로 미배선이고 그것을 발행할 엔드포인트도 등록되지 않는다**
|
||||
같은 리프의 다른 미배선 사슬이다.
|
||||
- **용량 보호 계층 전체가 자기 테스트 픽스처 안에서만 실행된다**
|
||||
같은 형태의 미등록 사례다.
|
||||
- **계층을 나눈 이유가 코드에 적혀 있어도 파생이 없으면 계층은 하나다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프는 마감을 다섯 계층으로 나눈다.
|
||||
|
||||
마감 전파기의 자바독이 그 이유를 적는다.
|
||||
|
||||
계층이 서로 다른 것을 뜻하고 서로 다른 시점에 만료되기 때문이라는 것이다. 전송 악수와 요청 실행과 하나의 해석기와 하나의 적재 배치, 그리고 구독의 경우 연결 자체다.
|
||||
|
||||
마지막 것은 의도적으로 요청 예산에서 파생하지 않는다는 것이다. 구독은 오래 사는 흐름이고 오 초 요청 시간 제한을 적용하면 모든 구독이 시작 오 초 뒤에 끝나기 때문이라는 것이다.
|
||||
|
||||
## 결론
|
||||
|
||||
그 파생을 수행하는 메서드 다섯 개 전부 프로덕션 호출자가 0 이다.
|
||||
|
||||
실제로 강제되는 것과 아닌 것이 갈린다.
|
||||
|
||||
요청 전체 마감은 작동한다. 플랫폼 인터셉터가 만들고 취소 장치가 끊는다.
|
||||
|
||||
개별 해석기가 남은 요청 예산으로 잘리는 것은 없다.
|
||||
|
||||
적재 배치 시간 제한이 남은 요청 예산으로 잘리는 것도 없다.
|
||||
|
||||
하류 호출에 남은 예산이 전달되는 것도 없다.
|
||||
|
||||
실패 시나리오는 이렇다.
|
||||
|
||||
요청 예산이 오 초이고 해석기 하나가 하류 호출을 부른다.
|
||||
|
||||
그 호출에 전달되는 마감은 나가는 어댑터 자신의 기본값이고, 남은 요청 예산이 일 초라는 사실은 전달되지 않는다.
|
||||
|
||||
요청은 오 초에 취소되지만 하류 호출은 계속 진행되어 연결과 스레드를 사 초 더 붙잡는다.
|
||||
|
||||
전파기의 하류 마감 메서드가 정확히 그 죄기를 위해 존재한다.
|
||||
|
||||
적재 쪽은 더 직접적이다.
|
||||
|
||||
배치 문맥이 마감을 레코드 성분으로 갖지만, 그 값을 남은 요청 예산으로 잘라 넣는 코드가 배치 시간 제한 메서드이고 호출자가 없다.
|
||||
|
||||
권고는 전파기를 빈으로 등록하고 세 지점에 연결하는 것이다.
|
||||
|
||||
배치 등록기가 배치 시간 제한을, 해석기 실행 경로가 해석기 예산을, 나가는 포트 호출 지점이 하류 마감을 쓰게 한다.
|
||||
|
||||
구독 계층은 별도 하위 범위에서 확인한다.
|
||||
|
||||
판정은 P2 다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 파생 메서드 호출자 계수와 강제 지점 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/182 계열에 있다.
|
||||
|
||||
1. 마감 전파기의 자바독을 읽는다.
|
||||
2. 파생 메서드 다섯을 나열한다.
|
||||
3. 각 메서드의 프로덕션 호출자를 센다.
|
||||
4. 요청 전체 마감이 어디서 만들어지고 어디서 끊기는지 확인한다.
|
||||
5. 배치 문맥이 마감을 어떻게 받는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GraphQlDeadlinePropagator`의 javadoc이 계층 분리의 이유를 정확히 적는다.
|
||||
|
||||
> "The layers are separate because they mean different things and expire at different points: a transport handshake, the request execution, one resolver, one DataLoader batch, and — for a subscription — the connection itself. The last one is deliberately not derived from the request budget: **a subscription is a long-lived stream, and applying a five-second request timeout to it would terminate every subscription five seconds after it started.**"
|
||||
|
||||
## GraphQlDeadlinePropagator 참조 위치
|
||||
|
||||
:::evidence key="analysis-finding-a16-f003" alt="코드베이스에서 GraphQlDeadlinePropagator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlDeadlinePropagator 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 파생 메서드 다섯 전부 호출자가 0이다
|
||||
|
||||
§11.1·§11.3.
|
||||
|
||||
## 강제되는 것과 아닌 것
|
||||
|
||||
요청 전체 데드라인은 `GraphQlPlatformWebInterceptor`가 만들고 `GraphQlCancellation`이 끊는다 — 작동한다. 개별 리졸버가 남은 요청 예산으로 잘리는 것, DataLoader 배치 타임아웃이 남은 요청 예산으로 잘리는 것, DB/HTTP 다운스트림 호출에 남은 예산이 전달되는 것은 없다.
|
||||
|
||||
## 실패 시나리오
|
||||
|
||||
요청 예산이 5초이고 리졸버 하나가 다운스트림 HTTP를 부른다. 그 호출에 전달되는 데드라인은 아웃바운드 어댑터 자신의 기본값(예: 10초)이고, 남은 요청 예산이 1초라는 사실은 전달되지 않는다. 요청은 5초에 취소되지만 다운스트림 호출은 계속 진행되어 연결과 스레드를 4초 더 붙잡는다. `GraphQlDeadlinePropagator.downstreamDeadline`이 정확히 그 clamping을 위해 존재한다. DataLoader 쪽은 더 직접적이다 — `dataloader/GraphQlBatchContext`가 `GraphQlDeadline`을 레코드 컴포넌트로 갖지만, 그 값을 남은 요청 예산으로 잘라 넣는 코드가 `dataLoaderBatchTimeout`이고 호출자가 없다. P2.
|
||||
|
||||
## 권고
|
||||
|
||||
`GraphQlDeadlinePropagator`를 빈으로 등록하고 세 지점에 연결한다 — `GraphQlBatchLoaderRegistrar`(autoconf=4)가 배치 타임아웃을, 리졸버 실행 경로가 `resolverBudget`을, 아웃바운드 포트 호출 지점이 `downstreamDeadline`을 쓰게 한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
요청 취소 후 하류 호출이 계속되는 것을 실행으로 재현하지 않았다. 호출자 부재상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a16-f005
|
||||
title: 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a16-f005
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a16-f005.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a16-f005
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a16-f005.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a16-f005.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L511 이다.
|
||||
---
|
||||
|
||||
# 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다
|
||||
|
||||
파서 계층은 이 방어의 첫 번째 관문이다. 리프가 한계를 값으로 갖고 설치 함수도 갖는데 호출하는 코드가 없다. 파서 옵션이 정적 전역이라 시작 시 한 번 설치하지 않으면 라이브러리 기본값이 유지된다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **배열 원소 상한이 선언만 되고 강제되지 않으며 백스톱도 없다**
|
||||
같은 형태의 선언과 강제 어긋남이다.
|
||||
- **5계층 예산 모델에서 요청 계층만 강제되고 나머지 파생이 전부 미배선이다**
|
||||
같은 리프의 다른 미배선 사례다.
|
||||
- **설정은 받아들여지고 검증되며 효과가 없다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
파서 계층은 이 프로토콜의 서비스 거부 방어에서 첫 번째 관문이다.
|
||||
|
||||
복잡도 계산도 구조 분석도 문서를 파싱한 뒤에 일어나므로, 파싱 자체를 폭발시키는 문서는 그 앞에서 막아야 한다.
|
||||
|
||||
이 리프가 그 한계를 실제로 거는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
걸지 않는다.
|
||||
|
||||
한계를 값으로 갖는 타입이 있고 클라이언트 정책에서 파생된다.
|
||||
|
||||
설치 함수도 있다. 연산 기본값 설치라는 이름이다.
|
||||
|
||||
호출하는 코드가 없다.
|
||||
|
||||
이름이 가리키듯 이 라이브러리의 파서 옵션은 정적 전역이다. 시작 시 한 번 설치하지 않으면 라이브러리 기본값이 유지된다.
|
||||
|
||||
실패 시나리오는 이렇다.
|
||||
|
||||
운영자가 설정으로 파서 한계를 조인다.
|
||||
|
||||
그 값은 플랫폼 설정을 거쳐 클라이언트 정책까지 도달한다. 그러나 한계 타입을 만드는 팩토리를 부르는 코드가 없어 파서에 닿지 않는다.
|
||||
|
||||
실제로 적용되는 것은 라이브러리의 기본값이다.
|
||||
|
||||
설정은 받아들여지고 검증되며 효과가 없다.
|
||||
|
||||
노출의 크기는 라이브러리 기본값이 정한다.
|
||||
|
||||
이 판본은 토큰 수와 공백 토큰 수와 규칙 깊이에 자체 기본 상한을 두므로 무제한은 아니다.
|
||||
|
||||
그리고 구조 한계와 복잡도 계산은 배선되어 있어 파싱 이후 계층은 작동한다.
|
||||
|
||||
그래서 P1 이 아니라 P2 다. 침묵하는 설정 표면이자 방어 계층 하나의 부재다.
|
||||
|
||||
권고는 플랫폼 자동 설정에 시작 시 한 번 설치를 부르는 초기화 지점을 두는 것이다.
|
||||
|
||||
정적 전역이므로 빈 메서드보다 초기화 콜백이 적절하다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 설치 함수 호출자 검색과 파서 옵션 전역성 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/182 계열에 있다.
|
||||
|
||||
1. 파서 한계 타입과 설치 함수를 확인한다.
|
||||
2. 설치 함수의 호출자를 센다.
|
||||
3. 라이브러리의 파서 옵션이 정적 전역인지 확인한다.
|
||||
4. 설정 값이 어디까지 도달하는지 추적한다.
|
||||
5. 구조 한계와 복잡도 계산이 배선되는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
파서 계층은 GraphQL DoS 방어의 **첫 번째** 관문이다 — 복잡도 계산도 구조 분석도 문서를 파싱한 뒤에 일어나므로, 파싱 자체를 폭발시키는 문서는 그 앞에서 막아야 한다.
|
||||
|
||||
## 파서 계층이 첫 관문인 이유
|
||||
|
||||
:::evidence key="analysis-finding-a16-f005" alt="분석 문서 analysis/16-adapter-inbound-graphql.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/16-adapter-inbound-graphql.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 값도 있고 설치 함수도 있고 호출하는 코드가 없다
|
||||
|
||||
이 leaf는 그 한계를 값으로 갖고(`GraphQlParserLimits`, 클라이언트 정책에서 파생), 설치 함수를 갖는다(`GraphQlParserOptionsFactory.installOperationDefaults`). `installOperationDefaults`라는 이름이 가리키듯 graphql-java의 파서 옵션은 정적 전역(`ParserOptions.setDefaultOperationParserOptions`)이고, 시작 시 한 번 설치하지 않으면 라이브러리 기본값이 유지된다.
|
||||
|
||||
## 실패 시나리오
|
||||
|
||||
운영자가 `backend.graphql.limits.*`로 파서 한계를 조인다. 그 값은 `GraphQlPlatformSettings` → `GraphQlClientPolicy`까지 도달하지만 `GraphQlParserLimits.from(...)`을 부르는 코드가 없어 파서에 닿지 않는다. 실제로 적용되는 것은 graphql-java 25.0의 기본값이다. 설정은 받아들여지고 검증되며 효과가 없다.
|
||||
|
||||
## 노출의 크기는 라이브러리 기본값이 정한다
|
||||
|
||||
graphql-java 25.0은 토큰 수·공백 토큰 수·규칙 깊이에 자체 기본 상한을 두므로 무제한은 아니다. 그리고 구조 한계(`GraphQlDocumentShapeAnalyzer`, autoconf=4)와 복잡도 계산(autoconf=4)은 배선돼 있어 파싱 이후 계층은 작동한다. 그래서 P1이 아니라 P2다 — 침묵하는 설정 표면이자 방어 계층 하나의 부재다.
|
||||
|
||||
## 권고
|
||||
|
||||
`GraphQlPlatformAutoConfiguration`에 시작 시 `GraphQlParserOptionsFactory.installOperationDefaults(GraphQlParserLimits.from(clientPolicy))`를 한 번 호출하는 초기화 지점을 둔다. 정적 전역이므로 `@Bean` 메서드보다 `InitializingBean`/`SmartInitializingSingleton`이 적절하다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
파서를 폭발시키는 문서를 보내 라이브러리 기본값이 적용되는지 재현하지 않았다. 호출자 부재상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a16-f006
|
||||
title: 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a16-f006
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a16-f006.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a16-f006
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a16-f006.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a16-f006.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L525 이다.
|
||||
---
|
||||
|
||||
# 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다
|
||||
|
||||
클라이언트 프로파일은 검증된 주체에서 정확히 해석되고 요청 문맥에 실린다. 그 값이 선택하는 것은 캐시 키와 지표 태그뿐이다. 정책은 프로파일과 무관하게 단일 빈이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **연산 이름 정책의 두 구현 중 하나만 배선되고 미배선 쪽만 정책 객체를 쓴다**
|
||||
같은 리프의 정책 이원화 사례다.
|
||||
- **설정으로 정한 파서 한계가 라이브러리에 설치되지 않는다**
|
||||
같은 리프의 다른 미배선 사례다.
|
||||
- **값이 정확히 해석되어도 그것을 쓰는 조회가 없으면 효과가 없다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프는 클라이언트 프로파일별로 다른 예산을 줄 수 있도록 설계되어 있다.
|
||||
|
||||
프로파일이 실제로 예산을 선택하는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
선택하지 않는다.
|
||||
|
||||
클라이언트 프로파일은 검증된 주체에서 정확히 해석되고 요청 문맥에 실린다.
|
||||
|
||||
그리고 그 값이 선택하는 것은 캐시 키와 지표 태그뿐이다.
|
||||
|
||||
정책은 프로파일과 무관하게 단일 빈이다.
|
||||
|
||||
설계가 이 구조를 명시적으로 거부한다.
|
||||
|
||||
성능 측정된 한계를 애플리케이션 코드가 아니라 환경 매니페스트에 둔다는 것이다.
|
||||
|
||||
지금은 코드 안의 기본 정책 하나다.
|
||||
|
||||
실패 시나리오는 이렇다.
|
||||
|
||||
배포가 내부 배치 클라이언트에는 큰 복잡도 예산을, 공개 이동 클라이언트에는 작은 예산을 주려 한다.
|
||||
|
||||
두 프로파일이 자격에서 정확히 구분되고, 두 요청 모두 같은 정책으로 평가된다.
|
||||
|
||||
프로파일을 나눈 목적이 달성되지 않으며, 그 사실은 어떤 오류로도 드러나지 않는다.
|
||||
|
||||
매니페스트가 약속한 성질도 성립할 기회가 없다.
|
||||
|
||||
알 수 없는 프로파일은 관대한 기본값으로 조용히 떨어지는 대신 시작이나 요청 실패가 된다는 것인데, 조회가 일어나지 않기 때문이다.
|
||||
|
||||
권고는 단일 정책 빈을 매니페스트 빈으로 바꾸고, 정책을 요구하는 여덟 지점이 요청 문맥의 프로파일로 조회하게 하는 것이다.
|
||||
|
||||
매니페스트는 중복 프로파일을 생성자에서 거부하므로 설정 오류가 부팅에서 드러난다.
|
||||
|
||||
판정은 P2 다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 프로파일 해석 경로와 정책 빈 구성 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/182 계열에 있다.
|
||||
|
||||
1. 클라이언트 프로파일이 어디서 해석되는지 확인한다.
|
||||
2. 그 값이 요청 문맥에 실리는지 확인한다.
|
||||
3. 그 값을 읽는 지점을 모두 센다.
|
||||
4. 정책 빈이 프로파일별인지 단일인지 확인한다.
|
||||
5. 매니페스트 타입의 생성자와 조회 메서드를 읽는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
클라이언트 프로파일은 검증된 principal에서 정확히 해석되고 요청 컨텍스트에 실린다(§15.2). 그리고 그 값이 선택하는 것은 **캐시 키와 지표 태그뿐**이다 — 정책은 프로파일과 무관하게 단일 빈이다.
|
||||
|
||||
## 프로파일이 실제로 고르는 것
|
||||
|
||||
:::evidence key="analysis-finding-a16-f006" alt="분석 문서 analysis/16-adapter-inbound-graphql.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/16-adapter-inbound-graphql.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 설계가 이 구조를 명시적으로 거부한다
|
||||
|
||||
"The design keeps benchmarked limits in an environment manifest rather than in application code." 지금은 코드 안의 `GraphQlClientPolicy.defaults(properties)` 하나다.
|
||||
|
||||
## 실패 시나리오
|
||||
|
||||
배포가 내부 배치 클라이언트에는 큰 복잡도 예산을, 공개 모바일 클라이언트에는 작은 예산을 주려 한다. 두 프로파일이 자격에서 정확히 구분되고, 두 요청 모두 같은 `GraphQlClientPolicy`로 평가된다. 프로파일을 나눈 목적이 달성되지 않으며, 그 사실은 어떤 오류로도 드러나지 않는다 — `GraphQlClientPolicyManifest`가 약속한 "An unknown profile is a startup or request failure rather than a silent fallback to a permissive default"는 조회가 일어나지 않으므로 성립할 기회가 없다. P2.
|
||||
|
||||
## 권고
|
||||
|
||||
`GraphQlClientPolicy` 단일 빈을 `GraphQlClientPolicyManifest` 빈으로 바꾸고, 정책을 요구하는 여덟 지점이 요청 컨텍스트의 프로파일로 조회하게 한다. 매니페스트는 중복 프로파일을 생성자에서 거부하므로 설정 오류가 부팅에서 드러난다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 프로파일로 요청을 보내 같은 예산이 적용되는 것을 재현하지 않았다. 단일 빈 구성상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a16-f008
|
||||
title: 파싱·검증 실패에 플랫폼 매퍼가 없다
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a16-f008
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: analysis-finding-a16-f008
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a16-f008.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a16-f008.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L680 이다.
|
||||
---
|
||||
|
||||
# 파싱·검증 실패에 플랫폼 매퍼가 없다
|
||||
|
||||
요청 오류 매퍼가 그 목적으로 존재하고 미배선이다. 배선된 두 매퍼는 각각 해석기 예외와 플랫폼 거부를 덮는다. 구문 오류 응답에는 이 플랫폼의 안정 확장이 붙지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **설정으로 정한 파서 한계가 라이브러리에 설치되지 않는다**
|
||||
같은 리프의 다른 파서 계층 사례다.
|
||||
- **5계층 예산 모델에서 요청 계층만 강제되고 나머지 파생이 전부 미배선이다**
|
||||
같은 리프의 다른 미배선 사례다.
|
||||
- **오류 코드로 분기하는 클라이언트는 형식이 갈리는 지점에서 빗나간다**
|
||||
이 사례가 그 규칙의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프는 오류 응답에 안정 코드와 분류 확장을 붙인다.
|
||||
|
||||
모든 실패 종류가 그것을 받는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
두 종류만 받는다.
|
||||
|
||||
배선된 매퍼가 둘이다. 하나는 해석기 예외를 덮고 하나는 플랫폼 거부를 덮는다.
|
||||
|
||||
파싱과 검증 실패를 덮을 매퍼가 그 목적으로 존재하는데 미배선이다.
|
||||
|
||||
결과적으로 구문 오류나 검증 실패의 응답에는 이 플랫폼의 안정 코드와 분류 확장이 붙지 않고 라이브러리의 기본 형식이 나간다.
|
||||
|
||||
클라이언트가 오류 코드로 분기한다면 그 분기가 파싱 오류에서만 빗나간다.
|
||||
|
||||
판정은 P3 다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 매퍼별 배선 여부와 담당 범위 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/182 계열에 있다.
|
||||
|
||||
1. 오류 매퍼 타입을 모두 나열한다.
|
||||
2. 각각이 어떤 실패를 덮는지 확인한다.
|
||||
3. 각각이 배선되는지 확인한다.
|
||||
4. 파싱 실패 응답의 형식을 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GraphQlRequestErrorMapper`(57)가 그 목적으로 존재하고 미배선이다(§19.3).
|
||||
|
||||
## GraphQlRequestErrorMapper 참조 위치
|
||||
|
||||
:::evidence key="analysis-finding-a16-f008" alt="코드베이스에서 GraphQlRequestErrorMapper 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlRequestErrorMapper 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 배선된 두 매퍼가 덮는 범위
|
||||
|
||||
`GraphQlExceptionResolver`는 리졸버 예외를, `GraphQlWireErrorMapper`는 플랫폼 거부를 덮는다.
|
||||
|
||||
## 그래서 파싱 오류만 형식이 다르다
|
||||
|
||||
구문 오류나 검증 실패의 응답에는 이 플랫폼의 안정 `code`/`category` 확장이 붙지 않고 graphql-java의 기본 형식이 나간다. 클라이언트가 오류 코드로 분기한다면 그 분기가 파싱 오류에서만 빗나간다. P3.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
구문 오류 요청을 보내 응답 형식을 확인하지 않았다. 배선 부재상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a16-f011
|
||||
title: 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a16-f011
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-analysis-finding-a16-f011.body.md
|
||||
assets:
|
||||
- key: analysis-finding-a16-f011
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a16-f011.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a16-f011.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L889 이다.
|
||||
---
|
||||
|
||||
# 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다
|
||||
|
||||
안정 능력 매니페스트가 커서 서명을 지원 목록에 넣는다. 그 목록의 용도는 안정 스타터에서 켜도 되는가를 판정하는 것이다. 그 능력은 켤 수 있는 것으로 판정되고, 켜는 코드는 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **기본 비활성은 존재하지 않는 스위치의 기본값을 서술한다**
|
||||
같은 리프의 같은 계열 사례다.
|
||||
- **프로파일별 정책 매니페스트가 미배선이라 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다**
|
||||
같은 리프의 다른 매니페스트 사례다.
|
||||
- **두 목록이 같은 사실을 말하게 하려면 한 test로 묶는다**
|
||||
권고의 형태다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프에 능력 등급을 말하는 원천이 둘 있다.
|
||||
|
||||
기계가 읽는 매니페스트와 사람이 읽는 등급표다.
|
||||
|
||||
두 원천이 같은 답을 내는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
한 항목에서 갈린다.
|
||||
|
||||
안정 능력 매니페스트가 커서 서명을 지원 목록에 넣는다.
|
||||
|
||||
그 목록의 용도가 자바독에 적혀 있다. 어떤 능력이 안정 스타터에서 활성화되어도 되는지 확인하며, 고급이거나 실험적이거나 미지원이면 예외를 던진다는 것이다.
|
||||
|
||||
즉 이 매니페스트는 안정 스타터에서 켜도 되는가를 판정한다.
|
||||
|
||||
커서 서명은 켤 수 있는 것으로 판정되고, 켜는 코드는 없다.
|
||||
|
||||
실패 시나리오는 이렇다.
|
||||
|
||||
채택자가 릴리스 게이트를 돌려 그 능력이 안정 등급에서 승인되는 것을 확인한다.
|
||||
|
||||
그것을 근거로 커서 쪽매김을 안정 계약의 일부로 문서화한다.
|
||||
|
||||
같은 리프의 다른 사례가 든 세 긍정 신호에 네 번째가 더해진다. 부팅 거부와 설정 수용과 상태 보고에 이어서다.
|
||||
|
||||
권고는 둘 중 하나다.
|
||||
|
||||
매니페스트에 모형 집합을 추가하거나, 그 능력을 실험 등급으로 옮기는 것이다.
|
||||
|
||||
등급표와 매니페스트가 같은 사실을 말하도록 두 목록을 한 테스트로 묶는 것이 더 낫다. 이 리프는 이미 개수 고정 형태를 두 번 쓰고 있다.
|
||||
|
||||
판정은 P3 다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 매니페스트 목록과 등급표 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/182 계열에 있다.
|
||||
|
||||
1. 안정 능력 매니페스트의 지원 목록을 확인한다.
|
||||
2. 그 목록을 소비하는 메서드의 자바독을 읽는다.
|
||||
3. 등급표에서 같은 능력의 등급을 확인한다.
|
||||
4. 그 능력을 켜는 코드가 있는지 검색한다.
|
||||
5. 이 리프의 다른 개수 고정 테스트 형태를 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GraphQlStableCapabilityManifest.STABLE`이 `SIGNED_CURSOR_CONNECTION`을 지원 목록에 넣는다. 그 목록의 용도는 `requireStable(...)`이고, javadoc은 이렇게 말한다.
|
||||
|
||||
> "Verifies a capability may be activated on the Stable starter. @throws GraphQlReleaseException when it is Advanced, Experimental or unsupported"
|
||||
|
||||
## 매니페스트가 지원 목록에 넣은 항목
|
||||
|
||||
:::evidence key="analysis-finding-a16-f011" alt="분석 문서 analysis/16-adapter-inbound-graphql.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/16-adapter-inbound-graphql.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 켤 수 있는 것으로 판정되고 켜는 코드가 없다
|
||||
|
||||
즉 이 매니페스트는 "Stable 스타터에서 켜도 되는가"를 판정한다.
|
||||
|
||||
## 실패 시나리오
|
||||
|
||||
adopter가 릴리스 게이트를 돌려 `SIGNED_CURSOR_CONNECTION`이 Stable에서 승인되는 것을 확인하고, 그것을 근거로 커서 페이지네이션을 Stable 계약의 일부로 문서화한다. §24.1의 세 긍정 신호(부팅 거부 · 설정 수용 · 액추에이터 보고)에 네 번째가 더해진다. P3.
|
||||
|
||||
## 권고
|
||||
|
||||
매니페스트에 `MODELLED` 집합을 추가하거나, `SIGNED_CURSOR_CONNECTION`을 `EXPERIMENTAL`로 옮긴다. 등급표와 매니페스트가 같은 사실을 말하도록 두 목록을 한 테스트로 묶는 것이 더 낫다 — 이 leaf는 이미 `WebArchitectureRulesTest` 형태의 개수 고정을 두 번 쓰고 있다(§3.4).
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
릴리스 게이트를 돌려 승인이 나오는 것을 실행하지 않았다. 목록 구성상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: analysis-finding-a16-f012
|
||||
title: '"기본 비활성"은 존재하지 않는 스위치의 기본값을 서술한다'
|
||||
topic: graphql-surface
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:analysis-finding-a16-f012
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: analysis-finding-a16-f012
|
||||
file: ../../../final/evidence/rendered/analysis-finding-a16-f012.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/analysis-finding-a16-f012.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L1002 이다.
|
||||
---
|
||||
|
||||
# "기본 비활성"은 존재하지 않는 스위치의 기본값을 서술한다
|
||||
|
||||
고급 능력을 켤 설정 표면이 없다. 등급표는 이 상태를 정확히 말한다. 어긋나는 것은 지침 문서 산문의 활성화 서술뿐이고, 그 두 문장은 활성화 경로의 존재를 전제한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 같은 능력에 대해 다르게 답한다**
|
||||
같은 리프의 같은 계열 사례다.
|
||||
- **선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다**
|
||||
다른 리프의 같은 형태이고 심각도가 다르다.
|
||||
- **등급표가 정확하면 산문만 고치면 된다**
|
||||
판정이 낮은 이유다.
|
||||
|
||||
## 문제
|
||||
|
||||
지침 문서가 고급 능력의 활성화를 서술한다.
|
||||
|
||||
기본이 비활성이며 명시적 승인 없이는 운영 활성화를 거부한다는 것이다.
|
||||
|
||||
그 서술이 성립하는지 확인했다.
|
||||
|
||||
## 결론
|
||||
|
||||
성립하지 않는다.
|
||||
|
||||
고급 능력을 켤 설정 표면이 없다.
|
||||
|
||||
깃발 레코드는 코드에서만 만들어지고, 그 활성화 메서드를 부르는 것은 테스트다.
|
||||
|
||||
그것을 결속하거나 소비하는 자동 설정이 없다.
|
||||
|
||||
등급표는 이 상태를 정확히 말한다.
|
||||
|
||||
고급 항목이 전부 모형 등급이다. 요청 경로에는 없다는 뜻이다. 그러므로 능력이 동작한다고 주장하지 않는다.
|
||||
|
||||
어긋나는 것은 지침 문서 산문의 활성화 서술뿐이다.
|
||||
|
||||
기본 비활성이라는 문장과 명시적 승인 없이는 거부한다는 문장이 둘 다 활성화 경로의 존재를 전제한다.
|
||||
|
||||
다른 리프의 같은 형태와 비교하면 심각도가 다르다.
|
||||
|
||||
거기서는 열한 능력이 선언되고 둘만 켤 수 있으면서 그 사실이 어디에도 없었다.
|
||||
|
||||
여기서는 켤 수 없다는 사실이 등급표에 모형으로 적혀 있고, 산문 한 문단만 그보다 앞서 나간다.
|
||||
|
||||
권고는 그 문단을 등급에 맞추는 것이다.
|
||||
|
||||
고급 능력은 현재 모형 등급이며 활성화 경로가 없고, 관련 깃발과 가드는 그 경로가 생길 때 쓸 판정 모델이라고 적으면 된다.
|
||||
|
||||
판정은 P3 다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
확인 방식 : 활성화 경로 검색과 등급표, 산문 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/182 계열에 있다.
|
||||
|
||||
1. 지침 문서의 활성화 서술 두 문장을 읽는다.
|
||||
2. 고급 깃발 레코드를 만드는 코드를 검색한다.
|
||||
3. 그것을 결속하거나 소비하는 자동 설정을 검색한다.
|
||||
4. 등급표에서 고급 항목의 등급을 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
Advanced 능력을 켤 설정 표면이 없다 — 플래그 record는 코드에서만 만들어지고(`enabling(...)`은 테스트가 부른다), 그것을 바인딩하거나 소비하는 자동설정이 없다(§38.2).
|
||||
|
||||
## GraphQlAdvancedFeatureFlags 참조 위치
|
||||
|
||||
:::evidence key="analysis-finding-a16-f012" alt="코드베이스에서 GraphQlAdvancedFeatureFlags 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlAdvancedFeatureFlags 코드베이스 검색 — 13줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 등급표는 이 상태를 정확히 말한다
|
||||
|
||||
Advanced 항목이 전부 `modelled`("요청 경로에는 없다")이므로 능력이 동작한다고 주장하지 않는다. 어긋나는 것은 CLAUDE.md 산문의 활성화 서술뿐이다 — "기본 비활성"과 "명시적 승인 없이는 production 활성화를 거부한다"는 둘 다 활성화 경로의 존재를 전제한다.
|
||||
|
||||
## inbound-web §36.1과 같은 형태이되 심각도가 다르다
|
||||
|
||||
거기서는 11개 능력이 선언되고 2개만 켤 수 있으면서 그 사실이 어디에도 없었다. 여기서는 켤 수 없다는 사실이 등급표에 `modelled`로 적혀 있고, 산문 한 문단만 그보다 앞서 나간다. P3.
|
||||
|
||||
## 권고
|
||||
|
||||
그 문단을 등급에 맞춘다 — "Advanced capability 는 현재 `modelled` 등급이며 활성화 경로가 없다. `GraphQlAdvancedFeatureFlags`·`GraphQlAdvancedModuleGuard`는 그 경로가 생길 때 쓸 판정 모델이다."
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
설정을 넣고 띄워 아무 일도 일어나지 않는 것을 재현하지 않았다. 소비자 부재상 그 결과가 나온다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user