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
+42
@@ -0,0 +1,42 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-graphql-c02
|
||||
title: 스키마 해시의 생산자도 소비자도 빈이 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-graphql-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-graphql-c02
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-graphql-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-graphql-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L221 이다.
|
||||
module: adapter-inbound-graphql
|
||||
---
|
||||
|
||||
# 스키마 해시의 생산자도 소비자도 빈이 없다
|
||||
|
||||
`GraphQlSchemaHash`의 유일한 생산 경로와 그 소비처가 둘 다 프로덕션 호출자를 갖지 않는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GraphQlSchemaHash`의 유일한 생산 경로는 `GraphQlSchemaAssemblyResult.schemaHash()`(`:94-95`)이고, 그 결과 타입은 `GraphQlSchemaAssembler.assemble(...)`만 만든다. 둘 다 프로덕션 호출자가 없다(§7.1).
|
||||
|
||||
## GraphQlSchemaHash 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-graphql-c02" alt="코드베이스에서 GraphQlSchemaHash 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlSchemaHash 코드베이스 검색 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 소비 쪽도 빈이 없다
|
||||
|
||||
소비 쪽은 `GraphQlPlatformActuatorEndpoint`가 생성자로 받는다. 그런데 `GraphQlPlatformAutoConfiguration`의 39개 `@Bean` 중 이것을 만드는 것이 없다. §8.1.
|
||||
|
||||
*(이 파일은 `autoconfigure` 패키지에 있어 sub-scope 01의 분모에 포함된다. 스키마 해시 사슬의 소비 쪽이므로 여기서 함께 다룬다.)*
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-graphql-c07
|
||||
title: 파싱·검증 실패의 와이어 형식은 이 플랫폼이 정하지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-graphql-c07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-graphql-c07
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-graphql-c07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-graphql-c07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L604 이다.
|
||||
module: adapter-inbound-graphql
|
||||
---
|
||||
|
||||
# 파싱·검증 실패의 와이어 형식은 이 플랫폼이 정하지 않는다
|
||||
|
||||
`GraphQlRequestErrorMapper`의 진단은 정확하지만 배선된 것은 리졸버 쪽뿐이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GraphQlRequestErrorMapper`의 javadoc이 자기 존재 이유를 적는다. 진단이 정확하고, 배선된 것은 리졸버 쪽(`GraphQlExceptionResolver`, autoconf=4)뿐이다. 파싱·검증 실패의 와이어 형식을 이 플랫폼이 정하지 않는다는 뜻이다.
|
||||
|
||||
## GraphQlRequestErrorMapper 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-graphql-c07" alt="코드베이스에서 GraphQlRequestErrorMapper 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GraphQlRequestErrorMapper 코드베이스 검색 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 플랫폼이 거부하는 실패는 안정 코드로 매핑된다
|
||||
|
||||
`runtime/GraphQlWireErrorMapper`(autoconf=4)와 `runtime/GraphQlPlatformRejectionMapper`(main_other=3)가 배선돼 있어 익명 연산, 복잡도 초과 등은 안정 코드로 매핑된다. 덮이지 않는 것은 graphql-java 자신이 만드는 구문/검증 오류다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-graphql-c11
|
||||
title: 키를 요구하는 코드는 둘이고 서명하는 코드는 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-graphql-c11
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-graphql-c11
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-graphql-c11.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-graphql-c11.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/16-adapter-inbound-graphql.md#L725 이다.
|
||||
module: adapter-inbound-graphql
|
||||
---
|
||||
|
||||
# 키를 요구하는 코드는 둘이고 서명하는 코드는 없다
|
||||
|
||||
`backend.graphql.cursor.key-ids`를 읽는 프로덕션 코드는 시작 검증기와 액추에이터 둘이다. 그 키로 커서에 서명하는 코드는 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`backend.graphql.cursor.key-ids`를 읽는 프로덕션 코드는 둘이다.
|
||||
|
||||
```text
|
||||
autoconfigure/GraphQlPlatformStartupValidator.java:42-43
|
||||
if (properties.production() && properties.cursor().keyIds().isEmpty()) {
|
||||
problems.add("a cursor signing key is required; unsigned cursors are client-editable");
|
||||
|
||||
autoconfigure/GraphQlPlatformActuatorEndpoint.java (보고서에 포함)
|
||||
```
|
||||
|
||||
## HmacGraphQlCursorCodec 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-graphql-c11" alt="코드베이스에서 HmacGraphQlCursorCodec 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HmacGraphQlCursorCodec 코드베이스 검색 — 16줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 서명하는 코드가 없다
|
||||
|
||||
`HmacGraphQlCursorCodec`과 `GraphQlCursorKeyRing`은 autoconf=0 · main_other=0이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-web-c08
|
||||
title: 페이지네이션 어휘가 한 번도 컨트롤러에 붙어 본 적이 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-web-c08
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-web-c08
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-web-c08.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-web-c08.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L843 이다.
|
||||
module: adapter-inbound-web
|
||||
---
|
||||
|
||||
# 페이지네이션 어휘가 한 번도 컨트롤러에 붙어 본 적이 없다
|
||||
|
||||
다섯 패키지 중 소비 모듈이 실제로 부르는 것은 `ETags` 하나다. 나머지는 전부 테스트 전용이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
다섯 패키지 중 소비 모듈이 실제로 부르는 것은 `ETags` 하나다 — `sample-portfolio`의 `WorkLogController`가 세 곳에서 쓴다(`:144` `If-None-Match` 비교, `:213` 버전에서 약한 ETag 생성, `:226` `If-Match` 검사). 나머지는 전부 테스트 전용이다.
|
||||
|
||||
## WorkLogController 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-web-c08" alt="코드베이스에서 WorkLogController 를 검색한 출력 18줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WorkLogController 코드베이스 검색 — 18줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 라이브러리인데도 기록해 두는 이유
|
||||
|
||||
라이브러리이므로 그 자체가 결함은 아니지만, 페이지네이션 어휘 19개 파일·버전 관리 7개 파일이 **한 번도 컨트롤러에 붙어 본 적이 없다**는 사실은 기록해 둘 값이 있다 — 이 저장소가 다른 곳에서 "타입은 있고 호출자가 없다"를 반복해서 결함으로 취급했기 때문이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-web-c10
|
||||
title: 어느 쪽이 정본인지 코드로 판정할 수 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-web-c10
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-web-c10
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-web-c10.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-web-c10.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L889 이다.
|
||||
module: adapter-inbound-web
|
||||
---
|
||||
|
||||
# 어느 쪽이 정본인지 코드로 판정할 수 없다
|
||||
|
||||
커서 코덱이 두 벌이고 `WebStableModule`이 둘을 다른 모듈로 선언한다. 둘 다 프로덕션 소비자가 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
커서 코덱이 두 벌이다 — `pagination/WebCursorCodec`(인터페이스) + `pagination/HmacWebCursorCodec`(135, 서명된 구현) + `pagination/WebCursorPayload` + `pagination/WebCursorKeyRing`, 그리고 별도로 `cursor/CursorCodec`(100) + `cursor/CursorException`. `WebStableModule`은 둘을 다른 모듈로 선언한다(`CURSOR` = "Opaque keyset cursor encoding and its failure type", `PAGINATION`).
|
||||
|
||||
## WebStableModule 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-web-c10" alt="코드베이스에서 WebStableModule 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebStableModule 코드베이스 검색 — 14줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 코드로 정본을 판정할 수 없다
|
||||
|
||||
둘 다 프로덕션 소비자가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-web-c11
|
||||
title: 기여자가 커스터마이저에 도달하지 않아 스키마가 문서에 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-web-c11
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-web-c11
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-web-c11.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-web-c11.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/14-adapter-inbound-web.md#L978 이다.
|
||||
module: adapter-inbound-web
|
||||
---
|
||||
|
||||
# 기여자가 커스터마이저에 도달하지 않아 스키마가 문서에 없다
|
||||
|
||||
배선된 `OpenApiCustomizer`는 익명 람다 하나이고, 607줄짜리 미배선 쪽이 담은 기여자들은 커스터마이저에 도달하지 않는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
배선된 것은 `config/OpenApiContractConfig`(37줄, `@Configuration`)가 익명 람다 `OpenApiCustomizer` 하나를 빈으로 등록하는 것뿐이다. 하는 일은 `ApiError.details` 스키마를 `ObjectSchema`로 되돌리는 것 한 가지다.
|
||||
|
||||
## OpenApiCustomizer 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-web-c11" alt="코드베이스에서 OpenApiCustomizer 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OpenApiCustomizer 코드베이스 검색 — 7줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 배선되지 않은 607줄
|
||||
|
||||
`openapi/WebOpenApiCustomizer`(74줄)와 그것이 쓰는 `ProblemSchemaContributor`(88) · `CursorSchemaContributor`(47) · `WebOpenApiProfile`(72) · `WebOpenApiBreakingPolicy`(187) · `WebOpenApiReleaseGate`(100) · `WebOpenApiDiffResult`(39). 빈으로 등록하는 코드가 main·app-bootstrap에 없고, 참조는 자기들끼리와 테스트뿐이다.
|
||||
|
||||
## 문서에 기여되지 않는 스키마
|
||||
|
||||
springdoc이 생성하는 문서에는 RFC 9457 problem 스키마도 커서 스키마도 기여되지 않는다 — 그 기여자들이 커스터마이저에 도달하지 않기 때문이다. SS2에서 확인한 "problem 계약이 문서에 없다"(§7.3)와 같은 방향의 사실이 스키마 쪽에서도 성립한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-websocket-c03
|
||||
title: 오류 형식이 세 벌이다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-websocket-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-websocket-c03
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-websocket-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-websocket-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/17-adapter-inbound-websocket.md#L267 이다.
|
||||
module: adapter-inbound-websocket
|
||||
---
|
||||
|
||||
# 오류 형식이 세 벌이다
|
||||
|
||||
`WebSocketFailureCategory`(main_other=10)가 이 sub-scope에서 가장 널리 참조되는 타입인데, 실제 STOMP 오류는 세 번째 형식이 만든다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`error` 패키지의 `WebSocketFailureCategory`(main_other=10)가 이 sub-scope에서 가장 널리 참조되는 타입이고, `WebSocketErrorMessage`(75, main 참조 0)와 `WebSocketErrorTransport`가 그것을 전송으로 옮긴다.
|
||||
|
||||
## WebSocketFailureCategory 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-websocket-c03" alt="코드베이스에서 WebSocketFailureCategory 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebSocketFailureCategory 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 실제 STOMP 오류를 만드는 세 번째 형식
|
||||
|
||||
`stomp/SafeStompSubProtocolErrorHandler`(32)가 만든다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-websocket-c04
|
||||
title: 키를 요구하는 검증기가 없어서 잘못된 확인 신호도 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-websocket-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-websocket-c04
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-websocket-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-websocket-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/17-adapter-inbound-websocket.md#L381 이다.
|
||||
module: adapter-inbound-websocket
|
||||
---
|
||||
|
||||
# 키를 요구하는 검증기가 없어서 잘못된 확인 신호도 없다
|
||||
|
||||
`ResumeTokenCodec`(217) + `ResumeTokenKeyRing`(87) 조합은 graphql §24.1의 커서 코덱 조합과 같은 형태인데, 차이가 하나 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ResumeTokenCodec`(217) + `ResumeTokenKeyRing`(87)이 서명된 재개 토큰을 만든다. 이 조합은 graphql §24.1의 `HmacGraphQlCursorCodec` + `GraphQlCursorKeyRing`과 같은 형태다.
|
||||
|
||||
## ResumeTokenCodec 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-websocket-c04" alt="코드베이스에서 ResumeTokenCodec 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ResumeTokenCodec 코드베이스 검색 — 26줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 잘못된 확인 신호가 없는 이유
|
||||
|
||||
**차이는 이쪽에는 그 키를 요구하는 시작 검증기가 없다는 것** — 즉 "키를 요구하고 서명하지 않는" 잘못된 확인 신호가 없다. 면책 목록에 replay/resume이 있으므로 문서·코드·검증이 일치한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-cache-redis-c07
|
||||
title: guard 아래에서는 명령 이름으로 위험을 다시 유도할 수 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-cache-redis-c07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-cache-redis-c07
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L350 이다.
|
||||
module: adapter-outbound-cache-redis
|
||||
---
|
||||
|
||||
# guard 아래에서는 명령 이름으로 위험을 다시 유도할 수 없다
|
||||
|
||||
`RedisCommandDescriptor`가 공식 서버 메타데이터와 조직 정책의 접합점이고, 생성자가 그 접합의 모순 네 가지를 거부한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`RedisCommandDescriptor`는 "the join between official server metadata and organization policy"이고, 그 아래를 못박는다 — "Nothing downstream of the guard is allowed to re-derive risk, access, or timeout from a command name." 생성자가 그 접합의 모순 네 가지를 거부한다.
|
||||
|
||||
| 불변식 | 의미 |
|
||||
|---|---|
|
||||
| `BLOCKED` ⇒ `access == NONE` | 차단된 명령은 ACL 계정을 갖지 않는다 |
|
||||
| `R4` ⇒ `BLOCKED` | 최고 위험은 반드시 차단 |
|
||||
| `R3` ⇒ `ADMIN_ONLY` 또는 `BLOCKED` | 관리 위험은 애플리케이션에 열리지 않는다 |
|
||||
| 쓰기 ∧ `retrySafe` ∧ `mayBeAmbiguous` ⇒ 거부 | 모호할 수 있는 쓰기를 재시도 안전으로 선언 불가 |
|
||||
|
||||
마지막 하나가 §23의 런타임 불변식과 같은 규칙의 **선언 시점** 짝이다 — 하나는 정책 파일이 거짓말하지 못하게 하고, 하나는 실패 객체가 거짓말하지 못하게 한다.
|
||||
|
||||
## RedisCommandDescriptor 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-cache-redis-c07" alt="코드베이스에서 RedisCommandDescriptor 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisCommandDescriptor 코드베이스 검색 — 28줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## KeySpec과 CommandId가 없애는 불일치
|
||||
|
||||
`KeySpec`은 공식 Redis 규약(1-based, 음수 lastKey는 뒤에서부터, `movable`은 정적 유도 불가)을 그대로 따르고, `movable`이면 `resolvePositions`가 던진다 — "movable key specification must be resolved by the server". `CommandId`는 항상 대문자로 정규화해 "a policy file, a server metadata reply, and an SDK call site cannot disagree because of casing."
|
||||
|
||||
## permit이 절대 넓히지 못하는 것
|
||||
|
||||
permit 세 종은 인터페이스이고 javadoc이 경계를 명확히 한다 — "Application code may implement this interface, but a self-made instance never passes `RedisPermitVerifier`… The final enforcement boundary remains the Redis ACL account, which a permit never widens." `PersistentKeyPermit`은 한 줄 더 붙인다: "Cache, session, lock, idempotency, and rate-limit APIs never accept this permit."
|
||||
|
||||
## 예산이 선택이 아닌 이유
|
||||
|
||||
`OperationBudget`도 규칙을 문서로 못박는다 — "Every R2 API requires a budget. The budget is never optional and never defaulted, because the whole point is that the caller states the cost it is prepared to pay before Redis is asked."
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-cache-redis-c11
|
||||
title: R4인데 BLOCKED이 아닌 정책 파일은 읽히지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-cache-redis-c11
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-cache-redis-c11
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c11.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c11.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L579 이다.
|
||||
module: adapter-outbound-cache-redis
|
||||
---
|
||||
|
||||
# R4인데 BLOCKED이 아닌 정책 파일은 읽히지 않는다
|
||||
|
||||
`RedisCommandPolicyLoader`가 일반 YAML 파서를 쓰지 않고, 교차 필드 불변식이 로딩 시점에 적용된다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`RedisCommandPolicyLoader`의 javadoc이 일반 YAML 파서를 쓰지 않는 이유를 적는다. 파서는 그만큼 좁다 — 탭 금지, 들여쓰기 0/2/4만 허용, `commands:` 루트 정확히 하나, 명령 블록 중복 금지, 필드 이름 allowlist(12종) 밖이면 거부, 빈 값 거부, 필드 중복 거부. test가 `rejectsUnknownFieldsEnumsAndDuplicates`로 잡는다.
|
||||
|
||||
## RedisCommandPolicyLoader 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-cache-redis-c11" alt="코드베이스에서 RedisCommandPolicyLoader 를 검색한 출력 6줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisCommandPolicyLoader 코드베이스 검색 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 모순된 정책 파일은 읽히지 않는다
|
||||
|
||||
`RedisCommandPolicy`/`RedisCommandDescriptor`의 교차 필드 불변식(§24)이 로딩 시점에 적용되므로, "R4인데 BLOCKED이 아닌" 정책 파일은 **읽히지 않는다**.
|
||||
|
||||
## 집합을 고정하는 테스트
|
||||
|
||||
`everyDestructiveCommandIsBlockedAndUnreachable`·`deprecatedCommandNamesAreNotReachable`·`arbitraryScriptSourceExecutionIsBlocked`·`theCatalogFailsClosedForAnUnclassifiedCommand`.
|
||||
|
||||
<!-- body:end -->
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-fileserver-c02
|
||||
title: 파싱은 되지만 우리가 쓰지 않았을 형태를 전부 거부한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-fileserver-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-fileserver-c02
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-fileserver-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-fileserver-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/08-adapter-outbound-fileserver.md#L168 이다.
|
||||
module: adapter-outbound-fileserver
|
||||
---
|
||||
|
||||
# 파싱은 되지만 우리가 쓰지 않았을 형태를 전부 거부한다
|
||||
|
||||
`FileserverControlRecordCodec`이 decode 직후 재encode해 바이트를 비교한다(`requireCanonical(bytes, encodeOperation(record))`).
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`FileserverControlRecordCodec`은 세 레코드와 receipt snapshot에 대해 **decode 직후 재encode해 바이트를 비교**한다(`requireCanonical(bytes, encodeOperation(record))`). 그래서 "파싱은 되지만 우리가 쓰지 않았을 형태"가 전부 거부된다 — 공백, 필드 재배열, 이스케이프, `-0`·선행 0 같은 숫자 표기, 후행 콘텐츠.
|
||||
|
||||
## FileserverControlRecordCodec 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-fileserver-c02" alt="코드베이스에서 FileserverControlRecordCodec 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FileserverControlRecordCodec 코드베이스 검색 — 12줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 파서 자체도 좁다
|
||||
|
||||
- 필드 집합을 **정확히 일치**시킨다(`values.keySet().equals(allowedFields)`) — 누락도 미지 필드도 거부
|
||||
- 중복 키를 거부한다(`putIfAbsent`)
|
||||
- UTF-8 디코딩이 `REPORT` 모드라 malformed 바이트가 대체문자로 조용히 바뀌지 않는다
|
||||
- `\b \f \n \r \t` 이스케이프를 **문법 수준에서 거부**한다("control characters are forbidden")
|
||||
- 짝 없는 서로게이트를 거부한다(`requireWellFormedUnicode`)
|
||||
- `Instant.parse` 후 `result.toString().equals(value)`로 **canonical UTC 표기**만 받는다
|
||||
- receipt snapshot은 `rsv1.` 접두사 + unpadded base64url이고, 디코딩 후 **재인코딩 문자열 비교**로 alias(후행 비트가 0이 아닌 변형)를 거부한다
|
||||
|
||||
## 오버플로가 상한 검사를 무력화하지 못한다
|
||||
|
||||
`requireFormulaCountWithinCells`가 `rowCount * columnCount` 곱을 하기 전에 `rowCount <= Long.MAX_VALUE / columnCount`를 먼저 본다. test가 그 하나하나를 이름으로 고정한다 — `canonicalDecoderRejectsWhitespaceReorderingEscapesNumbersUtf8AndTrailingContent`, `receiptSnapshotRejectsBase64urlAliasWithNonZeroTrailingBits`, `canonicalCodecRoundTripsSupplementaryUnicodeInOpaqueText`, `formulaMitigationCountCannotExceedCellsAndUsesOverflowSafeBounds`.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-httpclient-c03
|
||||
title: 검사할 수 없는 성분을 가진 본문은 재생 가능으로 인증되지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-httpclient-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-httpclient-c03
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-httpclient-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L237 이다.
|
||||
module: adapter-outbound-httpclient
|
||||
---
|
||||
|
||||
# 검사할 수 없는 성분을 가진 본문은 재생 가능으로 인증되지 않는다
|
||||
|
||||
`ObjectBody`의 재생 가능성은 값의 성질로 구조적으로 판정되고, 판정할 수 없으면 `ONE_SHOT`으로 떨어진다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 sub-scope에서 가장 신중한 코드다. 과거 동작과 그 결과가 적혀 있다.
|
||||
|
||||
## ObjectBody 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-httpclient-c03" alt="코드베이스에서 ObjectBody 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectBody 코드베이스 검색 — 29줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 값의 성질로 판정한다
|
||||
|
||||
지금은 `deeplyImmutable(value)`가 구조적으로 판정한다 — 문자열·숫자·불리언·문자·enum·UUID·`Temporal`은 통과, 컬렉션과 맵은 **JDK의 불변 뷰인지 이름으로 확인**하고 원소까지 재귀, record는 모든 성분을 반사로 재귀 확인, 그 외는 전부 `ONE_SHOT`.
|
||||
|
||||
## 인증할 수 없으면 재생하지 않는다
|
||||
|
||||
반사가 실패하면 "A component the platform cannot inspect cannot be certified, and an uncertified body is one-shot rather than optimistically replayable." 컬렉션 판정이 이름 기반인 이유도 적혀 있다 — "`List.of(...)` and `Collections.unmodifiableList(...)` return package-private classes with no shared marker interface. An ordinary `ArrayList` the caller still holds is exactly the case this must not accept." test 넷이 네 갈래를 고정한다 — `anImmutableRecordReplays`, `aMutableValueIsOneShot`, `aRecordWrappingMutableStateIsOneShot`, `anArbitraryBeanIsOneShot`.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-httpclient-c06
|
||||
title: 와이어 바이트와 디코드 바이트를 따로 세고, 읽는 도중에 상한을 건다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-httpclient-c06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-httpclient-c06
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-httpclient-c06.svg
|
||||
- key: adapter-outbound-httpclient-c06-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-httpclient-c06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-httpclient-c06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L403 이다.
|
||||
module: adapter-outbound-httpclient
|
||||
---
|
||||
|
||||
# 와이어 바이트와 디코드 바이트를 따로 세고, 읽는 도중에 상한을 건다
|
||||
|
||||
예산이 둘로 나뉘어 있고 강제 시점이 버퍼링 이후가 아니라 읽는 도중이며, 원격이 준 status는 채택되지 않는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ResponseSizeLimiter`가 **와이어 바이트와 디코드 바이트를 따로** 센다 — "a compressed payload passes a wire check and then expands, so a single limit either rejects legitimate traffic or lets a **decompression bomb** through."
|
||||
|
||||
## 예산이 강제되는 자리
|
||||
|
||||
:::evidence key="adapter-outbound-httpclient-c06-diagram" alt="와이어 바이트 예산, 읽는 도중 상한 강제, 디코드 바이트 예산, 오류 본문 제한 읽기가 읽는 순서대로 쌓여 있다" caption="응답 크기 예산이 강제되는 자리" zoom="false"
|
||||
:::
|
||||
|
||||
## 버퍼링이 끝난 뒤에는 늦다
|
||||
|
||||
`CountingBoundedInputStream`이 상한을 **읽는 도중에** 적용한다 — "a response that is discovered to be too large only once it is fully buffered has already cost the memory the limit exists to protect." `BoundedErrorBody`는 오류 본문을 RFC 9457 문서를 해독할 만큼만 읽고, "The bytes never reach an exception message or a log." `toString()`은 길이와 truncated 여부만 낸다.
|
||||
|
||||
## 원격이 준 status는 버린다
|
||||
|
||||
`RemoteProblemDecoder`의 규칙 한 줄이 이 계층의 성격을 요약한다 — "**The wire status wins.** A remote `status` member is read and discarded, because trusting it would let an upstream **relabel a 503 as a 400 and change our retry behaviour from its own body.**" 확장 속성도 allowlist로 걸러 "an upstream cannot inject unbounded attributes into our telemetry." test 넷이 그 갈래를 고정한다(`mapsProblemJsonWithoutTrustingBodyStatus`·`dropsExtensionsThatAreNotAllowlisted`·`treatsANonProblemContentTypeAsAnEmptyProblem`·`survivesAnUnparseableProblemDocument`).
|
||||
|
||||
## ResponseSizeLimiter 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-httpclient-c06" alt="코드베이스에서 ResponseSizeLimiter 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ResponseSizeLimiter 코드베이스 검색 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-messaging-c01
|
||||
title: 구성이 끝난 뒤에는 스키마를 가져올 방법이 남아 있지 않다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-messaging-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-messaging-c01
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-messaging-c01.svg
|
||||
- key: adapter-outbound-messaging-c01-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-messaging-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-messaging-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/12-adapter-outbound-messaging.md#L174 이다.
|
||||
module: adapter-outbound-messaging
|
||||
---
|
||||
|
||||
# 구성이 끝난 뒤에는 스키마를 가져올 방법이 남아 있지 않다
|
||||
|
||||
`LocalJsonSchemaRegistry`의 닫힘이 어휘·키워드·참조·메타스키마 권위 네 겹으로 표현되고, 매니페스트 핀이 실제 파일을 가리키는 것까지 probe로 확인했다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`LocalJsonSchemaRegistry`의 한 줄 요약이 계약이다 — "Immutable, startup-compiled Draft 2020-12 registry backed **only by explicitly supplied bytes**. Every reference is checked before NetworkNT compilation. After construction this type exposes **no loader, URL, file or classpath fetch operation**."
|
||||
|
||||
## 레지스트리가 닫힌 방식
|
||||
|
||||
:::evidence key="adapter-outbound-messaging-c01-diagram" alt="넘겨받은 바이트와 동봉된 메타스키마와 허용 어휘가 레지스트리 안에 놓이고 원격 참조 로더와 동적 키워드가 바깥에 빗금으로 놓인다" caption="레지스트리가 닫힌 방식" zoom="false"
|
||||
:::
|
||||
|
||||
## 닫힘이 네 겹으로 표현된다
|
||||
|
||||
1. **어휘 allowlist** — `KNOWN_VOCABULARIES` 8종(core·applicator·unevaluated·validation·meta-data·format-annotation·format-assertion·content) 밖의 `$vocabulary` 항목은 거부된다.
|
||||
2. **키워드 부분집합** — `$anchor`·`$dynamicRef`·`$dynamicAnchor`·`$recursiveRef`·`$recursiveAnchor` 다섯이 `UNSUPPORTED_CLOSED_SUBSET_KEYWORDS`로 **문서 어디에서든** 거부된다(test `rejectsDynamicRecursiveAndAnchorKeywordsEverywhereInTheClosedSubset`).
|
||||
3. **참조 사전 검사** — `validateAllReferences`가 NetworkNT 컴파일 **전에** 모든 `$ref`를 확인하고, 원격 참조와 설정된 깊이를 넘는 참조 그래프를 거부한다.
|
||||
4. **핀 고정된 메타스키마 권위** — 9개 Draft 2020-12 메타 문서를 리소스로 동봉하고 `authority.sha256` 매니페스트로 해시를 고정하며, 도메인 분리 상수(`ca-skeleton.messaging.draft-2020-12-authority.v1`)를 섞는다. 매니페스트는 UTF-8 디코딩을 `REPORT` 모드로 읽어 잘못된 바이트를 조용히 대체하지 않는다.
|
||||
|
||||
## 핀이 실제 파일을 가리키는지 실행해서 봤다
|
||||
|
||||
동봉된 9개 파일의 SHA-256이 `authority.sha256`의 아홉 줄과 **전부 일치한다**(`178-...` §8.3).
|
||||
|
||||
## LocalJsonSchemaRegistry 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-messaging-c01" alt="코드베이스에서 LocalJsonSchemaRegistry 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalJsonSchemaRegistry 코드베이스 검색 — 37줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 식별자는 정확한 URN 스킴만 받는다
|
||||
|
||||
`$id`는 정확한 URN 스킴만 허용하고(`acceptsOnlyExactUrnSchemeForRootIdentifiersAndAbsoluteReferences`), 중첩 `$id`는 상대·절대 어느 쪽도 허용하지 않으며 **값 타입과 무관하게 키 자체를** 거부한다(`rejectsNestedSchemaIdentifierKeysRegardlessOfValueType`).
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-messaging-c02
|
||||
title: 봉투를 쓰는 경로에 JSON 파서도 생성기도 없다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-messaging-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-messaging-c02
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-messaging-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-messaging-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/12-adapter-outbound-messaging.md#L189 이다.
|
||||
module: adapter-outbound-messaging
|
||||
---
|
||||
|
||||
# 봉투를 쓰는 경로에 JSON 파서도 생성기도 없다
|
||||
|
||||
`DeterministicEnvelopeWriter`가 페이로드를 선언된 shape대로 스냅샷해 그 바이트를 리터럴로 이어 붙이므로, 같은 키로 다른 바이트가 나오는 경로가 구조적으로 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DeterministicEnvelopeWriter`는 페이로드를 **선언된 shape을 따라 스냅샷**한 뒤 그 정확한 바이트를 봉투에 끼워 넣는다 — "those exact trusted bytes are then embedded in the envelope **without any raw JSON parser or generator API**." `embedExactPayload`가 `,"payload":` 리터럴로 이어 붙이는 방식이다.
|
||||
|
||||
## DeterministicEnvelopeWriter 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-messaging-c02" alt="코드베이스에서 DeterministicEnvelopeWriter 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DeterministicEnvelopeWriter 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 입력이 통과해야 하는 것
|
||||
|
||||
draft의 페이로드가 **정확히 등록된 final record 클래스**여야 하고(`exactPayloadClassIsRequiredAndNoAssignableTypeSearchOccurs`), contractId와 payloadVersion이 컴파일된 계약과 같아야 하며, 레코드 성분 수·문자열 UTF-8 길이·배열/객체 크기·깊이가 모두 `EnvelopeAdmissionLimits`로 유계다. 그리고 **쓰는 도중에** 출력 크기를 본다(`boundsJsonOutputDuringWritesInsteadOfOnlyInspectingTheCompletedBuffer`).
|
||||
|
||||
## 접근자를 한 번만 부른다
|
||||
|
||||
가변 페이로드 처리도 명시적이다 — `snapshotsStatefulMutablePayloadAccessorsOnceAndEmbedsThoseExactBytes`. 접근자를 한 번만 부르고 그 바이트를 고정하므로, httpclient의 `ObjectBody` 문제(같은 키로 다른 바이트)가 여기서는 구조적으로 불가능하다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-objectstorage-c04
|
||||
title: 새 레코드를 추가하면 컴파일이 codec 갱신을 강제한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-objectstorage-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-objectstorage-c04
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-objectstorage-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L173 이다.
|
||||
module: adapter-outbound-objectstorage
|
||||
---
|
||||
|
||||
# 새 레코드를 추가하면 컴파일이 codec 갱신을 강제한다
|
||||
|
||||
`ObjectControlRecord`가 sealed이고 codec이 exhaustive switch를 쓰므로 계열이 닫혀 있고, 모르는 스키마는 덮어쓰지 않고 격리된다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ObjectControlRecord`는 열한 개 구현만 허용하는 `sealed interface`이고, javadoc이 규칙을 적는다 — "Unknown families and schemas fail closed." codec의 `payload(...)`가 그 계열에 대해 **exhaustive switch**를 쓰므로, 새 레코드를 추가하면 컴파일이 강제로 codec을 갱신하게 만든다.
|
||||
|
||||
## ObjectControlRecord 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-objectstorage-c04" alt="코드베이스에서 ObjectControlRecord 를 검색한 출력 35줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectControlRecord 코드베이스 검색 — 35줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 모르는 스키마는 덮어쓰지 않고 격리한다
|
||||
|
||||
스키마 버전은 `ControlRecordSupport.header`가 `schemaVersion != 1`을 거부한다 — "only control schema version 1 is writable". 더 새로운 스키마를 만나면 `UnsupportedObjectControlSchemaException`으로 격리한다("A newer or unknown durable schema that must be quarantined rather than overwritten").
|
||||
|
||||
## 누출 금지가 가시성으로 성립한다
|
||||
|
||||
`objectstorage.control` 패키지를 leaf 밖에서 참조하는 코드는 **0**이다(`151-...` §8.1, exit=1). CLAUDE.md의 "control-record types leaking into application-core" 금지가 가시성으로 성립한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-objectstorage-c06
|
||||
title: 다른 route의 identity로 만든 키는 파싱 단계에서 거부된다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-objectstorage-c06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-objectstorage-c06
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-objectstorage-c06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-objectstorage-c06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/09-adapter-outbound-objectstorage.md#L273 이다.
|
||||
module: adapter-outbound-objectstorage
|
||||
---
|
||||
|
||||
# 다른 route의 identity로 만든 키는 파싱 단계에서 거부된다
|
||||
|
||||
키를 만드는 인코더가 각 계열마다 하나뿐이고, route 격리와 핸들 계열 분리가 파싱 시점 검사로 성립한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ObjectControlKeyCodec`과 `ObjectDataKeyCodec`이 각각 "Sole encoder"를 자칭하고, 저장소에서 `"control/v1/"`·`"data/v1/"` 리터럴은 이 두 파일에만 있다(`152-...` §8.3). 키 형태는 `control/v1/<family>/<route>/<shard>/<identity>`이고 shard는 identity의 SHA-256 앞 두 자리다.
|
||||
|
||||
## ObjectControlKeyCodec 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-objectstorage-c06" alt="코드베이스에서 ObjectControlKeyCodec 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ObjectControlKeyCodec 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## route 격리가 구조적이다
|
||||
|
||||
`requireMatchingRoute(route, routedIdentity)`가 routed identity의 route 구획을 파싱해 현재 route와 다르면 거부한다("routed identity belongs to a different route"). reference·session·stage handle 키 모두 이 검사를 지난다.
|
||||
|
||||
## 핸들 계열이 접두사로 분리된다
|
||||
|
||||
`osh1`(stage), `osu1`(direct upload), `osm1`(multipart), `osv1`(version), `osr1`(reference). `ObjectNamespaceCodecTest.referenceAndHandleFamiliesRemainSeparated`가 그 분리를 고정하고, `dataKeyApiHasNoRawNameStringParameter`는 **API 서명 자체에 raw 이름 문자열이 없음**을 단언한다.
|
||||
|
||||
## 잘림을 조용히 넘기지 않는다
|
||||
|
||||
`CrockfordBase32`는 소문자 정규 알파벳(`0123456789abcdefghjkmnpqrstvwxyz` — I·L·O·U 제외)을 쓰고, 인코딩 후 남은 값이 있으면 거부한다("base32 output length is too small").
|
||||
|
||||
## 무엇이 이 성질을 고정하고 있나
|
||||
|
||||
property-based 검사가 있다 — `routeParserRejectsArbitraryNonCanonicalText(@ForAll String candidate)`(jqwik), `namespaceRejectsAliasesAndTraversalInputs`. fingerprint codec에는 **golden vector**가 고정돼 있다(`canonicalIntentHasAFrozenGoldenVector`, `sameIntentIsStableAndEverySemanticChangeChangesTheFingerprint`). `policySnapshotCodecIsCanonicalAndContainsNoCredentialSurface`는 정책 스냅샷에 자격증명 표면이 없음을 test로 고정한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c05
|
||||
title: 자기가 만든 토큰을 자기가 거부하는 경계값
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c05
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c05.svg
|
||||
- key: adapter-outbound-persistence-jpa-c05-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L358 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 자기가 만든 토큰을 자기가 거부하는 경계값
|
||||
|
||||
trust boundary 방어는 촘촘한데, decode 전 크기 추정 helper가 padding을 가정해 encode 허용 영역과 decode 허용 영역이 어긋난다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
codec은 다음 형태의 token을 만든다.
|
||||
|
||||
```text
|
||||
v1.<base64url(payload)>.<base64url(HMAC-SHA256(version + '.' + payload))>
|
||||
```
|
||||
|
||||
확인한 방어는 다음과 같다.
|
||||
|
||||
- signing key 최소 32 bytes
|
||||
- URL-safe Base64 / no padding
|
||||
- version까지 MAC input에 포함
|
||||
- token 전체 길이 4096-character cap
|
||||
- payload 2048-byte cap
|
||||
- presented MAC 32-byte exact length 확인
|
||||
- `MessageDigest.isEqual` constant-time comparison
|
||||
- MAC 검증 전에 application payload decoder를 호출하지 않음
|
||||
- oversized public input을 substring/decode/MAC allocation 전에 거부하려는 선행 check
|
||||
|
||||
기존 dedicated tests도 tampering, foreign key, unknown version, short key, oversized token, wrong-length MAC, oversized payload 등을 폭넓게 검증한다.
|
||||
|
||||
## SignedJsonCursorCodec 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c05" alt="코드베이스에서 SignedJsonCursorCodec 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SignedJsonCursorCodec 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 추정 helper가 padding을 가정한다
|
||||
|
||||
문제는 decoded payload size를 decode **전에** 추정하는 helper다. 이 함수는 "최대 decoded size"를 빠르게 계산하려는 의도로 commit `2f5d2fc`에서 hostile-input bounds와 함께 추가됐다. 그러나 codec은 **unpadded Base64URL**을 사용한다.
|
||||
|
||||
## encode와 decode의 허용 범위
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c05-diagram" alt="상한 크기의 payload가 base64url 인코드를 지나 크기 사전 추정 단계에서 decode 쪽 거부로 이어지는 경로" caption="encode와 decode의 허용 범위" zoom="false"
|
||||
:::
|
||||
|
||||
실제 self-round-trip probe 결과 현재 accepted encode domain과 accepted decode domain이 다르다. 이건 hostile token을 더 엄격히 거부하는 정도가 아니다. **codec의 자기 round-trip contract를 깨는 boundary defect**다. 실행 evidence는 `evidence/raw/035a-jpa-cursor-boundary-probe.java`와 `evidence/raw/035-jpa-cursor-boundary-probe.txt`에 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c25
|
||||
title: 문서 상수와 같은 상수를 assert하는 test를 피한 이유
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c25
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c25
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c25.svg
|
||||
- key: adapter-outbound-persistence-jpa-c25-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c25.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c25.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1643 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 문서 상수와 같은 상수를 assert하는 test를 피한 이유
|
||||
|
||||
`HibernateProviderPolicy`가 선언된 baseline 상수와 classpath에서 읽은 runtime version을 분리해, drift를 보고할 수 있게 한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`HibernateProviderPolicy`는 상수로 선언된 Stable provider baseline과 실제 classpath에서 읽은 runtime version을 구분한다. 이 설계가 필요한 이유는 repository가 과거 "7.4를 Stable baseline이라고 문서화하면서 실제 Spring Boot BOM은 7.1.x를 resolve"한 상태를 경험했기 때문이다.
|
||||
|
||||
## 선언 기준과 런타임 판본의 분리
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c25-diagram" alt="왼쪽에 정책 상수 baseline과 문서가 선언한 판본이 오른쪽에 org.hibernate.Version 조회와 BOM이 해석한 판본이 같은 축에 놓인다" caption="선언 기준과 런타임 판본의 분리" zoom="false"
|
||||
:::
|
||||
|
||||
## 두 값을 비교하는 지점
|
||||
|
||||
declared baseline은 policy constant이고, runtime provider는 `org.hibernate.Version`에서 읽는다. drift 여부는 `driftsFromDeclaredBaseline()`이 판정하며, app-bootstrap capability/report가 runtime value를 사용한다. 즉 "문서 상수와 같은 상수를 assert해서 green"인 self-fulfilling test는 피한다.
|
||||
|
||||
## HibernateProviderPolicy 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c25" alt="코드베이스에서 HibernateProviderPolicy 를 검색한 출력 25줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HibernateProviderPolicy 코드베이스 검색 — 25줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 sub-scope에서 도달성이 확실한 타입
|
||||
|
||||
outside-leaf production consumer가 명확히 존재하는 핵심 Hibernate type도 `HibernateProviderPolicy`다. app-bootstrap이 이를 composition/report에 사용한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c45
|
||||
title: V3와 V4는 registry revision을 올리지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c45
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c45
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c45.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c45.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L3166 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# V3와 V4는 registry revision을 올리지 않는다
|
||||
|
||||
Fileserver persistence는 opt-in production capability이고, migration V3·V4가 registry revision을 갱신하지 않는 차이가 뒤따르는 startup fail-open finding의 직접 원인이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
Fileserver persistence는 latent helper가 아니라 실제 opt-in production capability다. `PersistenceJpaRootAutoConfiguration`이 `FileserverJpaPersistenceConfig`를 import한다.
|
||||
|
||||
## PersistenceJpaRootAutoConfiguration 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c45" alt="코드베이스에서 PersistenceJpaRootAutoConfiguration 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PersistenceJpaRootAutoConfiguration 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 켜진 배포에서 무엇이 도달 가능해지나
|
||||
|
||||
`app.fileserver-platform.enabled=true`이면 Fileserver entity/repository/component scan이 열린다. `FileserverStorageConfiguration.fileserverSchemaActivation()`은 `JdbcOperations`가 있으면 startup에서 `requireActive()`를 호출한다. 따라서 schema activation, quota, cleanup, recovery adapter는 Fileserver capability가 켜진 배포에서 production-reachable하다.
|
||||
|
||||
## revision이 2에서 멈춘다
|
||||
|
||||
V1은 registry에 `jpa-fileserver-metadata-v1`, `feature_revision=1`, `INSTALLED_INACTIVE`를 기록하고, V2는 recovery schema를 추가한 뒤 revision을 2로 올린다. 이후 V3는 fenced cleanup lease column을, V4는 upload terminal lifecycle column을 추가하지만 registry revision은 더 이상 갱신하지 않는다. 이 차이는 아래 startup fail-open finding의 직접 원인이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c49
|
||||
title: 평문처럼 보이는 envelope를 DB constraint가 거부한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c49
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c49
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c49.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c49.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L3561 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 평문처럼 보이는 envelope를 DB constraint가 거부한다
|
||||
|
||||
request variable payload는 보호 collaborator를 필수로 받고, contact point는 네 조각으로 분리되며, V10이 같은 규칙을 스키마에 넣는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
request variable payload는 `NotificationPayloadProtection`을 필수 collaborator로 받아 보호된 envelope를 저장하고, contact point는 ciphertext/nonce/lookup HMAC/key id로 분리된다. V10은 plaintext-looking request envelope를 **DB constraint로도** 거부한다.
|
||||
|
||||
## NotificationPayloadProtection 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c49" alt="코드베이스에서 NotificationPayloadProtection 를 검색한 출력 37줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="NotificationPayloadProtection 코드베이스 검색 — 37줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이번 완독에서 이 경계 자체를 우회하는 production write path는 확인하지 못했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-mongo-c02
|
||||
title: 선언된 축이 컴파일되지 않으면 startup에서 거부된다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-mongo-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-mongo-c02
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L493 이다.
|
||||
module: adapter-outbound-persistence-mongo
|
||||
---
|
||||
|
||||
# 선언된 축이 컴파일되지 않으면 startup에서 거부된다
|
||||
|
||||
이 package의 나머지는 api manifest를 말이 아니라 코드로 만든다 — 조립 순서, startup 거부, 축 컴파일, 실제 등록된 변환기로 만든 guard, 정밀도 검사.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
P1과 별개로, 이 package의 나머지는 api manifest를 말이 아니라 코드로 만든다.
|
||||
|
||||
## 변환기 선택이 실행마다 달라지지 않는다
|
||||
|
||||
`MongoCustomConversionsFactory.converters(...)`가 변환기를 **명시적 List 순서로** 조립한다. 이유가 주석에 있다 — Spring의 conversion service는 첫 매칭 변환기를 쓰므로 `Set`이나 classpath 스캔에서 조립하면 JVM 실행마다 다른 변환기가 선택될 수 있다. `fingerprint(manifest)`가 manifest fingerprint에 변환기 클래스 이름을 이어 붙여 golden BSON snapshot이 비교할 identity를 만든다.
|
||||
|
||||
## 등록하지 않은 축은 startup에서 걸린다
|
||||
|
||||
같은 factory가 `requireEveryAxisImplemented(...)`로 `LOCAL_DATE_TIME_WITH_REGISTERED_CONVERTER`를 startup에서 거부한다. enum 상수 자신이 "selecting this without registering the named converter is a startup failure"라고 적어 둔 규칙을 실제로 집행하는 지점이다.
|
||||
|
||||
## LocalDateTimeMappingGuard 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-mongo-c02" alt="코드베이스에서 LocalDateTimeMappingGuard 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="LocalDateTimeMappingGuard 코드베이스 검색 — 14줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 선언만 있고 컴파일되지 않았던 축
|
||||
|
||||
`BigIntegerRepresentationConverters.forRepresentation(...)`은 manifest의 BigInteger 축을 세 변환기 쌍으로 컴파일한다. 주석이 과거 상태를 기록한다 — 이 축은 선언만 있고 컴파일되지 않아 `STRING`과 `DECIMAL128`이 동일한 document를 만들었고, 하나는 사전식으로 다른 하나는 수치로 정렬된다.
|
||||
|
||||
## guard를 실제 등록된 변환기로 만든다
|
||||
|
||||
`LocalDateTimeMappingGuard`는 `MongoMappingConfiguration`이 **실제 등록된 변환기**로 만든다. javadoc이 이전 결함을 적는다 — guard를 `withoutConverters()`로 만들고 manifest를 검증하게 해서, 명명된 변환기를 등록한 배포와 등록하지 않은 배포를 똑같이 거부했다.
|
||||
|
||||
## 조용한 반올림이 금액을 바꾸지 못한다
|
||||
|
||||
`BigDecimalToDecimal128Converter`는 driver 호출 전에 34 유효숫자·지수 범위를 검사한다. `Decimal128`은 초과 정밀도를 조용히 반올림하므로, 검사가 없으면 금액이 다른 값으로 저장되고 아무 오류도 나지 않는다.
|
||||
|
||||
## 저장된 문자열이 클래스를 고르지 못한다
|
||||
|
||||
`PolicyAwareMongoTypeMapper`는 alias에 점을 금지하고, 읽을 때 점의 유무로 "legacy class name"과 "alias"를 구분한다 — 그래서 미등록 alias가 class loading으로 fallback하는 일이 없다. `readType(source, basicType)`은 저장된 타입이 caller의 기대 타입과 호환되지 않으면 조용히 caller 타입으로 읽지 않고 schema 오류를 던진다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-mongo-c05
|
||||
title: 배선된 enforcer를 부르는 코드가 미배선이다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-mongo-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-mongo-c05
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L709 이다.
|
||||
module: adapter-outbound-persistence-mongo
|
||||
---
|
||||
|
||||
# 배선된 enforcer를 부르는 코드가 미배선이다
|
||||
|
||||
auto-configuration이 이 sub-scope에서 만드는 bean은 하나이고, 그 하나의 유일한 production 소비자가 미배선이라 아무도 호출하지 않는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
auto-configuration이 이 sub-scope에서 만드는 bean은 **`MongoBudgetEnforcer` 하나**다(`132-...` §8.1). `MongoQueryPolicy`·`PolicyAwareMongoQueryBuilder`·`MongoRegexPolicy`·`MongoBudgetPolicyRegistry`·`MongoKeysetCursorCodec`·`PolicyAwareMongoAggregationExecutor`는 bean도 아니고 `main` 안에 소비자도 없다(§8.1 세 번째 검색 exit=1).
|
||||
|
||||
## MongoBudgetEnforcer 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-mongo-c05" alt="코드베이스에서 MongoBudgetEnforcer 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoBudgetEnforcer 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 그 하나조차 짝이 없다
|
||||
|
||||
`MongoBudgetEnforcer`의 유일한 production 소비자는 `PolicyAwareMongoAggregationExecutor`인데 그것이 미배선이므로, 배선된 enforcer는 현재 아무도 호출하지 않는다. `MongoKeysetCursorCodec`은 32바이트 이상 서명 키를 요구하는데 그 키를 공급하는 production 코드가 없다 — 생성자 호출은 test 3곳뿐이다.
|
||||
|
||||
## 이것 자체는 결함이 아니다
|
||||
|
||||
이 leaf는 가짜 도메인을 두지 않고 collection profile·field descriptor·budget을 fork가 선언하도록 설계돼 있으며, CLAUDE.md가 "Real forks add their own document, repository, mapper"라고 명시한다.
|
||||
|
||||
## 그래도 기록하는 두 이유
|
||||
|
||||
(a) README의 D1/D2 표는 "typed query, mapping manifest, atomic update, optimistic revision"을 노출 계층의 내용으로 제시하는데, 그중 typed query 계열은 배선 없이 fork가 조립해야 한다는 사실이 그 표에 없다. (b) §41의 다음 항목이 그 조립 시점에만 문제가 된다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-mongo-c09
|
||||
title: @Configuration을 게이팅하면 게이트를 공급하는 것을 게이팅한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-mongo-c09
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-mongo-c09
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c09.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c09.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L1269 이다.
|
||||
module: adapter-outbound-persistence-mongo
|
||||
---
|
||||
|
||||
# @Configuration을 게이팅하면 게이트를 공급하는 것을 게이팅한다
|
||||
|
||||
advanced 분류 불변식이 문서가 아니라 코드로 서 있고, 제외 항목마다 근거가 붙어 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
세어 봤다(`137-...` §8.1b·§8.1c).
|
||||
|
||||
- `@MongoAdvancedEntryPoint` **7개**: `MongoChangeMessagingBridge`(CHANGE_STREAM), `MongoCsfleClientFactory`(CSFLE), `MongoQueryableEncryptionCollectionManager`(QUERYABLE_ENCRYPTION), `MongoGridFsMigrationJob`(GRIDFS_COMPATIBILITY), `MongoShardingAdminGateway`(SHARDING), `MongoTenantClientRegistry`·`MongoTenantMigrationCoordinator`(DATABASE_PER_TENANT).
|
||||
- `@MongoAdvancedPolicy` **11개**.
|
||||
- 어느 쪽도 아닌 구체 클래스 **1개**: `MongoAdvancedConfiguration`.
|
||||
|
||||
## MongoChangeMessagingBridge 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-mongo-c09" alt="코드베이스에서 MongoChangeMessagingBridge 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoChangeMessagingBridge 코드베이스 검색 — 16줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 그 하나가 누락이 아닌 이유
|
||||
|
||||
`MongoAdvancedRules.concreteClass()`가 `@Configuration`을 명시적으로 제외하며 이유를 적는다: "A `@Configuration` class is the package's composition root: it builds entry points through the guard rather than being one, and **gating it would gate the thing that supplies the guard**." interface·enum·record·익명·private 중첩·abstract도 같은 방식으로 제외되고 각각 근거가 붙어 있다. 즉 §83이 말하는 불변식은 문서가 아니라 코드로 서 있다. 이 leaf에서 "문서가 주장하고 코드가 지키지 않는다"를 여러 번 본 뒤라, 여기서는 그 반대가 성립한다는 것을 명시해 둘 가치가 있다.
|
||||
|
||||
## 던지기만 하는 메서드를 값으로 바꾼 두 수리
|
||||
|
||||
`MongoTimeSeriesCapabilityValidator`는 네 개의 던지기만 하는 메서드를 `supportFor(capability) → MongoTimeSeriesSupport`(지원 여부 + 이유)로 바꿨고, `MongoQueryableEncryptionProfile`은 세 개의 던지기만 하는 static factory를 `supportFor(MongoQueryShape) → MongoQueryShapeSupport`로 바꿨다. 근거도 동일하다 — "A factory that never returns is not an API: it cannot appear in working code, so its only reachable use is a test asserting that it throws, and the design-time question it was meant to answer is only answered by running it."
|
||||
|
||||
<!-- body:end -->
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: application-core-c10
|
||||
title: 임의 Object 대신 sealed 변수 대수를 쓴 이유
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:application-core-c10
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: application-core-c10
|
||||
file: ../../../final/evidence/rendered/application-core-c10.svg
|
||||
- key: application-core-c10-diagram
|
||||
file: ../../../final/assets/diagrams/application-core-c10.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/application-core-c10.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/03-application-core.md#L230 이다.
|
||||
module: application-core
|
||||
---
|
||||
|
||||
# 임의 Object 대신 sealed 변수 대수를 쓴 이유
|
||||
|
||||
가변·임의 변수가 serialization/fingerprint drift와 `toString` 충돌을 만들 수 있었던 것이 변경 근거이고, 그 뒤로 비밀 경계가 계약 바깥에 따로 서 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **legacy storage/notification compatibility surface의 제거 조건 추적**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
public contract는 arbitrary `Object`/`Map<String,Object>`를 허용하지 않고 sealed `NotificationVariable` algebra를 사용한다. 과거 mutable/arbitrary variable 때문에 serialization/fingerprint drift와 `toString` collision이 가능했던 것이 변경 근거다.
|
||||
|
||||
## 공개 계약과 비밀이 나뉘는 자리
|
||||
|
||||
:::evidence key="application-core-c10-diagram" alt="sealed 변수 대수와 고정된 템플릿 판본과 유계 수신자 수가 공개 알림 계약 안에 놓이고 임의 Object 변수와 연락처 원문이 바깥에 빗금으로 놓인다" caption="공개 계약과 비밀이 나뉘는 자리" zoom="false"
|
||||
:::
|
||||
|
||||
## 이 기록이 다루는 범위
|
||||
|
||||
:::evidence key="application-core-c10" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## vacuous pass가 남긴 regression guard
|
||||
|
||||
structural test는 public API에 arbitrary Object가 다시 들어오지 않는지 검사하며, 과거 잘못된 test root로 vacuous pass했던 문제도 regression guard로 남아 있다. `NotificationPlan`은 exact template version을 pin한다. recipient/metadata/variable count/depth가 bounded되어 있고, receipt는 "durable logical acceptance"이지 provider delivery를 의미하지 않는다.
|
||||
|
||||
## decrypt 실패를 빈 값으로 떨어뜨리지 않는다
|
||||
|
||||
contact point는 encrypted value + keyed fingerprint로 분리되고 protected contact rendering은 원문을 노출하지 않는다. template variable 자체에 reset token 같은 secret이 들어갈 수 있어 payload protection이 존재한다. contact lookup/provider request/callback fingerprint는 HMAC purpose를 분리해 동일 secret-purpose reuse를 피한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: grpc-policy-c01
|
||||
title: 재개 토큰 — 서명하고, 구분자를 봉인한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:grpc-policy-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-policy-c01
|
||||
file: ../../../final/evidence/rendered/grpc-policy-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-policy-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-policy.md#L73 이다.
|
||||
module: grpc-policy
|
||||
---
|
||||
|
||||
# 재개 토큰 — 서명하고, 구분자를 봉인한다
|
||||
|
||||
GrpcResumeToken 의 아홉 성분 각각이 왜 필요한지가 javadoc 에 있다 — 스냅숏 판본 없이는 사라진 뷰의 위치에서 재개하고, 만료 없이는 이력이 사라진 커서에서 재개하고, 필터 지문 없이는 남의 필터를 자기 위치에서 재개해 요청하지 않은 행을 받는다. 그리고 문자열 성분이 구분자를 담지 못하게 생성자가 거부한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcResumeToken` 의 아홉 성분 각각이 왜 필요한지가 javadoc 에 있다 — 스냅숏 판본 없이는 사라진 뷰의 위치에서 재개하고, 만료 없이는 이력이 사라진 커서에서 재개하고, 필터 지문 없이는 남의 필터를 자기 위치에서 재개해 요청하지 않은 행을 받는다.
|
||||
|
||||
## GrpcResumeToken 참조 위치
|
||||
|
||||
:::evidence key="grpc-policy-c01" alt="코드베이스에서 GrpcResumeToken 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcResumeToken 코드베이스 검색 — 19줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 구분자를 담지 못하게 생성자가 거부한다
|
||||
|
||||
문자열 성분이 구분자를 담으면 생성자에서 거부된다.
|
||||
|
||||
## 검증이 지키는 세 성질
|
||||
|
||||
`GrpcResumeTokenCodec` 의 검증은 상수 시간 비교(`MessageDigest.isEqual`), 알 수 없는 키 식별자 거부, 세 실패의 구분 불가를 지킨다.
|
||||
|
||||
<!-- body:end -->
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-admin-api-c03
|
||||
title: 서명 대상 객체가 존재한다는 것이 4-eyes 통과를 뜻한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-admin-api-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-c03
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-c03.svg
|
||||
- key: messaging-admin-api-c03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-admin-api-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L185 이다.
|
||||
module: messaging-admin-api
|
||||
---
|
||||
|
||||
# 서명 대상 객체가 존재한다는 것이 4-eyes 통과를 뜻한다
|
||||
|
||||
8개 필드. 그중 requestedBy 가 생성자에서 4-eyes 를 강제한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ApprovalGrant` 는 8개 필드다. 그중 `requestedBy` 가 생성자에서 4-eyes 를 강제한다. "우회할 수 없는 자리에 표현했다" — 검사기가 아니라 **record 생성자**에 두었으므로, 서명 대상 객체가 존재하는 것 자체가 4-eyes 통과를 뜻한다.
|
||||
|
||||
## canonicalForm 이 길이 접두 인코딩인 것
|
||||
|
||||
버전 접두 `"v1"` 이 앞에 있어 형식 교체 여지를 남긴 것도 의도적으로 보인다. 이 규칙이 계획 다이제스트 쪽에는 적용되지 않았다 — §12.3(b).
|
||||
|
||||
## 승인 검증이 도는 순서
|
||||
|
||||
:::evidence key="messaging-admin-api-c03-diagram" alt="다이제스트 대조에서 윈도우로 일치가 건너가고 윈도우에서 서명 파싱으로 기간 안이 건너가고 서명 파싱에서 HMAC 으로 형식 통과가 건너간다" caption="승인 검증이 도는 순서" zoom="false"
|
||||
:::
|
||||
|
||||
**다이제스트 대조 → 윈도우 → 16진 파싱 → HMAC**. 다이제스트를 먼저 보는 이유가 주석에 있다.
|
||||
|
||||
## ApprovedReplayPlan 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-c03" alt="코드베이스에서 ApprovedReplayPlan 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApprovedReplayPlan 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 검증하는 쪽은 반드시 서명할 수도 있다
|
||||
|
||||
`sign(...)` 이 같은 클래스에 public 으로 있고, javadoc 이 그 위험을 스스로 명시한다. 이 문장과 대칭키 선택이 만드는 구조적 결과가 §17 의 한 항목이다.
|
||||
|
||||
## javadoc 이 세지 않는 두 검사
|
||||
|
||||
실제 분기는 여섯이다. 5번에는 별도 인라인 주석이 있어 의도된 검사임이 분명하다. "검사는 있는데 클래스 javadoc 이 세지 않는" 두 항목이, 동시에 **어떤 테스트에도 도달하지 않는** 두 항목이다(`EVD-303`, §12.1).
|
||||
|
||||
## dry run 을 항상 허용하는 근거
|
||||
|
||||
"the way to make operators plan before they act is to make planning free". 이것은 보안 완화가 아니라 행동 설계다.
|
||||
|
||||
## 토폴로지를 두 번 보는 이유
|
||||
|
||||
`ApprovedReplayPlan` 생성자(`:19-52`)와 `requireExecutable(now, currentTopologyVersion)`(`:54-85`)이 토폴로지를 **두 번** 본다. 승인이 서명된 토폴로지와 계획이 계산된 토폴로지가 다를 수 있으므로 둘 다 현재와 대조한다.
|
||||
|
||||
## 진행처럼 보이는 루프
|
||||
|
||||
`ApprovedRedrivePlan` 은 같은 네 검사에 더해 `loopAcknowledged` 를 별도 필드로 갖고, `requireExecutable` 의 마지막 분기가 그것을 강제한다. "진행처럼 보이는 루프" 는 이 리프에서 반복되는 관점이다 — 대시보드에서 옳아 보이는 실패를 타입으로 막는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-cloudevents-c01
|
||||
title: 싣고 쓰지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-cloudevents-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-cloudevents-c01
|
||||
file: ../../../final/evidence/rendered/messaging-cloudevents-c01.svg
|
||||
- key: messaging-cloudevents-c01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-cloudevents-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-cloudevents-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-cloudevents.md#L48 이다.
|
||||
module: messaging-cloudevents
|
||||
---
|
||||
|
||||
# 싣고 쓰지 않는다
|
||||
|
||||
플랫폼 봉투와 CloudEvents 1.0.2 사이의 양방향 매퍼. 적용 범위를 인터페이스 javadoc이 한정한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
플랫폼 봉투와 CloudEvents 1.0.2 사이의 양방향 매퍼. 적용 범위를 인터페이스 javadoc이 한정한다.
|
||||
|
||||
## 싣기와 쓰기가 갈리는 자리
|
||||
|
||||
:::evidence key="messaging-cloudevents-c01-diagram" alt="messaging-cloudevents 적재만 배포 아티팩트 안에 놓이고 이 리프 타입의 호출자가 바깥에 빗금으로 놓인다" caption="싣기와 쓰기가 갈리는 자리" zoom="false"
|
||||
:::
|
||||
|
||||
`runtime_memberships`가 `["app-bootstrap"]`이다 — 배포 아티팩트가 싣는다. 그런데 소비자가 하나도 없다(§12.1). Avro·Protobuf는 "싣지도 않고 쓰지도 않는다"로 정합하지만, 이 leaf는 **싣고 쓰지 않는다.**
|
||||
|
||||
## CloudEvent 참조 위치
|
||||
|
||||
:::evidence key="messaging-cloudevents-c01" alt="코드베이스에서 CloudEvent 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEvent 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 좌표를 나눠 지킨 의존성 선언
|
||||
|
||||
의존성 선언에 이 저장소에서 가장 자세한 근거 주석이 붙어 있다. **둘의 scope가 다른 것이 정확하다** — `cloudevents-api`(`CloudEvent`, `CloudEventData`)는 public 시그니처에 나오므로 `api`, `cloudevents-core`(`CloudEventBuilder`, `BytesCloudEventData`)는 구현 안에서만 쓰이므로 `implementation`이다. `src/messaging/CLAUDE.md:40-43`의 게이트가 잡는 구분이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-cloudevents-c02
|
||||
title: 런타임 classpath에는 오르고 어느 @Bean도 만들지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-cloudevents-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-cloudevents-c02
|
||||
file: ../../../final/evidence/rendered/messaging-cloudevents-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-cloudevents-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-cloudevents.md#L80 이다.
|
||||
module: messaging-cloudevents
|
||||
---
|
||||
|
||||
# 런타임 classpath에는 오르고 어느 @Bean도 만들지 않는다
|
||||
|
||||
들어오는 것: messaging-core-api(api), messaging-schema-api(api), cloudevents-api:4.0.1(api), cloudevents-core:4.0.1(implementation). 나가는 것: messaging-spring-boot-starter의 allowed_dependencies에 포함된다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
들어오는 것은 `messaging-core-api`(api), `messaging-schema-api`(api), `cloudevents-api:4.0.1`(api), `cloudevents-core:4.0.1`(implementation)이다. 나가는 것은 `messaging-spring-boot-starter`의 `allowed_dependencies`에 포함된다.
|
||||
|
||||
## CloudEventMapper 참조 위치
|
||||
|
||||
:::evidence key="messaging-cloudevents-c02" alt="코드베이스에서 CloudEventMapper 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEventMapper 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## classpath에 오르는 경로
|
||||
|
||||
`app-bootstrap` → starter → 이 leaf 경로로 런타임 classpath에 오른다.
|
||||
|
||||
## 그런데 아무도 부르지 않는다
|
||||
|
||||
starter의 어느 `@Bean`도 `CloudEventMapper`를 만들지 않고, 어느 클래스도 import하지 않는다(§12.1). bean 없음(Spring 주석 0개).
|
||||
|
||||
<!-- body:end -->
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-cloudevents-c03
|
||||
title: 확장 이름이 봉투 필드명과 다른 이유
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-cloudevents-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-cloudevents-c03
|
||||
file: ../../../final/evidence/rendered/messaging-cloudevents-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-cloudevents-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-cloudevents.md#L92 이다.
|
||||
module: messaging-cloudevents
|
||||
---
|
||||
|
||||
# 확장 이름이 봉투 필드명과 다른 이유
|
||||
|
||||
CloudEventExtensions의 javadoc이 이름이 봉투 필드명과 다른 이유를 적는다 — "CloudEvents requires extension names to be lowercase alphanumeric".
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 leaf의 타입 구성은 다음과 같다.
|
||||
|
||||
```text
|
||||
CloudEventMapper (interface)
|
||||
├── toCloudEvent(MessageEnvelope<?>, URI) → CloudEvent
|
||||
└── fromCloudEvent(CloudEvent) → MessageEnvelope<EncodedMessage>
|
||||
|
||||
DefaultCloudEventMapper (구현)
|
||||
├── toCloudEvent : occurredAt 필수, payload는 이미 인코딩된 것만
|
||||
├── fromCloudEvent : time 필수, schemaversion 확장 필수
|
||||
├── stringExtension / intExtension
|
||||
└── producerFrom(URI) : 마지막 세그먼트를 producer id로
|
||||
|
||||
CloudEventExtensions (상수 4개)
|
||||
correlationid · causationid · schemaversion · tenantcontext
|
||||
```
|
||||
|
||||
## CloudEventExtensions 참조 위치
|
||||
|
||||
:::evidence key="messaging-cloudevents-c03" alt="코드베이스에서 CloudEventExtensions 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CloudEventExtensions 코드베이스 검색 — 14줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이름이 소문자 영숫자인 이유
|
||||
|
||||
`CloudEventExtensions`의 javadoc이 이름이 봉투 필드명과 다른 이유를 적는다 — "CloudEvents requires extension names to be lowercase alphanumeric".
|
||||
|
||||
<!-- body:end -->
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-cloudevents-c05
|
||||
title: 두 방향 모두 시각이 없으면 거절한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-cloudevents-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-cloudevents-c05
|
||||
file: ../../../final/evidence/rendered/messaging-cloudevents-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-cloudevents-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-cloudevents.md#L290 이다.
|
||||
module: messaging-cloudevents
|
||||
---
|
||||
|
||||
# 두 방향 모두 시각이 없으면 거절한다
|
||||
|
||||
나가는 방향: occurredAt 확인(없으면 거절) → CloudEventBuilder.v1()에 id·source·type·time·datacontenttype·schemaversion → 선택 확장 셋 → payload 종류 판정 → dataschema(있을 때) → build() 들어오는 방향: time 확인(없으면 거절) → datacontenttype(기본 application/json) → data(없으면 빈 배열) → MessageId·MessageType·SchemaVersion·ProducerId·확장 셋 → MessageEnvelope 조립
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
**나가는 방향:** `occurredAt` 확인(없으면 거절) → `CloudEventBuilder.v1()`에 id·source·type·time·datacontenttype·schemaversion → 선택 확장 셋 → payload 종류 판정 → `dataschema`(있을 때) → `build()`
|
||||
|
||||
**들어오는 방향:** `time` 확인(없으면 거절) → `datacontenttype`(기본 `application/json`) → `data`(없으면 빈 배열) → `MessageId`·`MessageType`·`SchemaVersion`·`ProducerId`·확장 셋 → `MessageEnvelope` 조립
|
||||
|
||||
## MessageId 참조 위치
|
||||
|
||||
:::evidence key="messaging-cloudevents-c05" alt="코드베이스에서 MessageId 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageId 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-cloudevents-c06
|
||||
title: 외부 데이터가 들어오는 유일한 진입점의 실패가 분류되지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-cloudevents-c06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-cloudevents-c06
|
||||
file: ../../../final/evidence/rendered/messaging-cloudevents-c06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-cloudevents-c06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-cloudevents.md#L298 이다.
|
||||
module: messaging-cloudevents
|
||||
---
|
||||
|
||||
# 외부 데이터가 들어오는 유일한 진입점의 실패가 분류되지 않는다
|
||||
|
||||
분류된 실패 넷, 분류되지 않은 실패 여덟. 매퍼가 직접 던지는 것은 전부 MessageValidationException이지만, 값 객체 생성자에 위임한 검증은 전부 raw IllegalArgumentException이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
**분류된 실패 넷, 분류되지 않은 실패 여덟.** 매퍼가 직접 던지는 것은 전부 `MessageValidationException`이지만, 값 객체 생성자에 위임한 검증은 전부 raw `IllegalArgumentException`이다.
|
||||
|
||||
## MessageValidationException 참조 위치
|
||||
|
||||
:::evidence key="messaging-cloudevents-c06" alt="코드베이스에서 MessageValidationException 를 검색한 출력 9줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageValidationException 코드베이스 검색 — 9줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 하필 그 진입점이다
|
||||
|
||||
`fromCloudEvent`는 **외부에서 온 데이터**를 다루는 유일한 진입점인데, 그 진입점의 실패 대부분이 플랫폼 실패 어휘 밖이다.
|
||||
|
||||
## 설계 의도와 어긋나는 지점
|
||||
|
||||
`messaging-core-api`의 `FailureDescriptor` 설계 전체가 "예외 클래스로 분기하지 말고 선언된 분류로 판단하라"였다. 이 경로는 그 분류를 만들지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-core-api-c02
|
||||
title: 의존이 하나도 없고 나머지 24개 전부가 이것을 의존한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-core-api-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-core-api-c02
|
||||
file: ../../../final/evidence/rendered/messaging-core-api-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-core-api-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-core-api.md#L101 이다.
|
||||
module: messaging-core-api
|
||||
---
|
||||
|
||||
# 의존이 하나도 없고 나머지 24개 전부가 이것을 의존한다
|
||||
|
||||
들어오는 것: 없음. registry allowed_dependencies: []이고 build.gradle에 선언이 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
들어오는 것은 없다 — registry `allowed_dependencies: []`이고 `build.gradle`에 선언이 없다.
|
||||
|
||||
## 이 기록이 다루는 범위
|
||||
|
||||
:::evidence key="messaging-core-api-c02" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 나가는 쪽은 messaging family 전부다
|
||||
|
||||
`messaging-schema-api`, `messaging-schema-json`, `messaging-schema-avro`, `messaging-schema-protobuf`, `messaging-cloudevents`, `messaging-policy`, `messaging-transport-spi`, `messaging-runtime-core`, `messaging-observability`, `messaging-security`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-reliability-api`, `messaging-outbox-jdbc-postgresql`, `messaging-inbox-jdbc-postgresql`, `messaging-claim-check`, `messaging-admin-api`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit` — 나머지 **24개 전부**.
|
||||
|
||||
## 편입이 전이로 일어난다
|
||||
|
||||
`runtime_memberships: ["app-bootstrap"]`이고, 그 편입은 직접 선언이 아니라 **전이(transitive)**로 일어난다. `src/app-bootstrap/build.gradle:87`이 선언하는 것은 starter 하나이고, starter의 `allowed_dependencies`가 17개 leaf를 끌고 오며 그 closure에 `messaging-core-api`가 있다. 즉 **배포 아티팩트가 이 leaf를 싣는다.** 실행 여부는 별개이고 master switch `app.messaging.enabled`(기본 `false`)가 결정한다(`src/messaging/CLAUDE.md:56-57`).
|
||||
|
||||
## 비교할 sibling bean이 없다
|
||||
|
||||
이 leaf 자체는 bean을 하나도 만들지 않는다. Spring stereotype·`@Bean`·`@Conditional`·`@Profile` 주석이 leaf 전체에 0개다(`evidence/raw/269` §F, `git grep` exit=1). 따라서 §12.2의 conditional sibling 비교는 이 leaf에 **적용 대상이 없다**.
|
||||
|
||||
<!-- body:end -->
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-core-api-c05
|
||||
title: 코드가 실제로 도는 네 지점
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-core-api-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-core-api-c05
|
||||
file: ../../../final/evidence/rendered/messaging-core-api-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-core-api-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-core-api.md#L406 이다.
|
||||
module: messaging-core-api
|
||||
---
|
||||
|
||||
# 코드가 실제로 도는 네 지점
|
||||
|
||||
이 leaf에는 실행 경로가 거의 없다. 실제로 코드가 도는 지점은 넷이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 leaf에는 실행 경로가 거의 없다. 실제로 코드가 도는 지점은 넷이다.
|
||||
|
||||
## 이 기록이 다루는 범위
|
||||
|
||||
:::evidence key="messaging-core-api-c05" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 네 지점
|
||||
|
||||
1. **봉투 생성** — `new MessageEnvelope<>(...)` → 14개 non-null 검사 + partitionKey/orderingKey wire 검사
|
||||
2. **헤더 생성** — `MessageHeaders.application/platform(Map)` → 개수(≤64) → 이름별 예약/자격증명/중복 검사 → 총 바이트(≤32,768)
|
||||
3. **식별자 생성** — `MessageId.newId()` → `UuidV7.next()` → `AtomicLong.updateAndGet(advance)`
|
||||
4. **결과 조립** — `new PublishResult(...)` / `new SettlementResult(...)` → 조합 검증
|
||||
|
||||
## 나머지가 있는 곳
|
||||
|
||||
나머지는 전부 인터페이스 선언이고, 구현은 `messaging-runtime-core`·`messaging-kafka`·`messaging-rabbit` 등 다른 leaf가 소유한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-api-c02
|
||||
title: Spring 주석 0개인 leaf가 배포 아티팩트에 실린다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-api-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-api-c02
|
||||
file: ../../../final/evidence/rendered/messaging-schema-api-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-api-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-api.md#L79 이다.
|
||||
module: messaging-schema-api
|
||||
---
|
||||
|
||||
# Spring 주석 0개인 leaf가 배포 아티팩트에 실린다
|
||||
|
||||
들어오는 것: messaging-core-api(api 노출). 나가는 것: messaging-schema-json, messaging-schema-avro, messaging-schema-protobuf, messaging-cloudevents, messaging-policy, messaging-transport-spi, messaging-runtime-core, messaging-kafka, messaging-rabbit, messaging-pulsar-experimental, messaging-nats-experimental, messaging-spring-boot-starter, messaging-testkit.
|
||||
|
||||
## 관계
|
||||
|
||||
- **port 계약은 동시성 요구를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **도달성 판정은 단어가 아니라 import로 확인한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
들어오는 것은 `messaging-core-api`(api 노출)다. 나가는 것은 `messaging-schema-json`, `messaging-schema-avro`, `messaging-schema-protobuf`, `messaging-cloudevents`, `messaging-policy`, `messaging-transport-spi`, `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`, `messaging-testkit`이다.
|
||||
|
||||
## 이 기록이 다루는 범위
|
||||
|
||||
:::evidence key="messaging-schema-api-c02" alt="코드베이스에서 파일 목록을 만든 출력 10줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 편입 경로가 core-api 와 같다
|
||||
|
||||
`app-bootstrap`이 `messaging-spring-boot-starter`를 선언하고 그 closure가 이 leaf를 끌어온다. 이 leaf는 bean을 만들지 않는다 — Spring 주석 0개.
|
||||
|
||||
<!-- body:end -->
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-api-c03
|
||||
title: 아무것도 실패하지 않고 잘못된 성공이 나온다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-api-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-api-c03
|
||||
file: ../../../final/evidence/rendered/messaging-schema-api-c03.svg
|
||||
- key: messaging-schema-api-c03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-schema-api-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-api-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-api.md#L113 이다.
|
||||
module: messaging-schema-api
|
||||
---
|
||||
|
||||
# 아무것도 실패하지 않고 잘못된 성공이 나온다
|
||||
|
||||
이 leaf에서 가장 밀도 높은 javadoc이다. 핵심은 "Nothing failed"다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **port 계약은 동시성 요구를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **도달성 판정은 단어가 아니라 import로 확인한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 leaf에서 가장 밀도 높은 javadoc이다. 타입만으로 키를 잡으면 실패가 발생하지 않고 **잘못된 성공**이 발생한다. 그리고 그 결과에는 등록된 적 없는 버전 라벨이 붙어 하위 감사 기록까지 오염된다.
|
||||
|
||||
## 타입만 키로 잡은 경로
|
||||
|
||||
:::evidence key="messaging-schema-api-c03-diagram" alt="미등록 버전 라벨이 타입만으로 조회와 이전 파서로 디코딩을 지나 잘못된 성공으로 이어진다" caption="타입만 키로 잡은 경로" zoom="false"
|
||||
:::
|
||||
|
||||
## 세 codec이 두 실패를 구분한다
|
||||
|
||||
`JacksonMessageCodec.requireRegistered`, `AvroMessageCodec.schemaFor`, `ProtobufMessageCodec.requireRegistered`가 모두 "타입은 아는데 버전을 모른다"와 "타입 자체를 모른다"를 **다른 에러 코드**로 구분한다(`SCHEMA_VERSION_NOT_REGISTERED` vs `UNKNOWN_MESSAGE_TYPE`). 그 구분이 있어야 운영자가 "등록을 빠뜨렸다"와 "오타다"를 나눌 수 있다.
|
||||
|
||||
## JacksonMessageCodec 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-api-c03" alt="코드베이스에서 JacksonMessageCodec 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JacksonMessageCodec 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 한도를 미리 잡지 않는다
|
||||
|
||||
`new ByteArrayOutputStream(Math.min(maxBytes, 8_192))` — 주석: "a 1 GiB bound must not pre-allocate 1 GiB."
|
||||
|
||||
## codec 자기 에러 코드를 그대로 던진다
|
||||
|
||||
`errorCode`가 생성자 인자다. 그래서 Avro는 `AVRO_PAYLOAD_TOO_LARGE`, JSON은 `PAYLOAD_TOO_LARGE`가 나온다. 테스트가 이 성질을 직접 단언한다(`BoundedByteSinkTest.java:69-77`, `as("the sink reports the codec's own code, not a generic one")`).
|
||||
|
||||
## requireFits 는 예산을 소비하지 않는다
|
||||
|
||||
Protobuf는 직렬화 크기를 미리 알므로 첫 바이트 전에 거절할 수 있다. 그 뒤의 쓰기도 여전히 경계 안이다 — 주석: "this is a cheaper refusal, not a replacement for the bound." `refuseIfBeyondLimit`가 `size > maxBytes - written`으로 비교하는 것도 의도적이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-api-c04
|
||||
title: 진화 검사 경로는 이 저장소에서 실행되지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-api-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-api-c04
|
||||
file: ../../../final/evidence/rendered/messaging-schema-api-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-api-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-api.md#L255 이다.
|
||||
module: messaging-schema-api
|
||||
---
|
||||
|
||||
# 진화 검사 경로는 이 저장소에서 실행되지 않는다
|
||||
|
||||
1. 경계 있는 인코딩 — codec이 BoundedByteSink.of(maxBytes, code)를 만들고 → 포맷 라이브러리가 sink에 쓰고 → 한도를 넘는 write에서 MessageTooLargeException → 아니면 sink.toByteArray()로 EncodedMessage 조립 2.
|
||||
|
||||
## 관계
|
||||
|
||||
- **port 계약은 동시성 요구를 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **도달성 판정은 단어가 아니라 import로 확인한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
세 경로가 있다.
|
||||
|
||||
1. **경계 있는 인코딩** — codec이 `BoundedByteSink.of(maxBytes, code)`를 만들고 → 포맷 라이브러리가 sink에 쓰고 → 한도를 넘는 write에서 `MessageTooLargeException` → 아니면 `sink.toByteArray()`로 `EncodedMessage` 조립
|
||||
2. **계약 조회** — `new MessageContractKey(type, version)` → registry lookup → 미스면 "타입 미등록" vs "버전 미등록" 구분
|
||||
3. **진화 검사** — `registry.compatibilityOf(subject)` → `versionsToCheck` → (포맷별 게이트가 실제 비교)
|
||||
|
||||
## MessageTooLargeException 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-api-c04" alt="코드베이스에서 MessageTooLargeException 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageTooLargeException 코드베이스 검색 — 13줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 셋째만 실행되지 않는다
|
||||
|
||||
3번은 이 저장소에서 실행되지 않는다(§12.1).
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-avro-c01
|
||||
title: 형제 leaf가 implementation이고 이쪽이 api인 것이 규칙의 증거다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-avro-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-avro-c01
|
||||
file: ../../../final/evidence/rendered/messaging-schema-avro-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-avro-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L49 이다.
|
||||
module: messaging-schema-avro
|
||||
---
|
||||
|
||||
# 형제 leaf가 implementation이고 이쪽이 api인 것이 규칙의 증거다
|
||||
|
||||
선택적(optional) Avro codec. Stable이 아니고 registry membership이 비어 있다 — build-only / incubating이며, docs/messaging/support-matrix.md의 등급과는 다른 축이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
선택적(optional) Avro codec. Stable이 아니고 registry membership이 비어 있다 — **build-only / incubating**이며, `docs/messaging/support-matrix.md`의 등급과는 다른 축이다.
|
||||
|
||||
## 이 기록이 다루는 범위
|
||||
|
||||
:::evidence key="messaging-schema-avro-c01" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## Avro를 api로 선언한 근거
|
||||
|
||||
build.gradle 주석에 있다. `src/messaging/CLAUDE.md:40-43`이 기술하는 게이트 — public/protected 시그니처에 나오는 vendor 라이브러리가 `api`로 선언됐는지 대조 — 를 이 leaf가 통과한다. 형제 `messaging-schema-json`은 Jackson 타입이 시그니처에 없으므로 `implementation`이고, 그 판정 차이가 규칙이 실제로 작동한다는 증거다.
|
||||
|
||||
## 클래스 둘의 실행 시점이 다르다
|
||||
|
||||
게이트의 javadoc이 그 이유를 적는다 — "By the time a producer has published one incompatible record, the damage is durable: the record sits in a retained log that every current and future consumer must be able to read."
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-avro-c02
|
||||
title: 소비자 없음과 membership 없음이 일치한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-avro-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-avro-c02
|
||||
file: ../../../final/evidence/rendered/messaging-schema-avro-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-avro-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L75 이다.
|
||||
module: messaging-schema-avro
|
||||
---
|
||||
|
||||
# 소비자 없음과 membership 없음이 일치한다
|
||||
|
||||
들어오는 것: messaging-core-api(api), messaging-schema-api(api), avro:1.12.0(api). 나가는 것: 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
들어오는 것은 `messaging-core-api`(api), `messaging-schema-api`(api), `avro:1.12.0`(api)다. 나가는 것은 **없다** — 어떤 leaf의 `allowed_dependencies`에도 `messaging-schema-avro`가 없고, `messaging-spring-boot-starter`의 17개 의존 목록에도 없다.
|
||||
|
||||
## 이 기록이 다루는 범위
|
||||
|
||||
:::evidence key="messaging-schema-avro-c02" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 런타임 배선 없음
|
||||
|
||||
`runtime_memberships: []`이므로 배포 아티팩트에 실리지 않는다. bean도 없다(Spring 주석 0개).
|
||||
|
||||
## 정합적인 incubating 상태
|
||||
|
||||
소비자 없음과 membership 없음이 일치한다. `messaging-cloudevents`와 대비된다 — 그쪽은 membership이 있고 소비자가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-avro-c03
|
||||
title: 틀린 스키마로 디코딩해도 실패하지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-avro-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-avro-c03
|
||||
file: ../../../final/evidence/rendered/messaging-schema-avro-c03.svg
|
||||
- key: messaging-schema-avro-c03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-schema-avro-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-avro-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L106 이다.
|
||||
module: messaging-schema-avro
|
||||
---
|
||||
|
||||
# 틀린 스키마로 디코딩해도 실패하지 않는다
|
||||
|
||||
"does not fail — it produces plausible garbage"가 이 leaf의 모든 방어의 전제다. JSON이나 Protobuf와 달리 Avro는 잘못된 스키마로 디코딩해도 예외를 던지지 않는 경우가 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
"does not fail — it produces plausible garbage"가 이 leaf의 모든 방어의 전제다. JSON이나 Protobuf와 달리 Avro는 잘못된 스키마로 디코딩해도 예외를 던지지 않는 경우가 있다. single-object encoding에 헤더를 붙이지 않는 것도 명시적 결정이다 — "The framing that would carry a schema fingerprint belongs to the transport headers, where the platform already carries schema identity for every format, rather than being duplicated inside the Avro payload for this one format."
|
||||
|
||||
## 얕은 복사가 만든 구멍
|
||||
|
||||
:::evidence key="messaging-schema-avro-c03-diagram" alt="중첩 맵 쪽에 바깥 맵만 복사와 안쪽 맵은 호출자 소유가 빗금으로 놓이고 평탄화된 키 쪽에 두 레벨 모두 복사와 버전이 키의 일부가 놓인다" caption="얕은 복사가 만든 구멍" zoom="false"
|
||||
:::
|
||||
|
||||
생성자가 받는 것은 중첩 맵 `Map<MessageType, Map<SchemaVersion, Schema>>`이고, `Map.copyOf`는 **바깥 레벨만** 복사한다 — 안쪽 맵은 호출자 객체로 남아, 참조를 쥔 호출자가 생성 후에 버전을 추가·교체·제거하면 codec이 조용히 그것으로 인코딩하기 시작했다. `(type, version)` 키로 평탄화하면 두 레벨이 모두 복사되고 버전이 조회 identity의 일부가 된다. 이 결함이 위험했던 이유는 위와 곱해진다 — 스키마가 바뀌어도 디코딩이 실패하지 않고 그럴듯한 쓰레기를 낸다.
|
||||
|
||||
## AvroRegistryBoundsTest 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-avro-c03" alt="코드베이스에서 AvroRegistryBoundsTest 를 검색한 출력 1줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AvroRegistryBoundsTest 코드베이스 검색 — 1줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
`mutatingTheCallersMapAfterConstructionChangesNothing`이 세 가지를 한 번에 확인한다 — 생성 후 추가한 버전은 미등록, 생성 후 추가한 타입도 미등록, 원래 등록한 스키마는 그대로. 평탄화가 `MessageContractKey`(schema-api)를 키로 쓰므로 §4.5의 2단 에러 구분도 자연히 따라온다.
|
||||
|
||||
## direct encoder 한 줄에 방어가 걸려 있다
|
||||
|
||||
`BoundedByteSink`(schema-api)의 경계가 실제로 작동하려면 인코더가 증분적으로 써야 한다. `EncoderFactory.get().binaryEncoder(...)`는 버퍼링하므로 sink가 첫 write를 보기 전에 큰 레코드가 이미 할당된다. 즉 **schema-api의 방어가 이 한 줄에 의존한다.**
|
||||
|
||||
## 인코딩 전 검사 둘
|
||||
|
||||
payload가 `GenericRecord`인가 → `AVRO_PAYLOAD_NOT_A_RECORD`. `schema.equals(record.getSchema())`인가 → `AVRO_SCHEMA_MISMATCH`. 두 번째는 테스트가 이유를 적는다 — `as("encoding v2 data under the v1 version would produce bytes nothing can decode")`.
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-avro-c04
|
||||
title: 쓰기 스키마와 읽기 스키마를 둘 다 넘기는 경로
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-avro-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-avro-c04
|
||||
file: ../../../final/evidence/rendered/messaging-schema-avro-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-avro-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L254 이다.
|
||||
module: messaging-schema-avro
|
||||
---
|
||||
|
||||
# 쓰기 스키마와 읽기 스키마를 둘 다 넘기는 경로
|
||||
|
||||
encode: schemaFor → GenericRecord 확인 → 스키마 동일성 확인 → BoundedByteSink + direct encoder → writer.write + flush → EncodedMessage(bytes, AVRO, SchemaReference) decode(동일 버전): requireWithinLimit → schemaFor → 대상 타입이 GenericRecord 계열인지 → boundedReader(writer, writer) → reader.read decodeEvolved: requireWithinLimit → schemaFor(writer) + schemaFor(reader) → boundedReader(writer, reader) → reader.read CI 게이트: check(candidate, history, mode) → 모드에 따라 비교 대상 선정 → 방향별 checkReaderWriterCompatibility
|
||||
|
||||
## 관계
|
||||
|
||||
- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
네 경로가 있다.
|
||||
|
||||
**encode:** `schemaFor` → `GenericRecord` 확인 → 스키마 동일성 확인 → `BoundedByteSink` + direct encoder → `writer.write` + `flush` → `EncodedMessage(bytes, AVRO, SchemaReference)`
|
||||
|
||||
**decode(동일 버전):** `requireWithinLimit` → `schemaFor` → 대상 타입이 `GenericRecord` 계열인지 → `boundedReader(writer, writer)` → `reader.read`
|
||||
|
||||
**decodeEvolved:** `requireWithinLimit` → `schemaFor(writer)` + `schemaFor(reader)` → `boundedReader(writer, reader)` → `reader.read`
|
||||
|
||||
**CI 게이트:** `check(candidate, history, mode)` → 모드에 따라 비교 대상 선정 → 방향별 `checkReaderWriterCompatibility`
|
||||
|
||||
## GenericRecord 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-avro-c04" alt="코드베이스에서 GenericRecord 를 검색한 출력 29줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GenericRecord 코드베이스 검색 — 29줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-avro-c05
|
||||
title: 크기 실패가 인코딩 실패로 접히지 않게 하는 재던지기
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-avro-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-avro-c05
|
||||
file: ../../../final/evidence/rendered/messaging-schema-avro-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-avro-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-avro.md#L266 이다.
|
||||
module: messaging-schema-avro
|
||||
---
|
||||
|
||||
# 크기 실패가 인코딩 실패로 접히지 않게 하는 재던지기
|
||||
|
||||
예외 재던지기 패턴이 세 곳에 반복된다. BoundedByteSink가 던지는 MessageTooLargeException은 RuntimeException이므로 catch에 걸린다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
**예외 재던지기 패턴이 세 곳에 반복된다.** `BoundedByteSink`가 던지는 `MessageTooLargeException`은 `RuntimeException`이므로 catch에 걸린다. 그것을 그대로 통과시키지 않으면 크기 실패가 인코딩 실패로 접힌다 — JSON codec의 `unwrapTooLarge`와 같은 문제를 다른 방식(원인 사슬 탐색이 아니라 즉시 `instanceof`)으로 푼다. §12.3.
|
||||
|
||||
## BoundedByteSink 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-avro-c05" alt="코드베이스에서 BoundedByteSink 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BoundedByteSink 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 방어를 우회하는 값이 존재하지만 읽기 실패로 끝난다
|
||||
|
||||
`AvroHostileInputTest.aCountBeyondIntRangeFailsWhileReadingRatherThanWhileReserving`가 흥미로운 경계를 잡는다 — 2³²을 주장하면 int로 잘려 무해한 값이 되고, 그 다음 읽기가 입력 부족으로 실패해 `MessageSerializationException`이 된다. 즉 `newArray` 방어를 우회하는 값이 존재하지만 그 우회는 할당이 아니라 읽기 실패로 끝난다.
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-json-c02
|
||||
title: 출하 구성의 codec registry에는 JSON 하나만 들어간다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-json-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-json-c02
|
||||
file: ../../../final/evidence/rendered/messaging-schema-json-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-json-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-json.md#L71 이다.
|
||||
module: messaging-schema-json
|
||||
---
|
||||
|
||||
# 출하 구성의 codec registry에는 JSON 하나만 들어간다
|
||||
|
||||
들어오는 것: messaging-core-api(api), messaging-schema-api(api), jackson-databind(implementation). 나가는 것: messaging-spring-boot-starter(registry allowed_dependencies에 포함).
|
||||
|
||||
## 관계
|
||||
|
||||
- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
들어오는 것은 `messaging-core-api`(api), `messaging-schema-api`(api), `jackson-databind`(implementation)다. 나가는 것은 `messaging-spring-boot-starter`(registry `allowed_dependencies`에 포함)다. **실제 배선 지점이 하나 있다** — 이 플랫폼에서 유일하게 조립되는 codec이다.
|
||||
|
||||
## MessageContracts 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-json-c02" alt="코드베이스에서 MessageContracts 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageContracts 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## varargs 자리가 비어 있다
|
||||
|
||||
`RegisteredMessageCodecs.of(defaultCodec, codecs...)`의 varargs 자리가 비어 있다. 즉 **출하 구성의 codec registry에는 JSON 하나만 들어간다.** Avro·Protobuf·raw bytes는 등록되지 않는다.
|
||||
|
||||
## 포맷 중립이어야 할 정책이 한 포맷 상수를 참조한다
|
||||
|
||||
두 번째 배선 지점은 상수 참조다. payload 정책의 상한이 **JSON codec의 상수에서 파생된다** — §17에서 다룬다.
|
||||
|
||||
## bean이 없으면 빈 registry로 만들어진다
|
||||
|
||||
`contracts.getIfAvailable(MessageContracts::none)`이 기본값이므로, 애플리케이션이 `MessageContracts` bean을 내놓지 않으면 **빈 registry**로 codec이 만들어진다. 그 codec은 모든 `encode`/`decode`를 `UNKNOWN_MESSAGE_TYPE`으로 거절한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-json-c03
|
||||
title: 인코딩 상한과 파서 상한이 하나의 값에서 나온다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-json-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-json-c03
|
||||
file: ../../../final/evidence/rendered/messaging-schema-json-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-json-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-json.md#L124 이다.
|
||||
module: messaging-schema-json
|
||||
---
|
||||
|
||||
# 인코딩 상한과 파서 상한이 하나의 값에서 나온다
|
||||
|
||||
여섯 가지 방어가 한 곳에 있다. 그리고 polymorphic default typing을 켜지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
여섯 가지 방어가 한 곳(`strictMapper`)에 있다 — `maxNestingDepth`(100), `maxDocumentLength`(codec 상한), `maxNumberLength`(1,000), `maxStringLength`·`maxNameLength`(5,000,000), `STRICT_DUPLICATE_DETECTION`, 그리고 `FAIL_ON_READING_DUP_TREE_KEY`·`FAIL_ON_TRAILING_TOKENS`·`FAIL_ON_UNKNOWN_PROPERTIES`. 그리고 **polymorphic default typing을 켜지 않는다** — javadoc이 그것이 대부분의 JSON gadget chain의 기반이라고 적는다.
|
||||
|
||||
## 두 상한이 갈라지지 않는다
|
||||
|
||||
`maxDocumentLength`가 `maxBytes`와 같다는 점이 중요하다 — 인코딩 상한과 디코딩 파서 상한이 하나의 값에서 나온다. 따로 두면 둘이 갈라진다.
|
||||
|
||||
## MessageTooLargeException 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-json-c03" alt="코드베이스에서 MessageTooLargeException 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageTooLargeException 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 증분 쓰기라 한도에서 멈춘다
|
||||
|
||||
주석이 이유를 적는다 — "Jackson writes incrementally, so a payload whose serialized form is far larger than the limit stops at the limit instead of after the whole graph has been rendered into a buffer nobody bounded."
|
||||
|
||||
## unwrapTooLarge 가 원인 사슬을 훑는다
|
||||
|
||||
`for (Throwable cause = exception; cause != null; cause = cause.getCause())` — 끝까지 훑어 `MessageTooLargeException`을 찾는다. 못 찾으면 `MessageSerializationException("JSON_ENCODE_FAILED")`.
|
||||
|
||||
## 에러 메시지만으로 운영자가 판단할 수 있다
|
||||
|
||||
`SCHEMA_VERSION_NOT_REGISTERED` 메시지에는 `registeredVersions(type)`가 정렬되어 포함된다. 테스트가 그 내용을 직접 단언한다 — `hasMessageContaining("order.created v999").hasMessageContaining("[1, 2]")`(`JsonContractRegistryTest.java:58-61`).
|
||||
|
||||
## 인코딩과 디코딩의 비대칭
|
||||
|
||||
인코딩에서 `OrderCreated`의 하위 타입을 넘기면 Jackson이 등록된 형태로 직렬화한다. 디코딩에서 하위 타입을 허용하면 등록된 계약과 다른 클래스로 역직렬화되므로 정확 일치여야 한다. 다만 이 비대칭은 주석으로 설명되지 않았다 — §15의 추론 항목이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-json-c04
|
||||
title: 디코딩은 등록 클래스와 정확히 같은지를 본다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-json-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-json-c04
|
||||
file: ../../../final/evidence/rendered/messaging-schema-json-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-json-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-json.md#L238 이다.
|
||||
module: messaging-schema-json
|
||||
---
|
||||
|
||||
# 디코딩은 등록 클래스와 정확히 같은지를 본다
|
||||
|
||||
encode: requireRegistered(type, version) → payload가 등록 타입의 인스턴스인지 → BoundedByteSink 생성 → mapper.writeValue(sink, payload) → 실패 시 unwrapTooLarge → EncodedMessage(bytes, JSON, SchemaReference) decode: requireRegistered(type, version) → 요청 클래스가 등록 클래스와 정확히 같은지 → encoded.length 상한 → mapper.readValue → JacksonException이면 JSON_DECODE_FAILED
|
||||
|
||||
## 관계
|
||||
|
||||
- **실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
**encode:** `requireRegistered(type, version)` → payload가 등록 타입의 인스턴스인지 → `BoundedByteSink` 생성 → `mapper.writeValue(sink, payload)` → 실패 시 `unwrapTooLarge` → `EncodedMessage(bytes, JSON, SchemaReference)`
|
||||
|
||||
**decode:** `requireRegistered(type, version)` → 요청 클래스가 등록 클래스와 정확히 같은지 → `encoded.length` 상한 → `mapper.readValue` → `JacksonException`이면 `JSON_DECODE_FAILED`
|
||||
|
||||
## BoundedByteSink 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-json-c04" alt="코드베이스에서 BoundedByteSink 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="BoundedByteSink 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-protobuf-c01
|
||||
title: 소비자가 계약을 등록하려면 vendor 타입을 이름 불러야 한다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-protobuf-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-protobuf-c01
|
||||
file: ../../../final/evidence/rendered/messaging-schema-protobuf-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-protobuf-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-protobuf.md#L49 이다.
|
||||
module: messaging-schema-protobuf
|
||||
---
|
||||
|
||||
# 소비자가 계약을 등록하려면 vendor 타입을 이름 불러야 한다
|
||||
|
||||
선택적 Protobuf codec. runtime_memberships: []이고 starter의 codec registry에도 등록되지 않는다 — build-only / incubating.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
선택적 Protobuf codec. `runtime_memberships: []`이고 starter의 codec registry에도 등록되지 않는다 — build-only / incubating.
|
||||
|
||||
## 이 기록이 다루는 범위
|
||||
|
||||
:::evidence key="messaging-schema-protobuf-c01" alt="코드베이스에서 파일 목록을 만든 출력 2줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## protobuf를 api로 선언한 근거
|
||||
|
||||
build.gradle 주석에 있다. `src/messaging/CLAUDE.md:40-43`의 vendor `api` 게이트를 통과한다 — `ProtobufMessageContract(Class<? extends Message>, Parser<? extends Message>)`가 public record이므로 소비자가 그 타입을 이름 부르지 않고는 계약을 등록할 수 없다.
|
||||
|
||||
## 이 leaf의 핵심 문제 인식
|
||||
|
||||
클래스 javadoc이 한 문장으로 적는다. `messaging-schema-avro`의 "does not fail — it produces plausible garbage"와 같은 성질이다. **JSON은 틀린 스키마로 디코딩하면 대개 실패하고, Avro와 Protobuf는 실패하지 않는다.** 그래서 두 leaf 모두 registry를 계약의 중심에 둔다.
|
||||
|
||||
<!-- body:end -->
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-protobuf-c04
|
||||
title: 계약 등록 실패가 시작 시점에 난다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-protobuf-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-protobuf-c04
|
||||
file: ../../../final/evidence/rendered/messaging-schema-protobuf-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-protobuf-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-protobuf.md#L229 이다.
|
||||
module: messaging-schema-protobuf
|
||||
---
|
||||
|
||||
# 계약 등록 실패가 시작 시점에 난다
|
||||
|
||||
계약 등록: new ProtobufMessageContract(class, parser) → 빈 입력 파싱 → 클래스 일치 확인 → 실패 시 MessagingConfigurationException encode: requireRegistered → Message이고 등록 클래스인지 → requireFits(getSerializedSize()) → writeTo(sink) → EncodedMessage(bytes, PROTOBUF, SchemaReference) decode: requireRegistered → 요청 클래스 정확 일치 → encoded.length 상한 → parser.parseFrom
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
세 경로가 있다.
|
||||
|
||||
**계약 등록:** `new ProtobufMessageContract(class, parser)` → 빈 입력 파싱 → 클래스 일치 확인 → 실패 시 `MessagingConfigurationException`
|
||||
|
||||
**encode:** `requireRegistered` → `Message`이고 등록 클래스인지 → `requireFits(getSerializedSize())` → `writeTo(sink)` → `EncodedMessage(bytes, PROTOBUF, SchemaReference)`
|
||||
|
||||
**decode:** `requireRegistered` → 요청 클래스 정확 일치 → `encoded.length` 상한 → `parser.parseFrom`
|
||||
|
||||
## MessagingConfigurationException 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-protobuf-c04" alt="코드베이스에서 MessagingConfigurationException 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingConfigurationException 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-protobuf-c05
|
||||
title: 세 codec이 같은 문제를 라이브러리에 맞춰 다르게 푼다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-protobuf-c05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-protobuf-c05
|
||||
file: ../../../final/evidence/rendered/messaging-schema-protobuf-c05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-protobuf-c05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-protobuf.md#L239 이다.
|
||||
module: messaging-schema-protobuf
|
||||
---
|
||||
|
||||
# 세 codec이 같은 문제를 라이브러리에 맞춰 다르게 푼다
|
||||
|
||||
Avro와 다른 점 하나. Avro는 catch (IOException | RuntimeException) 안에서 MessageTooLargeException을 instanceof로 통과시킨다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
**Avro와 다른 점 하나.** Avro는 `catch (IOException | RuntimeException)` 안에서 `MessageTooLargeException`을 `instanceof`로 통과시킨다. Protobuf는 `catch (IOException failure)`만 잡으므로 sink가 던지는 `MessageTooLargeException`(`RuntimeException`)이 그대로 전파된다.
|
||||
|
||||
## MessageTooLargeException 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-protobuf-c05" alt="코드베이스에서 MessageTooLargeException 를 검색한 출력 4줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageTooLargeException 코드베이스 검색 — 4줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 별도 통과 로직이 필요 없는 이유
|
||||
|
||||
protobuf-java가 예외를 감싸지 않기 때문이다. 세 codec이 같은 문제를 세 가지로 푸는데(JSON은 원인 사슬 탐색, Avro는 즉시 `instanceof`, Protobuf는 아무것도 안 함) 각각 라이브러리 동작에 맞는 최소 해법이다. 다만 그 이유가 코드에 적혀 있지 않다.
|
||||
|
||||
## 계약 위반은 메시지 실패가 아니다
|
||||
|
||||
`ProtobufMessageContract` 생성 실패는 registry를 조립하는 시점, 즉 시작 시점에 난다. 카테고리는 `CONFIGURATION`이고, `MessagingConfigurationException` javadoc이 그 의도를 적는다 — "Raised at startup wherever possible."
|
||||
|
||||
<!-- body:end -->
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-schema-protobuf-c07
|
||||
title: 실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-schema-protobuf-c07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-schema-protobuf-c07
|
||||
file: ../../../final/evidence/rendered/messaging-schema-protobuf-c07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-schema-protobuf-c07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-schema-protobuf.md#L458 이다.
|
||||
module: messaging-schema-protobuf
|
||||
---
|
||||
|
||||
# 실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다
|
||||
|
||||
ProtobufMessageContract javadoc이 두 결함을 보존한다. 두 번째가 특히 이 저장소의 반복 주제다 — 실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ProtobufMessageContract` javadoc이 두 결함을 보존한다. 두 번째가 특히 이 저장소의 반복 주제다 — **실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다.** `messaging-core-api`의 `FailureDescriptor` 설계, `MessageContractKey`의 2단 에러, JSON codec의 `unwrapTooLarge`가 전부 같은 관심사다.
|
||||
|
||||
## ProtobufMessageContract 참조 위치
|
||||
|
||||
:::evidence key="messaging-schema-protobuf-c07" alt="코드베이스에서 ProtobufMessageContract 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ProtobufMessageContract 코드베이스 검색 — 12줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## proto 파일의 주석이 남긴 규칙 셋
|
||||
|
||||
"Field numbers are the contract, not the field names ... Tags are never reused, and removed fields are reserved so that a later edit cannot take the number back." 이 규칙 셋 중 둘(개명 안전, 태그 재사용 위험)이 테스트로 증명되고 하나(reserved)는 증명되지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-testkit-c03
|
||||
title: 등급이 증거를 만들지 않고 증거가 등급을 만든다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-testkit-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-testkit-c03
|
||||
file: ../../../final/evidence/rendered/messaging-testkit-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-testkit-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-testkit.md#L140 이다.
|
||||
module: messaging-testkit
|
||||
---
|
||||
|
||||
# 등급이 증거를 만들지 않고 증거가 등급을 만든다
|
||||
|
||||
단일 패키지 dev.caskeleton.messaging.testkit. 세 소스셋이 같은 패키지를 공유하므로 InMemoryMessagingHarness(test)가 FaultController(main)를 package-private 없이 구현할 수 있고, EnvelopeCodecBenchmark(jmh)도 같은 패키지에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
단일 패키지 `dev.caskeleton.messaging.testkit`. 세 소스셋이 같은 패키지를 공유하므로 `InMemoryMessagingHarness`(test)가 `FaultController`(main)를 package-private 없이 구현할 수 있고, `EnvelopeCodecBenchmark`(jmh)도 같은 패키지에 있다.
|
||||
|
||||
## InMemoryMessagingHarness 참조 위치
|
||||
|
||||
:::evidence key="messaging-testkit-c03" alt="코드베이스에서 InMemoryMessagingHarness 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InMemoryMessagingHarness 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 화살표 방향이 한 번도 역전되지 않는다
|
||||
|
||||
데이터 흐름은 한 방향이다. 등급이 증거를 만들지 않고 증거가 등급을 만든다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: shared-contract-c04
|
||||
title: digest는 고정하지만 validator 상호운용은 증명하지 않는다
|
||||
topic: schema-and-wire-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:shared-contract-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: shared-contract-c04
|
||||
file: ../../../final/evidence/rendered/shared-contract-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/shared-contract-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/02-shared-contract.md#L114 이다.
|
||||
module: shared-contract
|
||||
---
|
||||
|
||||
# digest는 고정하지만 validator 상호운용은 증명하지 않는다
|
||||
|
||||
contracts/messaging/envelope/v1.schema.json은 Draft 2020-12 schema resource이며 envelopeVersion/eventId/contractId/payloadVersion/logicalDestination/aggregate/occurredAt/correlationId/contentType/payload를 required로 고정하고 top-level/aggregate에 unevaluatedProperties:false를 둔다. checked-in SHA-256은 bf6f2e13fafe01b8ef4cbb73d7ba3f5703bfc68d145bdfe43190bf606dbd00b1이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`contracts/messaging/envelope/v1.schema.json`은 Draft 2020-12 schema resource이며 envelopeVersion/eventId/contractId/payloadVersion/logicalDestination/aggregate/occurredAt/correlationId/contentType/payload를 required로 고정하고 top-level/aggregate에 `unevaluatedProperties:false`를 둔다. checked-in SHA-256은 `bf6f2e13fafe01b8ef4cbb73d7ba3f5703bfc68d145bdfe43190bf606dbd00b1`이다.
|
||||
|
||||
## MessagingEnvelopeSchemaResourceTest 참조 위치
|
||||
|
||||
:::evidence key="shared-contract-c04" alt="코드베이스에서 MessagingEnvelopeSchemaResourceTest 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingEnvelopeSchemaResourceTest 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 테스트가 JDK API로 확인하는 것
|
||||
|
||||
schema text 자체, identifier regex parity, Java int/long 경계 vector, strict UTF-8, exact digest.
|
||||
|
||||
## 그것이 증명하지 않는 것
|
||||
|
||||
resource drift와 digest mismatch는 강하게 막지만, README가 명시하듯 실제 Draft 2020-12 validator interoperability나 broker runtime discovery는 증명하지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user