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>
224 lines
14 KiB
Markdown
224 lines
14 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a12-f002-jackson-databind
|
|
title: 근거를 없앤 커밋이 README 를 열고 그 줄만 두었다
|
|
topic: messaging-and-outbox
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a12-f002-jackson-databind
|
|
body: case-a12-f002-jackson-databind.body.md
|
|
evidenceCapturedOn: 2026-09-02
|
|
assets:
|
|
- key: a12-f002-jackson-databind
|
|
file: ../../../final/evidence/rendered/a12-f002-jackson-databind.svg
|
|
- key: a12-f002-jackson-databind-history
|
|
file: ../../../final/evidence/rendered/a12-f002-jackson-databind-history.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a12-f002-jackson-databind.txt
|
|
- ../../../final/evidence/raw/a12-f002-jackson-databind-history.txt
|
|
source:
|
|
- 원본 분석 절은 analysis/12-adapter-outbound-messaging.md#L118 이다. 등급은 P3 이다. README 문장 인용, 잠금 파일에서 그 좌표가 컴파일과 실행 양쪽에 있다는 대조, 같은 리프의 검증 태스크가 그 모듈을 필수로 요구한다는 지적, 구세대 이름공간 쪽은 실제로 금지되어 있으므로 서술이 그 이름공간을 뜻했다면 맞지만 문장이 한정하지 않는다는 판단, 코드 결함이 아니라 근거로 적힌 사실이 무너진 것이라는 결론이 그 절에 있다.
|
|
- 이 기록이 더한 것은 넷이다. 같은 주장이 클래스 자바독에도 있어 고칠 자리가 둘이라는 것. 잠금 파일이 아니라 이 리프의 빌드 선언이 그 좌표를 끌어온다는 것. 구성을 바꾼 커밋이 2026-07-31 이고, 그 커밋이 같은 자리에서 README 에 절 셋을 붙이면서 이 줄만 두고 갔으며 그 뒤로 이 파일을 연 커밋이 없다는 것. 그리고 그 라이브러리로 봉투를 만드는 옆 파일이 있지만 그것을 부르는 어댑터를 조립하는 곳은 없다는 것이다.
|
|
---
|
|
|
|
# 근거를 없앤 커밋이 README 를 열고 그 줄만 두었다
|
|
|
|
README 가 손수 짠 직렬화의 근거로 결속 라이브러리가 클래스패스에 없다는 사실을 들고, 그 클래스의 자바독이 같은 문장을 다시 적는다. 그 좌표를 배포되는 클래스패스로 올린 커밋이 같은 자리에서 README 에 절 셋을 붙이면서 이 줄만 두고 갔다.
|
|
|
|
## 관계
|
|
|
|
- **매 PR 을 막는 게이트가 초록일 수 없다**
|
|
같은 리프의 같은 잠금 좌표에서 갈라진 사례다. 저쪽은 판본이 올라간 커밋, 이쪽은 구성이 바뀐 커밋이다.
|
|
- **README가 "노출된 setting도 bean도 없다"고 적은 능력에 production bean 여덟이 있다**
|
|
두 사례 모두 README 가 적은 것과 실제 코드가 다르다.
|
|
- **문서가 지목하는 조정 레코드를 쓰는 코드가 없다**
|
|
문서가 가리킨 대상이 코드에 없다는 점이 같다.
|
|
|
|
## 문제
|
|
|
|
봉투 직렬화를 손으로 짠 이유가 README 에 적혀 있다.
|
|
|
|
이 모듈은 결속 라이브러리를 클래스패스에 두지 않아 뼈대를 가볍게 유지하며, 그래서 봉투 직렬화는 의존성 없는 손수 짠 JSON 이라는 것이다.
|
|
|
|
## 결론
|
|
|
|
손수 짠 클래스는 그 서술과 어긋나지 않는다. 바깥에서 끌어오는 타입이 없다.
|
|
|
|
다만 그 클래스의 자바독이 README 와 같은 문장을 다시 적는다. 모듈이 그 라이브러리를 클래스패스 밖에 둔다는 것이다. 고칠 자리가 하나가 아니라 둘이다.
|
|
|
|
잠금 파일만의 문제도 아니다. 이 리프의 빌드 파일이 스프링 JSON 스타터와 스키마 검증기를 직접 선언한다. 그 좌표가 컴파일과 실행 클래스패스에 올라 있는 것은 그 선언의 결과다.
|
|
|
|
같은 빌드 파일의 검증 태스크는 그 좌표가 런타임 그래프에 있어야 한다고 요구한다. 문서가 부재를 근거로 삼는 동안 게이트는 존재를 조건으로 건다.
|
|
|
|
문장이 쓰일 때는 맞았다. 초기 커밋 시점에는 그 좌표가 배포되는 클래스패스 어디에도 없었다.
|
|
|
|
바꾼 것은 2026-07-31 의 메시징 R2 커밋이다. 그 커밋이 빌드 파일에 JSON 스택 둘을 선언하면서 좌표를 컴파일과 실행으로 올렸다. 같은 커밋이 README 를 열어 절 셋을 새로 붙였고, 근거가 무너진 그 문장만 그대로 두었다. 그 뒤로 이 README 를 건드린 커밋은 없다.
|
|
|
|
실시간 팬아웃 봉투는 석 주 뒤에 들어왔다. 883 파일을 건드린 커밋이 이 리프에서 만진 것은 잠금 파일과 팬아웃 어댑터와 그 봉투 클래스 셋이고, 잠금 파일에서 바뀐 것은 판본뿐이다.
|
|
|
|
문장이 근거로 삼은 것은 모듈 전체의 부재이고, 그것이 깨진 뒤 옆 파일이 봉투 JSON 조립을 그 라이브러리로 처리한다.
|
|
|
|
다만 그 봉투를 부르는 어댑터를 조립하는 곳이 저장소 어디에도 없다. 선언은 그대로 있고 그것을 부르는 실행 경로만 없다.
|
|
|
|
구세대 이름공간의 결속과 코어를 세우는 것은 검증 태스크 안의 단언 한 줄이고, 구성 단위 제외 목록이 빼는 것은 형식 계열뿐이다. 애너테이션 쪽 2.21 은 아직 두 클래스패스에 다 남아 있다. 서술이 그 이름공간을 뜻했다면 맞지만, 문장에 이름공간이 없다.
|
|
|
|
판정은 P3 다. 코드 결함은 아니다.
|
|
|
|
README 를 읽고 이 리프의 의존성 정책을 판단하는 사람은 지금 없는 사실을 근거로 삼게 된다.
|
|
|
|
## 검증 환경
|
|
|
|
Gradle : 9.0.0
|
|
확인 방식 : README 와 자바독 대조, 빌드 선언과 잠금 파일 확인, 리프 main 의 참조 전수 확인, 커밋 이력 조회
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. README 의 직렬화 근거 문장과 손수 짠 클래스의 자바독을 나란히 읽는다.
|
|
2. 빌드 파일에서 그 라이브러리를 끌어오는 선언을 찾는다.
|
|
3. 같은 빌드 파일의 검증 태스크가 요구하는 좌표를 읽는다.
|
|
4. 리프 main 에서 그 라이브러리를 쓰는 곳을 임포트와 인라인 표기까지 전수로 찾는다.
|
|
5. 그중 봉투를 만드는 쪽의 본문을 읽고, 그 봉투와 어댑터를 자기 파일 밖에서 부르는 곳을 센다.
|
|
6. 잠금 파일에서 관련 좌표를 확인한다.
|
|
7. 그 좌표의 구성이 바뀐 이력을 커밋별로 뽑는다.
|
|
8. README 를 건드린 커밋 전부를 나열하고, 구성을 바꾼 커밋이 README 에 무엇을 했는지 그 앞뒤 36 행으로 확인한다.
|
|
9. 봉투 클래스가 들어온 커밋과 그 커밋이 이 리프에서 건드린 파일을 확인한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
봉투 직렬화를 손으로 짠 이유가 README 에 적혀 있고, 그 클래스의 자바독도 같은 말을 한다.
|
|
|
|
## 같은 주장을 하는 두 자리
|
|
|
|
:::evidence key="a12-f002-jackson-databind" alt="README 의 근거 문단과 손수 짠 클래스의 자바독이 같은 주장을 하는 것, 빌드 파일이 그 라이브러리를 끌어오는 선언과 같은 파일의 검증 태스크가 요구하는 좌표, 리프 main 에서 그 라이브러리를 쓰는 곳을 임포트와 인라인 표기까지 전수로 찾은 목록, 그중 봉투를 만드는 쪽의 본문, 그 봉투와 어댑터를 자기 파일 밖에서 부르는 곳의 수, 그리고 잠금 파일의 관련 좌표를 출력한 터미널 기록." caption="README 와 클래스 자바독이 같은 문장을 적는다 · 빌드 파일이 스프링 JSON 스타터와 스키마 검증기를 선언하고, 같은 파일의 검증 태스크는 그 좌표를 필수로 든다 · 리프 main 의 두 파일이 그 라이브러리를 쓰고 한쪽은 봉투를 만든다 · 그 봉투를 부르는 어댑터를 조립하는 곳은 0 — 44줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
```text
|
|
## OutboxEnvelopeJson — 손수 짠 JSON
|
|
|
|
이 모듈은 `jackson-databind` 를 classpath 에 두지 않아(스켈레톤을 가볍게 유지) outbox envelope
|
|
직렬화는 의존성 없는 손수 짠 JSON 이다.
|
|
```
|
|
|
|
```java
|
|
/**
|
|
* Hand-rolled, dependency-free JSON serialiser for the outbox envelope (no Jackson — the module
|
|
* deliberately keeps {@code jackson-databind} off its classpath).
|
|
```
|
|
|
|
손수 짠 코드 자체는 서술대로다. 임포트가 도메인 타입 하나뿐이다. 고쳐야 할 것은 그 코드가 아니라 두 자리에 적힌 근거다.
|
|
|
|
## 빌드 파일이 그것을 끌어온다
|
|
|
|
```text
|
|
8: implementation 'org.springframework.boot:spring-boot-starter-json'
|
|
9: implementation('com.networknt:json-schema-validator:3.0.2') {
|
|
```
|
|
|
|
잠금 파일이 스스로 그렇게 된 것이 아니다. 이 리프가 직접 선언한다. 그리고 같은 파일 아래쪽의 검증 태스크가 그 좌표를 요구한다.
|
|
|
|
```groovy
|
|
'com.networknt:json-schema-validator:3.0.2',
|
|
'tools.jackson.core:jackson-core:3.0.2',
|
|
'tools.jackson.core:jackson-databind:3.0.2'
|
|
```
|
|
|
|
README 가 없다고 적은 모듈을 이 리프의 게이트가 필수로 든다.
|
|
|
|
## 문장이 무너진 자리
|
|
|
|
:::evidence key="a12-f002-jackson-databind-history" alt="이 좌표의 구성이 바뀐 이력을 커밋별로 뽑은 목록, README 를 건드린 커밋 전부, 구성을 바꾼 커밋이 README 에 절 셋을 붙였다는 것과 그 커밋 앞뒤의 36 행, 그리고 봉투 클래스가 들어온 커밋과 그 커밋이 이 리프에서 건드린 파일을 출력한 터미널 기록." caption="초기 커밋에서는 시험 클래스패스에만 있었고, 2026-07-31 커밋이 컴파일과 실행으로 올렸다 · 그 커밋이 README 에 절 셋을 붙였는데 36 행은 앞뒤가 같다 · 그 뒤 README 를 건드린 커밋은 없다 · 봉투 클래스는 석 주 뒤 커밋이 들여왔고 그 커밋의 잠금 변경은 판본뿐 — 29줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
```text
|
|
# 이 좌표의 구성이 바뀐 이력
|
|
a24ece9c 2026-08-28 feat: web, websocket 어댑터 추가 구현
|
|
-tools.jackson.core:jackson-databind:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
|
+tools.jackson.core:jackson-databind:3.1.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
|
5f10b791 2026-08-11 chore: record pre-existing uncommitted repository state
|
|
e5af2912 2026-07-31 feat: add messaging R2 polling producer
|
|
-tools.jackson.core:jackson-databind:3.0.2=testCompileClasspath,testRuntimeClasspath
|
|
+tools.jackson.core:jackson-databind:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
|
821fe00c 2026-07-24 init: 클린 아키텍처 백엔드
|
|
+tools.jackson.core:jackson-databind:3.0.2=testCompileClasspath,testRuntimeClasspath
|
|
```
|
|
|
|
구성을 바꾼 것은 2026-07-31 커밋이다. 8월 커밋이 한 일은 판본을 올린 것뿐이다.
|
|
|
|
문장의 classpath 를 배포되는 쪽으로 읽으면 초기 커밋 때는 성립했다. 이름공간을 두고는 같은 호의를 주지 않는다는 것이 아래의 문제다.
|
|
|
|
## 그 커밋이 README 에 한 일
|
|
|
|
```text
|
|
# 구성을 바꾼 커밋이 README 에 한 일
|
|
README 를 건드렸는가 : 1
|
|
그 커밋이 README 에 새로 붙인 절 : 3 개
|
|
그 커밋 앞뒤의 36 행
|
|
before 이 모듈은 `jackson-databind` 를 classpath 에 두지 않아(스켈레톤을 가볍게 유지) outbox envelope
|
|
after 이 모듈은 `jackson-databind` 를 classpath 에 두지 않아(스켈레톤을 가볍게 유지) outbox envelope
|
|
```
|
|
|
|
같은 커밋이 이 README 를 열어 절 셋을 새로 붙였다. 방금 자기가 무너뜨린 그 한 줄만 그대로 두었다.
|
|
|
|
```text
|
|
# README 를 건드린 커밋 전부
|
|
e5af2912 2026-07-31 feat: add messaging R2 polling producer
|
|
b3add016 2026-07-28 feat: redis, fileserver, httpclient 런타임 시점 구현 추가
|
|
821fe00c 2026-07-24 init: 클린 아키텍처 백엔드
|
|
```
|
|
|
|
그 뒤로 이 파일을 연 커밋은 없다.
|
|
|
|
## 같은 리프에서 그 라이브러리를 쓰는 곳
|
|
|
|
```text
|
|
realtime/RealtimeFanoutEnvelopeJson.java:5:import tools.jackson.databind.ObjectMapper;
|
|
realtime/RealtimeFanoutEnvelopeJson.java:6:import tools.jackson.databind.json.JsonMapper;
|
|
realtime/RealtimeFanoutEnvelopeJson.java:7:import tools.jackson.databind.node.ObjectNode;
|
|
realtime/RealtimeFanoutEnvelopeJson.java:71: private static String text(tools.jackson.databind.JsonNode root, String field) {
|
|
envelope/LocalJsonSchemaRegistry.java:33:import tools.jackson.databind.DeserializationFeature;
|
|
envelope/LocalJsonSchemaRegistry.java:34:import tools.jackson.databind.JsonNode;
|
|
envelope/LocalJsonSchemaRegistry.java:35:import tools.jackson.databind.ObjectMapper;
|
|
envelope/LocalJsonSchemaRegistry.java:36:import tools.jackson.databind.json.JsonMapper;
|
|
```
|
|
|
|
README 가 근거로 든 것은 모듈 차원의 부재다. 그 부재가 깨진 자리에서 같은 리프의 옆 파일이 같은 종류의 일을 그 라이브러리로 한다.
|
|
|
|
```java
|
|
24: private static final String VERSION = "1";
|
|
25: private static final ObjectMapper MAPPER = JsonMapper.builder().build();
|
|
30: public static String toJson(DurableFanoutRecord record) {
|
|
31: Objects.requireNonNull(record, "record must not be null");
|
|
32: ObjectNode root = MAPPER.createObjectNode();
|
|
```
|
|
|
|
봉투 JSON 을 조립하는 일이다.
|
|
|
|
## 다만 그 경로는 아직 돌지 않는다
|
|
|
|
```text
|
|
RealtimeFanoutEnvelopeJson 을 자기 파일 밖에서 부르는 곳 : 1
|
|
MessagingDurableFanoutAdapter 를 자기 파일 밖에서 부르는 곳 : 0
|
|
```
|
|
|
|
부르는 하나는 팬아웃 어댑터이고, 그 어댑터를 조립하는 곳은 없다. 클래스패스 사실은 그대로이고, 도는 경로만 아직 없다.
|
|
|
|
## 이름공간을 한정하지 않는다
|
|
|
|
구세대 이름공간의 결속과 코어를 막는 것은 빌드 파일 검증 태스크의 단언이다. 구성 단위의 제외 목록은 형식 계열만 뺀다.
|
|
|
|
```text
|
|
8:com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
|
```
|
|
|
|
같은 이름공간의 애너테이션은 지금도 컴파일과 실행에 있다. 서술이 결속과 코어만 뜻했다면 맞지만, 문장에 이름공간이 없다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
실시간 팬아웃 봉투가 손수 짠 쪽과 같은 규율을 따라야 하는지는 판단하지 않았다. 여기서 확인한 것은 문장이 서술하는 사실 관계까지다.
|
|
|
|
<!-- body:end -->
|