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>
254 lines
14 KiB
Markdown
254 lines
14 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a12-f001-check-verifyjsonschemaruntimegraph
|
|
title: 매 PR 을 막는 게이트가 초록일 수 없다
|
|
topic: messaging-and-outbox
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a12-f001-check-verifyjsonschemaruntimegraph
|
|
evidenceCapturedOn: 2026-09-02
|
|
body: case-a12-f001-check-verifyjsonschemaruntimegraph.body.md
|
|
assets:
|
|
- key: a12-f001-check-verifyjsonschemaruntimegraph
|
|
file: ../../../final/evidence/rendered/a12-f001-check-verifyjsonschemaruntimegraph.svg
|
|
- key: a12-f001-check-verifyjsonschemaruntimegraph-run
|
|
file: ../../../final/evidence/rendered/a12-f001-check-verifyjsonschemaruntimegraph-run.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a12-f001-check-verifyjsonschemaruntimegraph.txt
|
|
- ../../../final/evidence/raw/a12-f001-check-verifyjsonschemaruntimegraph-run.txt
|
|
source:
|
|
- 원본 분석 절은 analysis/12-adapter-outbound-messaging.md#L82 이다. 등급은 P2 이다. 이 리프의 중심 규율을 강제하는 태스크가 검사 단계에 붙어 있다는 지형, 실행하면 실패한다는 관측, 필수 좌표에 정확한 패치 판본이 하드코딩되어 있고 잠긴 좌표는 다르다는 대조, BOM 이 올라가면서 두 좌표가 어긋났다는 원인 지목, 금지 조건 쪽은 여전히 옳다는 확인, 그리고 수정 방향 둘이 그 절에 있다.
|
|
- 이 기록이 더한 것은 넷이다. 하드코딩은 처음부터 틀린 것이 아니었고, 목록을 쓴 커밋 시점에는 잠금 파일과 일치했다. 883 파일을 건드린 커밋이 잠금 파일만 올리고 이 빌드 파일을 지나갔다. 이 태스크는 상위 자격 태스크 둘의 선행이기도 하지만 그 둘은 어떤 워크플로도 부르지 않고, 매 PR 마다 실제로 도는 것은 품질 게이트 잡의 검사 명령이며 그 잡은 릴리스 게이트의 필수 선행이다. 그리고 잠금 파일이 움직이기 여드레 전 같은 검사가 성공한 기록이 저장소에 남아 있다.
|
|
- 그래서 등급을 P1 로 올린다. 원본이 판단한 시점의 관찰은 게이트가 통과할 수 없다는 것까지였고, 그 게이트가 차단하는 범위와 그 상태가 이어진 기간은 이 기록에서 확인했다.
|
|
---
|
|
|
|
# 매 PR 을 막는 게이트가 초록일 수 없다
|
|
|
|
이 리프의 중심 규율을 강제하는 태스크가 검사 단계에 붙어 있고, CI 의 품질 게이트 잡이 매 PR 마다 그 검사를 부른다. 실행하면 종료 코드 1 이다. 필수 좌표에 패치 판본이 하드코딩되어 있는데, 그 판본은 태스크를 쓸 당시에는 맞았고 그 뒤 잠금 파일만 올라갔다.
|
|
|
|
## 관계
|
|
|
|
- **release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다**
|
|
릴리스 게이트가 실제로 차단하는 대상을 센 문서다.
|
|
- **durable-operation 게이트는 켤 수 없고 켜면 부팅이 실패한다**
|
|
두 게이트 모두 조건이 성립할 수 없어 통과 상태가 될 수 없다.
|
|
- **없다고 적은 라이브러리로 옆 파일이 봉투를 만든다**
|
|
같은 리프에서 같은 커밋이 만든 사례다.
|
|
|
|
## 문제
|
|
|
|
이 리프의 중심 규율은 JSON 검증 런타임을 닫는 것이다.
|
|
|
|
이 규율을 지키는 태스크는 검사 단계에만 붙어 있는 것이 아니라 CI 가 매 PR 마다 부르는 검사 안에 들어 있다.
|
|
|
|
그래서 실행했다.
|
|
|
|
## 결론
|
|
|
|
실패한다. 필수로 잠긴 모듈 하나가 없다는 이유로 종료 코드 1 을 낸다.
|
|
|
|
태스크가 요구하는 좌표는 셋이고 전부 판본이 3.0.2 다. 잠금 파일이 고정한 것은 검증기 라이브러리만 3.0.2 이고 직렬화 계열 셋은 3.1.5 다.
|
|
|
|
하드코딩이 처음부터 틀렸던 것은 아니다. 이 목록을 쓴 커밋 시점의 잠금 파일에는 검증기와 직렬화 계열이 모두 3.0.2 로 잡혀 있었다.
|
|
|
|
그 뒤 883 파일을 건드린 커밋이 잠금 파일의 직렬화 계열 세 좌표를 3.1.5 로 올렸다. 태스크가 이름으로 요구하는 둘이 그 안에 있다. 그 커밋은 같은 판본을 적어 둔 빌드 파일을 건드리지 않았다.
|
|
|
|
닿는 범위가 검사 단계 하나가 아니다. 상위 자격 태스크 둘도 이 태스크를 선행으로 걸지만, 그 둘을 부르는 워크플로가 없다. 실제로 매 PR 과 main 푸시마다 이 태스크를 돌리는 것은 품질 게이트 잡의 검사 명령이고, 그 잡은 릴리스 게이트의 필수 선행이다.
|
|
|
|
여드레 전에는 초록이었다. 저장소에 남은 실행 기록에 같은 검사 명령이 이 태스크를 돌고 성공으로 끝난 로그가 있다.
|
|
|
|
금지 조건 쪽은 걸리는 것이 0 이다. 다만 그 0 은 이 검사가 잡아낸 결과라기보다 설정 블록이 형식 계열 세 모듈을 미리 빼 둔 결과다. 그 위에 구세대 이름공간이 통째로 빠진 것도 아니다. 애너테이션 아티팩트가 남아 있고, 태스크는 그것을 금지 목록에서 의도적으로 뺀다.
|
|
|
|
판정을 P2 에서 올린다.
|
|
|
|
원본은 게이트 전체가 통과할 수 없다는 관찰로 P2 를 매겼다. 그 게이트가 매 PR 을 막는 필수 잡 안에 있고, 그 상태가 특정 날짜부터 이어지고 있으며, 그 전에 초록이던 기록이 저장소에 남아 있다. 상시 빨간 차단 게이트는 P1 이다.
|
|
|
|
수정은 필수 좌표에서 판본을 떼고 그룹과 이름만 확인하거나, 잠금 파일에서 판본을 읽어 비교하는 것이다. 닫힘 조건은 무엇이 없는가이지 어느 패치인가가 아니다.
|
|
|
|
## 검증 환경
|
|
|
|
Gradle : 9.0.0
|
|
확인 방식 : 태스크 실행, 검사 단계 예행 실행, 잠금 파일과 커밋 이력 조회
|
|
소스 수정 : x
|
|
비고 : 해결이 STRICT 의존성 잠금에 고정되어 있어 오프라인 여부가 판본을 바꾸지 않는다
|
|
|
|
## 재현 조건
|
|
|
|
1. 검증 태스크를 단독으로 실행하고 종료 코드와 메시지를 본다.
|
|
2. 검사 단계를 예행 실행해 그 태스크에 닿는지 확인한다.
|
|
3. 태스크가 요구하는 좌표 셋을 빌드 파일에서 읽는다.
|
|
4. 잠금 파일이 고정한 같은 그룹의 좌표를 뽑는다.
|
|
5. 이 목록을 쓴 커밋을 찾아 그 시점의 잠금 파일 값을 확인한다.
|
|
6. 잠금 파일의 판본을 올린 커밋을 찾아, 그 커밋의 잠금 파일 차이와 이 빌드 파일 포함 여부를 본다.
|
|
7. 이 태스크 이름이 나오는 곳을 확장자 제한 없이 세고, 검사 단계를 부르는 워크플로와 상위 자격 태스크를 부르는 워크플로를 각각 센다.
|
|
8. 그 전에 이 태스크가 초록이던 실행 기록이 저장소에 있는지 찾는다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
이 리프의 규율을 지키는 태스크 하나가 검사 단계에 붙어 있고, CI 의 품질 게이트 잡은 그 검사를 매 PR 마다 부른다.
|
|
|
|
## 태스크가 하는 일
|
|
|
|
:::evidence key="a12-f001-check-verifyjsonschemaruntimegraph" alt="검증 태스크의 정의 전체와 그 앞의 설정 제외 블록을 줄 번호와 함께, 이 태스크 이름이 저장소에서 나오는 곳 전부, CI 가 검사 단계를 부르는 줄과 릴리스 게이트의 필수 선행 목록, 상위 자격 태스크를 부르는 워크플로 수, 그리고 금지 목록에서 의도적으로 빠진 구세대 이름공간 아티팩트를 출력한 터미널 기록." caption="설정 블록이 형식 계열 세 모듈을 미리 빼고, 태스크는 그 뒤 필수 좌표 셋을 판본까지 확인한다 · CI 는 매 PR 과 main 푸시마다 검사 단계를 부르고 그 잡은 릴리스 게이트의 필수 선행이다 · 상위 자격 태스크를 부르는 워크플로는 0 · 구세대 이름공간의 애너테이션 아티팩트는 그래프에 남아 있다 — 71줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
```groovy
|
|
50: [
|
|
51: 'com.networknt:json-schema-validator:3.0.2',
|
|
52: 'tools.jackson.core:jackson-core:3.0.2',
|
|
53: 'tools.jackson.core:jackson-databind:3.0.2'
|
|
54: ].each { String required ->
|
|
55: if (!modules.contains(required)) {
|
|
56: throw new GradleException(
|
|
57: "Messaging JSON runtime is missing required locked module ${required}")
|
|
58: }
|
|
59: }
|
|
60: // Jackson 3 intentionally retains the 2.x-namespace annotations artifact. It is not a
|
|
61: // Jackson 2 databind/runtime engine and is part of the official Jackson 3 BOM graph.
|
|
62: }
|
|
63:}
|
|
64:
|
|
65:tasks.named('check') {
|
|
66: dependsOn tasks.named('verifyJsonSchemaRuntimeGraph')
|
|
67:}
|
|
```
|
|
|
|
## 돌려 보면
|
|
|
|
:::evidence key="a12-f001-check-verifyjsonschemaruntimegraph-run" alt="검증 태스크를 그대로 실행한 결과와 종료 코드, 검사 단계 예행 실행이 그 태스크에 닿는지, 태스크가 요구하는 좌표와 잠금 파일이 고정한 좌표, 이 목록을 쓴 커밋 시점의 잠금 값, 잠금 파일의 판본을 올린 커밋과 그 커밋의 잠금 파일 차이 및 이 빌드 파일 포함 여부, 그리고 그 전에 이 태스크가 초록이던 실행 기록을 출력한 터미널 기록." caption="태스크는 필수 모듈 하나가 없다며 종료 코드 1 로 끝나고, 검사 단계 예행 실행이 그 태스크에 닿는다 · 요구 판본은 셋 다 3.0.2 인데 잠금 파일의 직렬화 계열 셋은 3.1.5 · 883 파일 커밋이 그 셋을 올리면서 빌드 파일은 건드리지 않았다 · 여드레 전 실행 기록에는 같은 검사가 성공으로 끝나 있다 — 49줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
```text
|
|
[check 에 붙은 태스크를 그대로 실행]
|
|
Execution failed for task ':adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph'.
|
|
> Messaging JSON runtime is missing required locked module tools.jackson.core:jackson-core:3.0.2
|
|
EXIT=1
|
|
```
|
|
|
|
요구 판본 셋 중 둘이 어긋난다.
|
|
|
|
```text
|
|
[태스크가 요구하는 좌표]
|
|
com.networknt:json-schema-validator:3.0.2
|
|
tools.jackson.core:jackson-core:3.0.2
|
|
tools.jackson.core:jackson-databind:3.0.2
|
|
|
|
[잠금 파일이 고정한 좌표]
|
|
com.networknt:json-schema-validator:3.0.2
|
|
tools.jackson.core:jackson-core:3.1.5
|
|
tools.jackson.core:jackson-databind:3.1.5
|
|
tools.jackson:jackson-bom:3.1.5
|
|
```
|
|
|
|
검증기 라이브러리만 맞는다. 직렬화 계열은 BOM 을 포함해 셋이 3.1.5 로 올라가 있다.
|
|
|
|
## 처음부터 틀렸던 것은 아니다
|
|
|
|
```text
|
|
[태스크를 쓴 시점의 잠금 파일]
|
|
e5af2912 2026-07-31 feat: add messaging R2 polling producer
|
|
com.networknt:json-schema-validator:3.0.2
|
|
tools.jackson.core:jackson-core:3.0.2
|
|
tools.jackson.core:jackson-databind:3.0.2
|
|
tools.jackson:jackson-bom:3.0.2
|
|
```
|
|
|
|
이 목록을 쓴 커밋 시점에는 넷이 모두 3.0.2 였다.
|
|
|
|
```text
|
|
[잠금 파일을 움직인 커밋]
|
|
a24ece9c 2026-08-28 feat: web, websocket 어댑터 추가 구현
|
|
-com.fasterxml.jackson.core:jackson-annotations:2.20
|
|
+com.fasterxml.jackson.core:jackson-annotations:2.21
|
|
-tools.jackson.core:jackson-core:3.0.2
|
|
-tools.jackson.core:jackson-databind:3.0.2
|
|
-tools.jackson:jackson-bom:3.0.2
|
|
+tools.jackson.core:jackson-core:3.1.5
|
|
+tools.jackson.core:jackson-databind:3.1.5
|
|
+tools.jackson:jackson-bom:3.1.5
|
|
그 커밋이 건드린 파일 수 : 883
|
|
그중 messaging/build.gradle : 0 건
|
|
```
|
|
|
|
883 파일을 건드린 커밋이 BOM 을 3.1.5 로 올리면서 그것이 끌고 오는 두 좌표를 함께 올렸고, 같은 판본을 적어 둔 이 빌드 파일은 지나갔다.
|
|
|
|
## 어디까지 막는가
|
|
|
|
```text
|
|
src/build.gradle:850: dependsOn ':adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph'
|
|
src/build.gradle:892: dependsOn ':adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph'
|
|
src/adapter/outbound/messaging/README.md:139:Gradle dependency lock과 `verifyJsonSchemaRuntimeGraph`가 담당한다. 이 검증은 business schema
|
|
src/adapter/outbound/messaging/build.gradle:28:tasks.register('verifyJsonSchemaRuntimeGraph') {
|
|
src/adapter/outbound/messaging/build.gradle:66: dependsOn tasks.named('verifyJsonSchemaRuntimeGraph')
|
|
src/adapter/outbound/messaging/CLAUDE.md:55: `CodeSource` is a regular JAR. Strict dependency locks and `verifyJsonSchemaRuntimeGraph` own the
|
|
```
|
|
|
|
850 과 892 가 상위 자격 태스크 둘이다. 그런데 그 둘을 부르는 워크플로가 없다.
|
|
|
|
```text
|
|
verifyMessagingJsonSchemaV1 / verifyMessagingContracts 가 .github 아래 나오는 줄 : 0
|
|
```
|
|
|
|
실제로 도는 것은 66 쪽이다.
|
|
|
|
```text
|
|
50: run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace
|
|
release-gate:
|
|
needs:
|
|
- quality-gates
|
|
- sample-off
|
|
```
|
|
|
|
품질 게이트 잡이 매 PR 과 main 푸시마다 검사 단계를 부르고, 릴리스 게이트가 그 잡을 필수 선행으로 건다. 예행 실행이 그 경로를 보여 준다.
|
|
|
|
```text
|
|
[CI 가 부르는 check 가 이 태스크에 닿는가]
|
|
:adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph SKIPPED
|
|
:adapter:outbound:messaging:check SKIPPED
|
|
```
|
|
|
|
## 여드레 전에는 초록이었다
|
|
|
|
```text
|
|
[그 전에 초록이던 기록]
|
|
run-at: 2026-08-20T01:53:16Z
|
|
775:> Task :adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph
|
|
899:BUILD SUCCESSFUL in 5m 42s
|
|
901:exit=0
|
|
```
|
|
|
|
저장소에 커밋된 실행 기록이다. 잠금 파일이 움직이기 여드레 전, 같은 검사 명령이 이 태스크를 돌고 성공으로 끝났다.
|
|
|
|
## 금지 조건 쪽
|
|
|
|
걸리는 것은 0 이다. 다만 그 0 을 만드는 것의 상당 부분은 검사가 아니라 그 앞의 제외 블록이다.
|
|
|
|
```groovy
|
|
22:configurations.configureEach {
|
|
23: exclude group: 'tools.jackson.dataformat', module: 'jackson-dataformat-yaml'
|
|
24: exclude group: 'org.yaml', module: 'snakeyaml'
|
|
25: exclude group: 'org.snakeyaml', module: 'snakeyaml-engine'
|
|
26:}
|
|
```
|
|
|
|
금지 조건의 네 갈래 중 둘이 여기서 미리 제거된 모듈을 찾는다.
|
|
|
|
구세대 이름공간이 그래프에서 사라진 것도 아니다.
|
|
|
|
```text
|
|
8:com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
|
```
|
|
|
|
60~61 줄 주석이 이 아티팩트를 금지 목록에서 뺀 이유를 적는다. 닫힌 그래프 안에 구세대 이름공간 하나가 설계대로 남아 있다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
판본을 떼는 수정을 적용해 태스크가 초록이 되는지 확인하지 않았다. 문서 작업 범위에서 소스를 고치지 않는다.
|
|
|
|
두 커밋 사이에 같은 두 파일을 건드린 커밋이 둘 더 있다. 그 둘은 필수 목록과 직렬화 계열 잠금 줄을 손대지 않았다.
|
|
|
|
<!-- body:end -->
|