{ "schema_version": "1.0", "document": "/home/donghyeon/workspace/chat-gpt-container/document-haness/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": 40858, "line": 40858 }, "current_section": { "heading": { "line": 40858, "level": 5, "text": "4.4 `MessagingLifecycle`: 8단계 순서 계약" }, "start_line": 40858, "end_line": 40888, "text": "##### 4.4 `MessagingLifecycle`: 8단계 순서 계약\n\n```java\n// MessagingLifecycle.java:8-15\n *
The order in {@link ShutdownPhase} is the contract, not an implementation detail. Closing\n * connections before settlements have been transmitted loses the settlements, and pausing consumers\n * after draining lets fresh deliveries arrive into a runtime that is already shutting down. Each\n * adapter implements the phases; none of them chooses the order.\n *\n *
Implementations are driven by the Spring lifecycle rather than a JVM shutdown hook alone. A\n * shutdown hook runs after the context has already begun disposing beans, so a handler mid-drain\n * can find its datasource closed underneath it.\n```\n\n여덟 단계:\n\n| # | 단계 | 뜻 |\n|---:|---|---|\n| 1 | `STOP_PUBLISH_ADMISSION` | 새 발행 거부 |\n| 2 | `STOP_NEW_HANDLERS` | 새 핸들러 시작 거부 |\n| 3 | `PAUSE_CONSUMERS` | 브로커에 전달 중단 요청 |\n| 4 | `DRAIN_HANDLERS` | 실행 중 핸들러 완료 대기 |\n| 5 | `FLUSH_SETTLEMENTS` | 그 핸들러들이 만든 정산 전송 |\n| 6 | `AWAIT_PRODUCER_CONFIRMS` | 미확인 발행이 모호로 남지 않게 |\n| 7 | `RELEASE_OUTBOX_LEASES` | 다른 relay가 즉시 claim 가능하게 |\n| 8 | `CLOSE_CONNECTIONS` | 연결·채널 종료 |\n\n`shutdown(Duration)`이 마감 시점에 실행 중이던 단계를 반환한다 — 완주하면 `CLOSE_CONNECTIONS`.\n\n**이 인터페이스를 구현하는 것이 저장소에 없다.** §12.1.\n" }, "previous_section": { "heading": { "line": 40814, "level": 5, "text": "4.3 `GracefulShutdownCoordinator`: 세 단계와 그 이유" }, "start_line": 40814, "end_line": 40857, "text": "##### 4.3 `GracefulShutdownCoordinator`: 세 단계와 그 이유\n\n```java\n// GracefulShutdownCoordinator.java:12-22\n *
Shutdown has three phases, in order: stop accepting new work, let what is running finish, then\n * close. Skipping the middle phase is what produces the classic shutdown bug — a handler is\n * interrupted between its side effect and its settlement, so the message is redelivered and the\n * effect happens twice.\n *\n *
The deadline exists because draining cannot be unbounded: a stuck handler would otherwise hold\n * the process open forever. Work still running at the deadline is abandoned unsettled, so\n * the broker redelivers it rather than the platform pretending it completed.\n *\n *
No retry attempt is created once draining begins. Starting a fresh attempt during shutdown\n * guarantees it will be abandoned at the deadline.\n```\n\n`tryBeginWork`가 **이중 검사**다.\n\n```java\npublic boolean tryBeginWork() {\n if (draining.get()) return false;\n inFlight.incrementAndGet();\n if (draining.get()) { inFlight.decrementAndGet(); return false; }\n return true;\n}\n```\n\n증가 후 다시 확인해서, 증가와 `beginDrain` 사이의 경합에서 계수를 되돌린다. 이 패턴이 없으면 드레인 시작 직후 시작된 작업이 계수에 남아 `isDrained`가 영원히 false가 된다.\n\n`endWork`가 0에서 clamp한다.\n\n```java\n// :65-67\n *
Clamped at zero. A double release used to drive the count negative, and a negative in-flight\n * count reports the drain as complete while work is still running — which is exactly when the\n * process shuts down underneath it.\npublic void endWork() {\n inFlight.updateAndGet(current -> current > 0 ? current - 1 : current);\n}\n```\n\n`isDrained(now)`가 세 갈래다 — 드레인 전이면 false, 계수 0이면 true, 아니면 마감 경과 여부. `abandonedWorkAtDeadline`이 \"마감으로 끝났는가\"를 별도로 답해서, 완주한 드레인과 포기한 드레인을 구분할 수 있다.\n" }, "next_section": { "heading": { "line": 40889, "level": 5, "text": "4.5 `TransportConsumerRegistration`: 순서 단위별 pause" }, "start_line": 40889, "end_line": 40899, "text": "##### 4.5 `TransportConsumerRegistration`: 순서 단위별 pause\n\n```java\n// :8-10\n *
Pause and resume operate on an ordering unit rather than the whole consumer, because that is\n * what makes {@code PAUSE_PARTITION} retry possible: one stuck key must not stall every other\n * partition on the same connection.\n```\n\n`scope`가 빈 문자열이면 전체다. `core-api`의 `PauseResumeController`는 `\"*\"`를 전체로 쓴다 — 두 인터페이스가 같은 개념에 **다른 sentinel**을 쓴다. `PauseResumeController`는 소비자가 0이므로(§A19-MESSAGING-CORE-API §12.1) 오늘 충돌하지 않지만, 그것을 배선하려는 사람이 두 규약을 이어야 한다.\n" }, "context_range": { "start_line": 40814, "end_line": 40899 }, "context_lines": [ { "line": 40814, "text": "##### 4.3 `GracefulShutdownCoordinator`: 세 단계와 그 이유" }, { "line": 40815, "text": "" }, { "line": 40816, "text": "```java" }, { "line": 40817, "text": "// GracefulShutdownCoordinator.java:12-22" }, { "line": 40818, "text": " *
Shutdown has three phases, in order: stop accepting new work, let what is running finish, then" }, { "line": 40819, "text": " * close. Skipping the middle phase is what produces the classic shutdown bug — a handler is" }, { "line": 40820, "text": " * interrupted between its side effect and its settlement, so the message is redelivered and the" }, { "line": 40821, "text": " * effect happens twice." }, { "line": 40822, "text": " *" }, { "line": 40823, "text": " *
The deadline exists because draining cannot be unbounded: a stuck handler would otherwise hold" }, { "line": 40824, "text": " * the process open forever. Work still running at the deadline is abandoned unsettled, so" }, { "line": 40825, "text": " * the broker redelivers it rather than the platform pretending it completed." }, { "line": 40826, "text": " *" }, { "line": 40827, "text": " *
No retry attempt is created once draining begins. Starting a fresh attempt during shutdown" }, { "line": 40828, "text": " * guarantees it will be abandoned at the deadline." }, { "line": 40829, "text": "```" }, { "line": 40830, "text": "" }, { "line": 40831, "text": "`tryBeginWork`가 **이중 검사**다." }, { "line": 40832, "text": "" }, { "line": 40833, "text": "```java" }, { "line": 40834, "text": "public boolean tryBeginWork() {" }, { "line": 40835, "text": " if (draining.get()) return false;" }, { "line": 40836, "text": " inFlight.incrementAndGet();" }, { "line": 40837, "text": " if (draining.get()) { inFlight.decrementAndGet(); return false; }" }, { "line": 40838, "text": " return true;" }, { "line": 40839, "text": "}" }, { "line": 40840, "text": "```" }, { "line": 40841, "text": "" }, { "line": 40842, "text": "증가 후 다시 확인해서, 증가와 `beginDrain` 사이의 경합에서 계수를 되돌린다. 이 패턴이 없으면 드레인 시작 직후 시작된 작업이 계수에 남아 `isDrained`가 영원히 false가 된다." }, { "line": 40843, "text": "" }, { "line": 40844, "text": "`endWork`가 0에서 clamp한다." }, { "line": 40845, "text": "" }, { "line": 40846, "text": "```java" }, { "line": 40847, "text": "// :65-67" }, { "line": 40848, "text": " *
Clamped at zero. A double release used to drive the count negative, and a negative in-flight" }, { "line": 40849, "text": " * count reports the drain as complete while work is still running — which is exactly when the" }, { "line": 40850, "text": " * process shuts down underneath it." }, { "line": 40851, "text": "public void endWork() {" }, { "line": 40852, "text": " inFlight.updateAndGet(current -> current > 0 ? current - 1 : current);" }, { "line": 40853, "text": "}" }, { "line": 40854, "text": "```" }, { "line": 40855, "text": "" }, { "line": 40856, "text": "`isDrained(now)`가 세 갈래다 — 드레인 전이면 false, 계수 0이면 true, 아니면 마감 경과 여부. `abandonedWorkAtDeadline`이 \"마감으로 끝났는가\"를 별도로 답해서, 완주한 드레인과 포기한 드레인을 구분할 수 있다." }, { "line": 40857, "text": "" }, { "line": 40858, "text": "##### 4.4 `MessagingLifecycle`: 8단계 순서 계약" }, { "line": 40859, "text": "" }, { "line": 40860, "text": "```java" }, { "line": 40861, "text": "// MessagingLifecycle.java:8-15" }, { "line": 40862, "text": " *
The order in {@link ShutdownPhase} is the contract, not an implementation detail. Closing" }, { "line": 40863, "text": " * connections before settlements have been transmitted loses the settlements, and pausing consumers" }, { "line": 40864, "text": " * after draining lets fresh deliveries arrive into a runtime that is already shutting down. Each" }, { "line": 40865, "text": " * adapter implements the phases; none of them chooses the order." }, { "line": 40866, "text": " *" }, { "line": 40867, "text": " *
Implementations are driven by the Spring lifecycle rather than a JVM shutdown hook alone. A" }, { "line": 40868, "text": " * shutdown hook runs after the context has already begun disposing beans, so a handler mid-drain" }, { "line": 40869, "text": " * can find its datasource closed underneath it." }, { "line": 40870, "text": "```" }, { "line": 40871, "text": "" }, { "line": 40872, "text": "여덟 단계:" }, { "line": 40873, "text": "" }, { "line": 40874, "text": "| # | 단계 | 뜻 |" }, { "line": 40875, "text": "|---:|---|---|" }, { "line": 40876, "text": "| 1 | `STOP_PUBLISH_ADMISSION` | 새 발행 거부 |" }, { "line": 40877, "text": "| 2 | `STOP_NEW_HANDLERS` | 새 핸들러 시작 거부 |" }, { "line": 40878, "text": "| 3 | `PAUSE_CONSUMERS` | 브로커에 전달 중단 요청 |" }, { "line": 40879, "text": "| 4 | `DRAIN_HANDLERS` | 실행 중 핸들러 완료 대기 |" }, { "line": 40880, "text": "| 5 | `FLUSH_SETTLEMENTS` | 그 핸들러들이 만든 정산 전송 |" }, { "line": 40881, "text": "| 6 | `AWAIT_PRODUCER_CONFIRMS` | 미확인 발행이 모호로 남지 않게 |" }, { "line": 40882, "text": "| 7 | `RELEASE_OUTBOX_LEASES` | 다른 relay가 즉시 claim 가능하게 |" }, { "line": 40883, "text": "| 8 | `CLOSE_CONNECTIONS` | 연결·채널 종료 |" }, { "line": 40884, "text": "" }, { "line": 40885, "text": "`shutdown(Duration)`이 마감 시점에 실행 중이던 단계를 반환한다 — 완주하면 `CLOSE_CONNECTIONS`." }, { "line": 40886, "text": "" }, { "line": 40887, "text": "**이 인터페이스를 구현하는 것이 저장소에 없다.** §12.1." }, { "line": 40888, "text": "" }, { "line": 40889, "text": "##### 4.5 `TransportConsumerRegistration`: 순서 단위별 pause" }, { "line": 40890, "text": "" }, { "line": 40891, "text": "```java" }, { "line": 40892, "text": "// :8-10" }, { "line": 40893, "text": " *
Pause and resume operate on an ordering unit rather than the whole consumer, because that is" }, { "line": 40894, "text": " * what makes {@code PAUSE_PARTITION} retry possible: one stuck key must not stall every other" }, { "line": 40895, "text": " * partition on the same connection." }, { "line": 40896, "text": "```" }, { "line": 40897, "text": "" }, { "line": 40898, "text": "`scope`가 빈 문자열이면 전체다. `core-api`의 `PauseResumeController`는 `\"*\"`를 전체로 쓴다 — 두 인터페이스가 같은 개념에 **다른 sentinel**을 쓴다. `PauseResumeController`는 소비자가 0이므로(§A19-MESSAGING-CORE-API §12.1) 오늘 충돌하지 않지만, 그것을 배선하려는 사람이 두 규약을 이어야 한다." }, { "line": 40899, "text": "" } ], "numbered_context": "40814 | ##### 4.3 `GracefulShutdownCoordinator`: 세 단계와 그 이유\n40815 | \n40816 | ```java\n40817 | // GracefulShutdownCoordinator.java:12-22\n40818 | *
Shutdown has three phases, in order: stop accepting new work, let what is running finish, then\n40819 | * close. Skipping the middle phase is what produces the classic shutdown bug — a handler is\n40820 | * interrupted between its side effect and its settlement, so the message is redelivered and the\n40821 | * effect happens twice.\n40822 | *\n40823 | *
The deadline exists because draining cannot be unbounded: a stuck handler would otherwise hold\n40824 | * the process open forever. Work still running at the deadline is abandoned unsettled, so\n40825 | * the broker redelivers it rather than the platform pretending it completed.\n40826 | *\n40827 | *
No retry attempt is created once draining begins. Starting a fresh attempt during shutdown\n40828 | * guarantees it will be abandoned at the deadline.\n40829 | ```\n40830 | \n40831 | `tryBeginWork`가 **이중 검사**다.\n40832 | \n40833 | ```java\n40834 | public boolean tryBeginWork() {\n40835 | if (draining.get()) return false;\n40836 | inFlight.incrementAndGet();\n40837 | if (draining.get()) { inFlight.decrementAndGet(); return false; }\n40838 | return true;\n40839 | }\n40840 | ```\n40841 | \n40842 | 증가 후 다시 확인해서, 증가와 `beginDrain` 사이의 경합에서 계수를 되돌린다. 이 패턴이 없으면 드레인 시작 직후 시작된 작업이 계수에 남아 `isDrained`가 영원히 false가 된다.\n40843 | \n40844 | `endWork`가 0에서 clamp한다.\n40845 | \n40846 | ```java\n40847 | // :65-67\n40848 | *
Clamped at zero. A double release used to drive the count negative, and a negative in-flight\n40849 | * count reports the drain as complete while work is still running — which is exactly when the\n40850 | * process shuts down underneath it.\n40851 | public void endWork() {\n40852 | inFlight.updateAndGet(current -> current > 0 ? current - 1 : current);\n40853 | }\n40854 | ```\n40855 | \n40856 | `isDrained(now)`가 세 갈래다 — 드레인 전이면 false, 계수 0이면 true, 아니면 마감 경과 여부. `abandonedWorkAtDeadline`이 \"마감으로 끝났는가\"를 별도로 답해서, 완주한 드레인과 포기한 드레인을 구분할 수 있다.\n40857 | \n40858 | ##### 4.4 `MessagingLifecycle`: 8단계 순서 계약\n40859 | \n40860 | ```java\n40861 | // MessagingLifecycle.java:8-15\n40862 | *
The order in {@link ShutdownPhase} is the contract, not an implementation detail. Closing\n40863 | * connections before settlements have been transmitted loses the settlements, and pausing consumers\n40864 | * after draining lets fresh deliveries arrive into a runtime that is already shutting down. Each\n40865 | * adapter implements the phases; none of them chooses the order.\n40866 | *\n40867 | *
Implementations are driven by the Spring lifecycle rather than a JVM shutdown hook alone. A\n40868 | * shutdown hook runs after the context has already begun disposing beans, so a handler mid-drain\n40869 | * can find its datasource closed underneath it.\n40870 | ```\n40871 | \n40872 | 여덟 단계:\n40873 | \n40874 | | # | 단계 | 뜻 |\n40875 | |---:|---|---|\n40876 | | 1 | `STOP_PUBLISH_ADMISSION` | 새 발행 거부 |\n40877 | | 2 | `STOP_NEW_HANDLERS` | 새 핸들러 시작 거부 |\n40878 | | 3 | `PAUSE_CONSUMERS` | 브로커에 전달 중단 요청 |\n40879 | | 4 | `DRAIN_HANDLERS` | 실행 중 핸들러 완료 대기 |\n40880 | | 5 | `FLUSH_SETTLEMENTS` | 그 핸들러들이 만든 정산 전송 |\n40881 | | 6 | `AWAIT_PRODUCER_CONFIRMS` | 미확인 발행이 모호로 남지 않게 |\n40882 | | 7 | `RELEASE_OUTBOX_LEASES` | 다른 relay가 즉시 claim 가능하게 |\n40883 | | 8 | `CLOSE_CONNECTIONS` | 연결·채널 종료 |\n40884 | \n40885 | `shutdown(Duration)`이 마감 시점에 실행 중이던 단계를 반환한다 — 완주하면 `CLOSE_CONNECTIONS`.\n40886 | \n40887 | **이 인터페이스를 구현하는 것이 저장소에 없다.** §12.1.\n40888 | \n40889 | ##### 4.5 `TransportConsumerRegistration`: 순서 단위별 pause\n40890 | \n40891 | ```java\n40892 | // :8-10\n40893 | *
Pause and resume operate on an ordering unit rather than the whole consumer, because that is\n40894 | * what makes {@code PAUSE_PARTITION} retry possible: one stuck key must not stall every other\n40895 | * partition on the same connection.\n40896 | ```\n40897 | \n40898 | `scope`가 빈 문자열이면 전체다. `core-api`의 `PauseResumeController`는 `\"*\"`를 전체로 쓴다 — 두 인터페이스가 같은 개념에 **다른 sentinel**을 쓴다. `PauseResumeController`는 소비자가 0이므로(§A19-MESSAGING-CORE-API §12.1) 오늘 충돌하지 않지만, 그것을 배선하려는 사람이 두 규약을 이어야 한다.\n40899 | ",
"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