Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/messaging-and-outbox/case/case-a12-f001-check-verifyjsonschemaruntimegraph.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- 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>
2026-09-07 12:39:20 +09:00

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:
- 원본 분석 절은 final/document.md#a12#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 -->