{ "schema_version": "1.0", "document": "docs/clean-architecture-backend-template/final/document.md", "document_sha256": "8071fe71b3359d9cf60b95909c26c7b50653ce2f22bbc5fcf6988719bb91236d", "line_count": 47035, "line_number_space": "canonical-source-with-managed-blocks-collapsed", "anchor": { "kind": "line", "value": 35530, "line": 35530 }, "current_section": { "heading": { "line": 35530, "level": 5, "text": "4.1 `MessageContractKey`: 버전을 키에 넣는 이유" }, "start_line": 35530, "end_line": 35546, "text": "##### 4.1 `MessageContractKey`: 버전을 키에 넣는 이유\n\n이 leaf에서 가장 밀도 높은 javadoc이다.\n\n```java\n// MessageContractKey.java:10-17\n *
Keying on the message type alone is what let an unregistered version decode. The version\n * travels in the envelope and in {@link SchemaReference}, so a consumer receiving {@code\n * order.created v999} would look up {@code order.created}, find the v1 class or parser, decode\n * against it, and then keep the v999 label on the result. Nothing failed, and every downstream\n * compatibility gate and audit record then described a version that was never registered.\n```\n\n핵심은 \"Nothing failed\"다. 타입만으로 키를 잡으면 실패가 발생하지 않고 **잘못된 성공**이 발생한다. 그리고 그 결과에는 등록된 적 없는 버전 라벨이 붙어 하위 감사 기록까지 오염된다.\n\n이 결정은 세 codec에 전부 반영돼 있다 — `JacksonMessageCodec.requireRegistered`, `AvroMessageCodec.schemaFor`, `ProtobufMessageCodec.requireRegistered`가 모두 \"타입은 아는데 버전을 모른다\"와 \"타입 자체를 모른다\"를 **다른 에러 코드**로 구분한다(`SCHEMA_VERSION_NOT_REGISTERED` vs `UNKNOWN_MESSAGE_TYPE`). 그 구분이 있어야 운영자가 \"등록을 빠뜨렸다\"와 \"오타다\"를 나눌 수 있다.\n" }, "previous_section": { "heading": { "line": 35528, "level": 4, "text": "4. 계약·불변식·상태 모델" }, "start_line": 35528, "end_line": 35529, "text": "#### 4. 계약·불변식·상태 모델\n" }, "next_section": { "heading": { "line": 35547, "level": 5, "text": "4.2 `BoundedByteSink`: 보고 임계값 → 할당 경계" }, "start_line": 35547, "end_line": 35567, "text": "##### 4.2 `BoundedByteSink`: 보고 임계값 → 할당 경계\n\n```java\n// BoundedByteSink.java:11-15\n *
Every codec here used to serialize into an unbounded buffer and compare {@code bytes.length}\n * to the configured maximum afterwards. That makes the maximum a reporting threshold rather than an\n * allocation bound: a payload whose graph expands to hundreds of megabytes exhausts the heap while\n * being written, and the check that would have rejected it never runs. Under a broker consumer that\n * is a process-wide outage caused by one message.\n```\n\n세 가지 설계 결정이 붙어 있다.\n\n1. **버퍼를 한도로 미리 잡지 않는다.** `new ByteArrayOutputStream(Math.min(maxBytes, 8_192))` — 주석: \"a 1 GiB bound must not pre-allocate 1 GiB.\"\n2. **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\")`).\n3. **`requireFits(size)`는 예산을 소비하지 않는다.** Protobuf는 직렬화 크기를 미리 알므로 첫 바이트 전에 거절할 수 있다. 그리고 그 뒤의 쓰기도 여전히 경계 안이다 — 주석: \"this is a cheaper refusal, not a replacement for the bound.\"\n\n`refuseIfBeyondLimit`가 `size > maxBytes - written`으로 비교하는 것도 의도적이다. `written + size > maxBytes`였다면 `int` 오버플로가 가능하다.\n\n테스트가 실제 시나리오를 재현한다 — 10 MiB를 1 KiB씩 제공하고, `written()`이 한도(64) 이하로 유지되며 `toByteArray()`가 비어 있음을 확인한다(`BoundedByteSinkTest.java:34-53`).\n" }, "context_range": { "start_line": 35528, "end_line": 35567 }, "context_lines": [ { "line": 35528, "text": "#### 4. 계약·불변식·상태 모델" }, { "line": 35529, "text": "" }, { "line": 35530, "text": "##### 4.1 `MessageContractKey`: 버전을 키에 넣는 이유" }, { "line": 35531, "text": "" }, { "line": 35532, "text": "이 leaf에서 가장 밀도 높은 javadoc이다." }, { "line": 35533, "text": "" }, { "line": 35534, "text": "```java" }, { "line": 35535, "text": "// MessageContractKey.java:10-17" }, { "line": 35536, "text": " *
Keying on the message type alone is what let an unregistered version decode. The version" }, { "line": 35537, "text": " * travels in the envelope and in {@link SchemaReference}, so a consumer receiving {@code" }, { "line": 35538, "text": " * order.created v999} would look up {@code order.created}, find the v1 class or parser, decode" }, { "line": 35539, "text": " * against it, and then keep the v999 label on the result. Nothing failed, and every downstream" }, { "line": 35540, "text": " * compatibility gate and audit record then described a version that was never registered." }, { "line": 35541, "text": "```" }, { "line": 35542, "text": "" }, { "line": 35543, "text": "핵심은 \"Nothing failed\"다. 타입만으로 키를 잡으면 실패가 발생하지 않고 **잘못된 성공**이 발생한다. 그리고 그 결과에는 등록된 적 없는 버전 라벨이 붙어 하위 감사 기록까지 오염된다." }, { "line": 35544, "text": "" }, { "line": 35545, "text": "이 결정은 세 codec에 전부 반영돼 있다 — `JacksonMessageCodec.requireRegistered`, `AvroMessageCodec.schemaFor`, `ProtobufMessageCodec.requireRegistered`가 모두 \"타입은 아는데 버전을 모른다\"와 \"타입 자체를 모른다\"를 **다른 에러 코드**로 구분한다(`SCHEMA_VERSION_NOT_REGISTERED` vs `UNKNOWN_MESSAGE_TYPE`). 그 구분이 있어야 운영자가 \"등록을 빠뜨렸다\"와 \"오타다\"를 나눌 수 있다." }, { "line": 35546, "text": "" }, { "line": 35547, "text": "##### 4.2 `BoundedByteSink`: 보고 임계값 → 할당 경계" }, { "line": 35548, "text": "" }, { "line": 35549, "text": "```java" }, { "line": 35550, "text": "// BoundedByteSink.java:11-15" }, { "line": 35551, "text": " *
Every codec here used to serialize into an unbounded buffer and compare {@code bytes.length}" }, { "line": 35552, "text": " * to the configured maximum afterwards. That makes the maximum a reporting threshold rather than an" }, { "line": 35553, "text": " * allocation bound: a payload whose graph expands to hundreds of megabytes exhausts the heap while" }, { "line": 35554, "text": " * being written, and the check that would have rejected it never runs. Under a broker consumer that" }, { "line": 35555, "text": " * is a process-wide outage caused by one message." }, { "line": 35556, "text": "```" }, { "line": 35557, "text": "" }, { "line": 35558, "text": "세 가지 설계 결정이 붙어 있다." }, { "line": 35559, "text": "" }, { "line": 35560, "text": "1. **버퍼를 한도로 미리 잡지 않는다.** `new ByteArrayOutputStream(Math.min(maxBytes, 8_192))` — 주석: \"a 1 GiB bound must not pre-allocate 1 GiB.\"" }, { "line": 35561, "text": "2. **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\")`)." }, { "line": 35562, "text": "3. **`requireFits(size)`는 예산을 소비하지 않는다.** Protobuf는 직렬화 크기를 미리 알므로 첫 바이트 전에 거절할 수 있다. 그리고 그 뒤의 쓰기도 여전히 경계 안이다 — 주석: \"this is a cheaper refusal, not a replacement for the bound.\"" }, { "line": 35563, "text": "" }, { "line": 35564, "text": "`refuseIfBeyondLimit`가 `size > maxBytes - written`으로 비교하는 것도 의도적이다. `written + size > maxBytes`였다면 `int` 오버플로가 가능하다." }, { "line": 35565, "text": "" }, { "line": 35566, "text": "테스트가 실제 시나리오를 재현한다 — 10 MiB를 1 KiB씩 제공하고, `written()`이 한도(64) 이하로 유지되며 `toByteArray()`가 비어 있음을 확인한다(`BoundedByteSinkTest.java:34-53`)." }, { "line": 35567, "text": "" } ], "numbered_context": "35528 | #### 4. 계약·불변식·상태 모델\n35529 | \n35530 | ##### 4.1 `MessageContractKey`: 버전을 키에 넣는 이유\n35531 | \n35532 | 이 leaf에서 가장 밀도 높은 javadoc이다.\n35533 | \n35534 | ```java\n35535 | // MessageContractKey.java:10-17\n35536 | *
Keying on the message type alone is what let an unregistered version decode. The version\n35537 | * travels in the envelope and in {@link SchemaReference}, so a consumer receiving {@code\n35538 | * order.created v999} would look up {@code order.created}, find the v1 class or parser, decode\n35539 | * against it, and then keep the v999 label on the result. Nothing failed, and every downstream\n35540 | * compatibility gate and audit record then described a version that was never registered.\n35541 | ```\n35542 | \n35543 | 핵심은 \"Nothing failed\"다. 타입만으로 키를 잡으면 실패가 발생하지 않고 **잘못된 성공**이 발생한다. 그리고 그 결과에는 등록된 적 없는 버전 라벨이 붙어 하위 감사 기록까지 오염된다.\n35544 | \n35545 | 이 결정은 세 codec에 전부 반영돼 있다 — `JacksonMessageCodec.requireRegistered`, `AvroMessageCodec.schemaFor`, `ProtobufMessageCodec.requireRegistered`가 모두 \"타입은 아는데 버전을 모른다\"와 \"타입 자체를 모른다\"를 **다른 에러 코드**로 구분한다(`SCHEMA_VERSION_NOT_REGISTERED` vs `UNKNOWN_MESSAGE_TYPE`). 그 구분이 있어야 운영자가 \"등록을 빠뜨렸다\"와 \"오타다\"를 나눌 수 있다.\n35546 | \n35547 | ##### 4.2 `BoundedByteSink`: 보고 임계값 → 할당 경계\n35548 | \n35549 | ```java\n35550 | // BoundedByteSink.java:11-15\n35551 | *
Every codec here used to serialize into an unbounded buffer and compare {@code bytes.length}\n35552 | * to the configured maximum afterwards. That makes the maximum a reporting threshold rather than an\n35553 | * allocation bound: a payload whose graph expands to hundreds of megabytes exhausts the heap while\n35554 | * being written, and the check that would have rejected it never runs. Under a broker consumer that\n35555 | * is a process-wide outage caused by one message.\n35556 | ```\n35557 | \n35558 | 세 가지 설계 결정이 붙어 있다.\n35559 | \n35560 | 1. **버퍼를 한도로 미리 잡지 않는다.** `new ByteArrayOutputStream(Math.min(maxBytes, 8_192))` — 주석: \"a 1 GiB bound must not pre-allocate 1 GiB.\"\n35561 | 2. **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\")`).\n35562 | 3. **`requireFits(size)`는 예산을 소비하지 않는다.** Protobuf는 직렬화 크기를 미리 알므로 첫 바이트 전에 거절할 수 있다. 그리고 그 뒤의 쓰기도 여전히 경계 안이다 — 주석: \"this is a cheaper refusal, not a replacement for the bound.\"\n35563 | \n35564 | `refuseIfBeyondLimit`가 `size > maxBytes - written`으로 비교하는 것도 의도적이다. `written + size > maxBytes`였다면 `int` 오버플로가 가능하다.\n35565 | \n35566 | 테스트가 실제 시나리오를 재현한다 — 10 MiB를 1 KiB씩 제공하고, `written()`이 한도(64) 이하로 유지되며 `toByteArray()`가 비어 있음을 확인한다(`BoundedByteSinkTest.java:34-53`).\n35567 | ",
"headings": [
{
"line": 1,
"level": 1,
"text": "clean-architecture-backend-template — 상세 분석 (통합 정본)"
},
{
"line": 40,
"level": 2,
"text": "0. 이 문서를 읽는 법"
},
{
"line": 60,
"level": 2,
"text": "1. Project map — 숫자로 먼저"
},
{
"line": 62,
"level": 3,
"text": "1.1 빌드와 레지스트리"
},
{
"line": 81,
"level": 3,
"text": "1.2 가족별 분모와 출하 여부"
},
{
"line": 94,
"level": 3,
"text": "1.3 leaf별 규모 (main Java 기준 상위)"
},
{
"line": 119,
"level": 3,
"text": "1.4 이 표에서 읽어야 할 것"
},
{
"line": 168,
"level": 2,
"text": "2. Architectural boundaries — 무엇이 경계를 강제하는가"
},
{
"line": 173,
"level": 3,
"text": "2.1 강제 장치 목록"
},
{
"line": 189,
"level": 3,
"text": "2.2 `CleanArchitectureTest`의 규칙 14종"
},
{
"line": 212,
"level": 3,
"text": "2.3 검증된 경계 — 실제로 성립하는 것"
},
{
"line": 266,
"level": 3,
"text": "2.4 경계가 열려 있는 지점"
},
{
"line": 300,
"level": 2,
"text": "3. Representative execution paths"
},
{
"line": 302,
"level": 3,
"text": "3.1 HTTP 요청 — 출하 경로"
},
{
"line": 364,
"level": 3,
"text": "3.2 트랜잭션 — `application-core` 포트에서 PostgreSQL local timeout까지"
},
{
"line": 453,
"level": 3,
"text": "3.3 메시지 발행 — messaging 플랫폼"
},
{
"line": 494,
"level": 3,
"text": "3.4 gRPC — 채택 시점 경로"
},
{
"line": 518,
"level": 3,
"text": "3.5 알림 발송 — 논리적 수락과 provider 불확실성"
},
{
"line": 539,
"level": 2,
"text": "4. Data and state"
},
{
"line": 541,
"level": 3,
"text": "4.1 관계형 — `persistence-jpa` (605 파일 / main 350 / 27,744 LOC)"
},
{
"line": 654,
"level": 3,
"text": "4.2 문서형 — `persistence-mongo` (497 파일 / main 351 / 22,924 LOC)"
},
{
"line": 705,
"level": 3,
"text": "4.3 messaging 신뢰성 저장소 (`19` §7)"
},
{
"line": 757,
"level": 3,
"text": "4.4 fileserver / objectstorage / cache-redis"
},
{
"line": 788,
"level": 2,
"text": "5. Failure and operational behavior"
},
{
"line": 790,
"level": 3,
"text": "5.1 실패 분류 — 세 개의 계층"
},
{
"line": 824,
"level": 3,
"text": "5.2 관측 — 태그를 유한하게, 그리고 그 대가"
},
{
"line": 854,
"level": 3,
"text": "5.3 시작 검증기 — 법칙과 그 예외"
},
{
"line": 903,
"level": 3,
"text": "5.4 admin plane — 가장 잘 조립된 게이트"
},
{
"line": 939,
"level": 3,
"text": "5.5 gRPC 구현 층의 원자성 (`20` §7)"
},
{
"line": 1011,
"level": 2,
"text": "6. Tests and verification coverage"
},
{
"line": 1013,
"level": 3,
"text": "6.1 실행한 것"
},
{
"line": 1025,
"level": 3,
"text": "6.2 실행하지 않은 것과 그 이유"
},
{
"line": 1047,
"level": 3,
"text": "6.3 fail-closed 레인 규약"
},
{
"line": 1071,
"level": 3,
"text": "6.4 완전히 닫힌 게이트 하나 — messaging 인증 체인"
},
{
"line": 1111,
"level": 3,
"text": "6.5 evidence manifest — JPA의 R1/R2 분리"
},
{
"line": 1125,
"level": 3,
"text": "6.6 게이트가 통과하면서 아무것도 증명하지 않는 경우 — 14건"
},
{
"line": 1156,
"level": 2,
"text": "7. 이 저장소에서 반복된 네 가지 형태"
},
{
"line": 1160,
"level": 3,
"text": "7.1 형태 A — 판정하는 코드는 있고, 부르는 코드가 없다"
},
{
"line": 1203,
"level": 3,
"text": "7.2 형태 B — 게이트가 통과하면서 아무것도 증명하지 않는다"
},
{
"line": 1214,
"level": 3,
"text": "7.3 형태 C — 중복 장치에서 조립된 쪽이 약한 쪽이다"
},
{
"line": 1239,
"level": 3,
"text": "7.4 형태 D — 문서 드리프트, 그리고 그 방향"
},
{
"line": 1274,
"level": 3,
"text": "7.5 공시 스펙트럼 — 자기 미완성을 얼마나 말했는가"
},
{
"line": 1289,
"level": 3,
"text": "7.6 학습 전이 — messaging → grpc"
},
{
"line": 1308,
"level": 2,
"text": "8. Confirmed problems"
},
{
"line": 1310,
"level": 3,
"text": "8.1 P1 — 지금 출하되는 아티팩트에서 틀린 동작"
},
{
"line": 1349,
"level": 3,
"text": "8.2 P2 — 명확한 실패 시나리오를 가진 실질적 공백"
},
{
"line": 1392,
"level": 3,
"text": "8.3 심각도가 등급 때문에 낮아진 것"
},
{
"line": 1403,
"level": 2,
"text": "9. Reusable criteria and rules"
},
{
"line": 1452,
"level": 2,
"text": "10. Explicit project decisions"
},
{
"line": 1457,
"level": 3,
"text": "10.1 계약과 경계"
},
{
"line": 1468,
"level": 3,
"text": "10.2 실패와 불확실성"
},
{
"line": 1480,
"level": 3,
"text": "10.3 조립과 활성화"
},
{
"line": 1492,
"level": 3,
"text": "10.4 데이터와 경계값"
},
{
"line": 1506,
"level": 3,
"text": "10.5 증거와 게이트"
},
{
"line": 1523,
"level": 2,
"text": "11. Unresolved questions"
},
{
"line": 1564,
"level": 2,
"text": "12. Evidence index"
},
{
"line": 1581,
"level": 2,
"text": "13. Limits of this analysis"
},
{
"line": 1632,
"level": 2,
"text": "14. 사이클 2 — 18개 리프 재검증과 23개 리프 전수 통독"
},
{
"line": 1634,
"level": 3,
"text": "14.1 18개 리프 재검증"
},
{
"line": 1668,
"level": 3,
"text": "14.2 23개 리프 전수 통독"
},
{
"line": 1747,
"level": 2,
"text": "부록 A. 모듈 문서 지도"
},
{
"line": 1779,
"level": 2,
"text": "부록 B. 자주 쓸 명령"
},
{
"line": 1825,
"level": 2,
"text": "부록 C. 다시 읽는다면 이 순서"
},
{
"line": 1839,
"level": 1,
"text": "제2부 — 모듈 분석 전문"
},
{
"line": 1845,
"level": 2,
"text": "A00. project-overview"
},
{
"line": 1849,
"level": 3,
"text": "Project Overview"
},
{
"line": 1856,
"level": 4,
"text": "분석 기준 revision"
},
{
"line": 1867,
"level": 4,
"text": "최종 커버리지"
},
{
"line": 1884,
"level": 4,
"text": "Build and module map"
},
{
"line": 1939,
"level": 4,
"text": "Dependency direction"
},
{
"line": 1945,
"level": 4,
"text": "Runtime entry points"
},
{
"line": 1951,
"level": 4,
"text": "Persistence / messaging / external systems"
},
{
"line": 1955,
"level": 4,
"text": "Test topology"
},
{
"line": 1960,
"level": 4,
"text": "Configuration and operational surfaces"
},
{
"line": 1964,
"level": 4,
"text": "분석할 bounded scopes (계획 — 실제 문서 배치는 위 \"최종 커버리지\" 참조)"
},
{
"line": 1977,
"level": 4,
"text": "아직 단정하지 않는 것 (분석 시작 시점의 목록)"
},
{
"line": 1993,
"level": 2,
"text": "A01. domain-core"
},
{
"line": 1997,
"level": 3,
"text": "domain-core 상세 분석"
},
{
"line": 2000,
"level": 4,
"text": "SSOT identity — 2026-08-31 재검증"
},
{
"line": 2015,
"level": 4,
"text": "분석 범위와 결론 상태"
},
{
"line": 2026,
"level": 4,
"text": "1. Quantified scope map"
},
{
"line": 2028,
"level": 5,
"text": "Owned source"
},
{
"line": 2042,
"level": 4,
"text": "2. Coverage ledger"
},
{
"line": 2062,
"level": 4,
"text": "3. 이 모듈이 실제로 소유하는 것"
},
{
"line": 2064,
"level": 5,
"text": "관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다"
},
{
"line": 2073,
"level": 4,
"text": "4. Identifier contract"
},
{
"line": 2075,
"level": 5,
"text": "`ResourceId