{ "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": 25536, "line": 25536 }, "current_section": { "heading": { "line": 25536, "level": 5, "text": "4.4 `InMemoryAdminOperationJournal` — 프로토콜이 단순화되지 않았다" }, "start_line": 25536, "end_line": 25591, "text": "##### 4.4 `InMemoryAdminOperationJournal` — 프로토콜이 단순화되지 않았다\n\n```java\n// InMemoryAdminOperationJournal.java:17-27\n/**\n * A single-process journal, for tests and for local development.\n *\n *
It reports {@link #isDurable()} as false, and the starter refuses to run a production profile\n * on a journal that says so. That declaration is the point of this class existing at all: the\n * previous in-memory store was registered as the production default and nothing distinguished it\n * from a shared one, so the gap was invisible until two replicas executed the same approval.\n *\n *
The semantics are otherwise the real ones — uniqueness on {@code (ticket, digest)}, lease\n * takeover with a monotonic token, resume from checkpoint — so a test that passes here is testing\n * the protocol rather than a simplification of it.\n */\n```\n\n마지막 문장이 지켜지는지가 이 클래스의 값어치다. 확인 결과 지켜진다.\n\n`begin` 의 `claim(...)` 이 네 갈래다(`:64-115`).\n\n| 기존 상태 | 처리 | 코드 |\n|---|---|---|\n| 없음 | 새 record, token=1, itemsCompleted=0 | `:72-85` |\n| `COMPLETED` | `APPROVAL_ALREADY_EXECUTED` — \"an approval authorises one execution, not a standing permission\" | `:86-92` |\n| `STARTED` + 리스 유효 | `ADMIN_OPERATION_IN_FLIGHT` — \"two runtimes executing one approval is a duplicate storm, not a faster redrive\" | `:93-100` |\n| `FAILED` 또는 리스 만료 | 체크포인트 유지, **token+1** 로 인수 | `:101-114` |\n\n펜스는 `update(...)` 에 있다.\n\n```java\n// :206-214\nif (current.leaseToken() != lease.leaseToken()) {\n // The fence. A stalled runtime that wakes up and writes here would otherwise overwrite\n // the progress of whichever replica took the operation over.\n throw new MessageAuthorizationException(\"ADMIN_OPERATION_LEASE_LOST\", …);\n}\n```\n\n그리고 키 생성이 `ApprovalGrant.canonicalForm()` 의 규칙을 그대로 가져온다.\n\n```java\n// :219-225\nprivate static String key(String approvalTicket, PlanDigest planDigest) {\n // Length-prefixed for the same reason the grant's canonical form is: a ticket containing the\n // separator must not be able to collide with a different ticket and digest pair.\n return String.join(\"\", Integer.toString(approvalTicket.length()), \":\", approvalTicket,\n planDigest.value());\n}\n```\n\n길이 접두 규칙이 `messaging-admin-api` 밖으로 전파된 사례다. (그 규칙이 **닿지 않은** 유일한 곳이 계획 다이제스트라는 점은 §A19-MESSAGING-ADMIN-API §12.3(b)에 있다.)\n\n`checkpoint`/`complete`/`fail` 셋 다 `Math.max(current.itemsCompleted(), itemsCompleted)` 로 clamp 한다(`:128, :147, :167`). 이것이 `DefaultMessagingAdminService` 가 `journal.fail(lease, lease.resumeFrom(), …)` 로 **낡은 값**을 넘겨도 진행이 되돌아가지 않는 이유다. §12.4(c).\n" }, "previous_section": { "heading": { "line": 25506, "level": 5, "text": "4.3 `ReplayService` — 안전한 형태를 공짜로 만든다" }, "start_line": 25506, "end_line": 25535, "text": "##### 4.3 `ReplayService` — 안전한 형태를 공짜로 만든다\n\n```java\n// ReplayService.java:13-22\n/**\n *
An isolated replay reads alongside the live consumer and needs no approval, because it changes\n * nothing: a throwaway group has its own offsets. Replaying into an existing production group is a\n * different operation entirely — it rewinds a live consumer and reprocesses everything since — so\n * it goes through the destructive guard.\n *\n *
Making the safe form free and the destructive form approved is what keeps operators from\n * reaching for the destructive one out of convenience.\n */\n```\n\n판단은 옳다. 구현이 그 판단을 `dryRun` 파라미터로 표현한다.\n\n```java\n// :58-64\nboolean needsApproval = !request.isolatedConsumerGroup();\nguard.authorize(\n DestructiveOperation.REPLAY,\n request.destination(),\n approval,\n request.dryRun() || !needsApproval, // <- guard 의 dryRun 인자\n now);\n```\n\n`DestructiveOperationGuard.authorize` 는 `dryRun` 이 참이면 즉시 반환한다(`DestructiveOperationGuard.java:54-56`). 즉 격리 리플레이는 \"승인 불필요\" 가 아니라 \"dry run 인 척\" 으로 통과한다. 감사 이벤트는 그 구분을 남긴다 — `approval.map(VerifiedApproval::ticket).orElse(\"isolated\")`(`:77`) — 그러나 guard 쪽에는 남지 않는다. §17 P3.\n" }, "next_section": { "heading": { "line": 25592, "level": 5, "text": "4.5 `TopologyValidator` — severity 가 판단이다" }, "start_line": 25592, "end_line": 25618, "text": "##### 4.5 `TopologyValidator` — severity 가 판단이다\n\n```java\n// TopologyValidator.java:11-22\n/**\n *
Which discrepancies block is a judgement encoded here rather than left to configuration.\n * Replication factor and absence are blocking because a destination that is missing or unreplicated\n * cannot deliver the durability its profile promises. A partition count that is higher\n * than declared is advisory rather than blocking: extra partitions do not break durability, and\n * someone scaling a topic up deliberately should not be met with a refusal to start.\n *\n *
A partition count that is lower is blocking, because it silently reduces the\n * concurrency the destination was sized for and, on a keyed topic, changes which key lands where.\n */\n```\n\n| 조건 | severity | 코드 |\n|---|---|---|\n| 목적지 부재 | BLOCKING (그리고 즉시 반환) | `:39-43` |\n| `physicalName` 불일치 | BLOCKING | `:45-49` |\n| 파티션 < 선언 | BLOCKING | `:51-57` |\n| 파티션 > 선언 | **ADVISORY** | `:58-66` |\n| 복제 계수 < 선언 | BLOCKING | `:68-75` |\n| 필수 설정 불일치/부재 | BLOCKING (`\"unset\"`) | `:77-87` |\n\n부재 시 즉시 반환하는 것도 옳다 — 없는 목적지의 파티션 수를 보고할 이유가 없다.\n" }, "context_range": { "start_line": 25506, "end_line": 25618 }, "context_lines": [ { "line": 25506, "text": "##### 4.3 `ReplayService` — 안전한 형태를 공짜로 만든다" }, { "line": 25507, "text": "" }, { "line": 25508, "text": "```java" }, { "line": 25509, "text": "// ReplayService.java:13-22" }, { "line": 25510, "text": "/**" }, { "line": 25511, "text": " *
An isolated replay reads alongside the live consumer and needs no approval, because it changes" }, { "line": 25512, "text": " * nothing: a throwaway group has its own offsets. Replaying into an existing production group is a" }, { "line": 25513, "text": " * different operation entirely — it rewinds a live consumer and reprocesses everything since — so" }, { "line": 25514, "text": " * it goes through the destructive guard." }, { "line": 25515, "text": " *" }, { "line": 25516, "text": " *
Making the safe form free and the destructive form approved is what keeps operators from" }, { "line": 25517, "text": " * reaching for the destructive one out of convenience." }, { "line": 25518, "text": " */" }, { "line": 25519, "text": "```" }, { "line": 25520, "text": "" }, { "line": 25521, "text": "판단은 옳다. 구현이 그 판단을 `dryRun` 파라미터로 표현한다." }, { "line": 25522, "text": "" }, { "line": 25523, "text": "```java" }, { "line": 25524, "text": "// :58-64" }, { "line": 25525, "text": "boolean needsApproval = !request.isolatedConsumerGroup();" }, { "line": 25526, "text": "guard.authorize(" }, { "line": 25527, "text": " DestructiveOperation.REPLAY," }, { "line": 25528, "text": " request.destination()," }, { "line": 25529, "text": " approval," }, { "line": 25530, "text": " request.dryRun() || !needsApproval, // <- guard 의 dryRun 인자" }, { "line": 25531, "text": " now);" }, { "line": 25532, "text": "```" }, { "line": 25533, "text": "" }, { "line": 25534, "text": "`DestructiveOperationGuard.authorize` 는 `dryRun` 이 참이면 즉시 반환한다(`DestructiveOperationGuard.java:54-56`). 즉 격리 리플레이는 \"승인 불필요\" 가 아니라 \"dry run 인 척\" 으로 통과한다. 감사 이벤트는 그 구분을 남긴다 — `approval.map(VerifiedApproval::ticket).orElse(\"isolated\")`(`:77`) — 그러나 guard 쪽에는 남지 않는다. §17 P3." }, { "line": 25535, "text": "" }, { "line": 25536, "text": "##### 4.4 `InMemoryAdminOperationJournal` — 프로토콜이 단순화되지 않았다" }, { "line": 25537, "text": "" }, { "line": 25538, "text": "```java" }, { "line": 25539, "text": "// InMemoryAdminOperationJournal.java:17-27" }, { "line": 25540, "text": "/**" }, { "line": 25541, "text": " * A single-process journal, for tests and for local development." }, { "line": 25542, "text": " *" }, { "line": 25543, "text": " *
It reports {@link #isDurable()} as false, and the starter refuses to run a production profile" }, { "line": 25544, "text": " * on a journal that says so. That declaration is the point of this class existing at all: the" }, { "line": 25545, "text": " * previous in-memory store was registered as the production default and nothing distinguished it" }, { "line": 25546, "text": " * from a shared one, so the gap was invisible until two replicas executed the same approval." }, { "line": 25547, "text": " *" }, { "line": 25548, "text": " *
The semantics are otherwise the real ones — uniqueness on {@code (ticket, digest)}, lease" }, { "line": 25549, "text": " * takeover with a monotonic token, resume from checkpoint — so a test that passes here is testing" }, { "line": 25550, "text": " * the protocol rather than a simplification of it." }, { "line": 25551, "text": " */" }, { "line": 25552, "text": "```" }, { "line": 25553, "text": "" }, { "line": 25554, "text": "마지막 문장이 지켜지는지가 이 클래스의 값어치다. 확인 결과 지켜진다." }, { "line": 25555, "text": "" }, { "line": 25556, "text": "`begin` 의 `claim(...)` 이 네 갈래다(`:64-115`)." }, { "line": 25557, "text": "" }, { "line": 25558, "text": "| 기존 상태 | 처리 | 코드 |" }, { "line": 25559, "text": "|---|---|---|" }, { "line": 25560, "text": "| 없음 | 새 record, token=1, itemsCompleted=0 | `:72-85` |" }, { "line": 25561, "text": "| `COMPLETED` | `APPROVAL_ALREADY_EXECUTED` — \"an approval authorises one execution, not a standing permission\" | `:86-92` |" }, { "line": 25562, "text": "| `STARTED` + 리스 유효 | `ADMIN_OPERATION_IN_FLIGHT` — \"two runtimes executing one approval is a duplicate storm, not a faster redrive\" | `:93-100` |" }, { "line": 25563, "text": "| `FAILED` 또는 리스 만료 | 체크포인트 유지, **token+1** 로 인수 | `:101-114` |" }, { "line": 25564, "text": "" }, { "line": 25565, "text": "펜스는 `update(...)` 에 있다." }, { "line": 25566, "text": "" }, { "line": 25567, "text": "```java" }, { "line": 25568, "text": "// :206-214" }, { "line": 25569, "text": "if (current.leaseToken() != lease.leaseToken()) {" }, { "line": 25570, "text": " // The fence. A stalled runtime that wakes up and writes here would otherwise overwrite" }, { "line": 25571, "text": " // the progress of whichever replica took the operation over." }, { "line": 25572, "text": " throw new MessageAuthorizationException(\"ADMIN_OPERATION_LEASE_LOST\", …);" }, { "line": 25573, "text": "}" }, { "line": 25574, "text": "```" }, { "line": 25575, "text": "" }, { "line": 25576, "text": "그리고 키 생성이 `ApprovalGrant.canonicalForm()` 의 규칙을 그대로 가져온다." }, { "line": 25577, "text": "" }, { "line": 25578, "text": "```java" }, { "line": 25579, "text": "// :219-225" }, { "line": 25580, "text": "private static String key(String approvalTicket, PlanDigest planDigest) {" }, { "line": 25581, "text": " // Length-prefixed for the same reason the grant's canonical form is: a ticket containing the" }, { "line": 25582, "text": " // separator must not be able to collide with a different ticket and digest pair." }, { "line": 25583, "text": " return String.join(\"\", Integer.toString(approvalTicket.length()), \":\", approvalTicket," }, { "line": 25584, "text": " planDigest.value());" }, { "line": 25585, "text": "}" }, { "line": 25586, "text": "```" }, { "line": 25587, "text": "" }, { "line": 25588, "text": "길이 접두 규칙이 `messaging-admin-api` 밖으로 전파된 사례다. (그 규칙이 **닿지 않은** 유일한 곳이 계획 다이제스트라는 점은 §A19-MESSAGING-ADMIN-API §12.3(b)에 있다.)" }, { "line": 25589, "text": "" }, { "line": 25590, "text": "`checkpoint`/`complete`/`fail` 셋 다 `Math.max(current.itemsCompleted(), itemsCompleted)` 로 clamp 한다(`:128, :147, :167`). 이것이 `DefaultMessagingAdminService` 가 `journal.fail(lease, lease.resumeFrom(), …)` 로 **낡은 값**을 넘겨도 진행이 되돌아가지 않는 이유다. §12.4(c)." }, { "line": 25591, "text": "" }, { "line": 25592, "text": "##### 4.5 `TopologyValidator` — severity 가 판단이다" }, { "line": 25593, "text": "" }, { "line": 25594, "text": "```java" }, { "line": 25595, "text": "// TopologyValidator.java:11-22" }, { "line": 25596, "text": "/**" }, { "line": 25597, "text": " *
Which discrepancies block is a judgement encoded here rather than left to configuration." }, { "line": 25598, "text": " * Replication factor and absence are blocking because a destination that is missing or unreplicated" }, { "line": 25599, "text": " * cannot deliver the durability its profile promises. A partition count that is higher" }, { "line": 25600, "text": " * than declared is advisory rather than blocking: extra partitions do not break durability, and" }, { "line": 25601, "text": " * someone scaling a topic up deliberately should not be met with a refusal to start." }, { "line": 25602, "text": " *" }, { "line": 25603, "text": " *
A partition count that is lower is blocking, because it silently reduces the" }, { "line": 25604, "text": " * concurrency the destination was sized for and, on a keyed topic, changes which key lands where." }, { "line": 25605, "text": " */" }, { "line": 25606, "text": "```" }, { "line": 25607, "text": "" }, { "line": 25608, "text": "| 조건 | severity | 코드 |" }, { "line": 25609, "text": "|---|---|---|" }, { "line": 25610, "text": "| 목적지 부재 | BLOCKING (그리고 즉시 반환) | `:39-43` |" }, { "line": 25611, "text": "| `physicalName` 불일치 | BLOCKING | `:45-49` |" }, { "line": 25612, "text": "| 파티션 < 선언 | BLOCKING | `:51-57` |" }, { "line": 25613, "text": "| 파티션 > 선언 | **ADVISORY** | `:58-66` |" }, { "line": 25614, "text": "| 복제 계수 < 선언 | BLOCKING | `:68-75` |" }, { "line": 25615, "text": "| 필수 설정 불일치/부재 | BLOCKING (`\"unset\"`) | `:77-87` |" }, { "line": 25616, "text": "" }, { "line": 25617, "text": "부재 시 즉시 반환하는 것도 옳다 — 없는 목적지의 파티션 수를 보고할 이유가 없다." }, { "line": 25618, "text": "" } ], "numbered_context": "25506 | ##### 4.3 `ReplayService` — 안전한 형태를 공짜로 만든다\n25507 | \n25508 | ```java\n25509 | // ReplayService.java:13-22\n25510 | /**\n25511 | *
An isolated replay reads alongside the live consumer and needs no approval, because it changes\n25512 | * nothing: a throwaway group has its own offsets. Replaying into an existing production group is a\n25513 | * different operation entirely — it rewinds a live consumer and reprocesses everything since — so\n25514 | * it goes through the destructive guard.\n25515 | *\n25516 | *
Making the safe form free and the destructive form approved is what keeps operators from\n25517 | * reaching for the destructive one out of convenience.\n25518 | */\n25519 | ```\n25520 | \n25521 | 판단은 옳다. 구현이 그 판단을 `dryRun` 파라미터로 표현한다.\n25522 | \n25523 | ```java\n25524 | // :58-64\n25525 | boolean needsApproval = !request.isolatedConsumerGroup();\n25526 | guard.authorize(\n25527 | DestructiveOperation.REPLAY,\n25528 | request.destination(),\n25529 | approval,\n25530 | request.dryRun() || !needsApproval, // <- guard 의 dryRun 인자\n25531 | now);\n25532 | ```\n25533 | \n25534 | `DestructiveOperationGuard.authorize` 는 `dryRun` 이 참이면 즉시 반환한다(`DestructiveOperationGuard.java:54-56`). 즉 격리 리플레이는 \"승인 불필요\" 가 아니라 \"dry run 인 척\" 으로 통과한다. 감사 이벤트는 그 구분을 남긴다 — `approval.map(VerifiedApproval::ticket).orElse(\"isolated\")`(`:77`) — 그러나 guard 쪽에는 남지 않는다. §17 P3.\n25535 | \n25536 | ##### 4.4 `InMemoryAdminOperationJournal` — 프로토콜이 단순화되지 않았다\n25537 | \n25538 | ```java\n25539 | // InMemoryAdminOperationJournal.java:17-27\n25540 | /**\n25541 | * A single-process journal, for tests and for local development.\n25542 | *\n25543 | *
It reports {@link #isDurable()} as false, and the starter refuses to run a production profile\n25544 | * on a journal that says so. That declaration is the point of this class existing at all: the\n25545 | * previous in-memory store was registered as the production default and nothing distinguished it\n25546 | * from a shared one, so the gap was invisible until two replicas executed the same approval.\n25547 | *\n25548 | *
The semantics are otherwise the real ones — uniqueness on {@code (ticket, digest)}, lease\n25549 | * takeover with a monotonic token, resume from checkpoint — so a test that passes here is testing\n25550 | * the protocol rather than a simplification of it.\n25551 | */\n25552 | ```\n25553 | \n25554 | 마지막 문장이 지켜지는지가 이 클래스의 값어치다. 확인 결과 지켜진다.\n25555 | \n25556 | `begin` 의 `claim(...)` 이 네 갈래다(`:64-115`).\n25557 | \n25558 | | 기존 상태 | 처리 | 코드 |\n25559 | |---|---|---|\n25560 | | 없음 | 새 record, token=1, itemsCompleted=0 | `:72-85` |\n25561 | | `COMPLETED` | `APPROVAL_ALREADY_EXECUTED` — \"an approval authorises one execution, not a standing permission\" | `:86-92` |\n25562 | | `STARTED` + 리스 유효 | `ADMIN_OPERATION_IN_FLIGHT` — \"two runtimes executing one approval is a duplicate storm, not a faster redrive\" | `:93-100` |\n25563 | | `FAILED` 또는 리스 만료 | 체크포인트 유지, **token+1** 로 인수 | `:101-114` |\n25564 | \n25565 | 펜스는 `update(...)` 에 있다.\n25566 | \n25567 | ```java\n25568 | // :206-214\n25569 | if (current.leaseToken() != lease.leaseToken()) {\n25570 | // The fence. A stalled runtime that wakes up and writes here would otherwise overwrite\n25571 | // the progress of whichever replica took the operation over.\n25572 | throw new MessageAuthorizationException(\"ADMIN_OPERATION_LEASE_LOST\", …);\n25573 | }\n25574 | ```\n25575 | \n25576 | 그리고 키 생성이 `ApprovalGrant.canonicalForm()` 의 규칙을 그대로 가져온다.\n25577 | \n25578 | ```java\n25579 | // :219-225\n25580 | private static String key(String approvalTicket, PlanDigest planDigest) {\n25581 | // Length-prefixed for the same reason the grant's canonical form is: a ticket containing the\n25582 | // separator must not be able to collide with a different ticket and digest pair.\n25583 | return String.join(\"\", Integer.toString(approvalTicket.length()), \":\", approvalTicket,\n25584 | planDigest.value());\n25585 | }\n25586 | ```\n25587 | \n25588 | 길이 접두 규칙이 `messaging-admin-api` 밖으로 전파된 사례다. (그 규칙이 **닿지 않은** 유일한 곳이 계획 다이제스트라는 점은 §A19-MESSAGING-ADMIN-API §12.3(b)에 있다.)\n25589 | \n25590 | `checkpoint`/`complete`/`fail` 셋 다 `Math.max(current.itemsCompleted(), itemsCompleted)` 로 clamp 한다(`:128, :147, :167`). 이것이 `DefaultMessagingAdminService` 가 `journal.fail(lease, lease.resumeFrom(), …)` 로 **낡은 값**을 넘겨도 진행이 되돌아가지 않는 이유다. §12.4(c).\n25591 | \n25592 | ##### 4.5 `TopologyValidator` — severity 가 판단이다\n25593 | \n25594 | ```java\n25595 | // TopologyValidator.java:11-22\n25596 | /**\n25597 | *
Which discrepancies block is a judgement encoded here rather than left to configuration.\n25598 | * Replication factor and absence are blocking because a destination that is missing or unreplicated\n25599 | * cannot deliver the durability its profile promises. A partition count that is higher\n25600 | * than declared is advisory rather than blocking: extra partitions do not break durability, and\n25601 | * someone scaling a topic up deliberately should not be met with a refusal to start.\n25602 | *\n25603 | *
A partition count that is lower is blocking, because it silently reduces the\n25604 | * concurrency the destination was sized for and, on a keyed topic, changes which key lands where.\n25605 | */\n25606 | ```\n25607 | \n25608 | | 조건 | severity | 코드 |\n25609 | |---|---|---|\n25610 | | 목적지 부재 | BLOCKING (그리고 즉시 반환) | `:39-43` |\n25611 | | `physicalName` 불일치 | BLOCKING | `:45-49` |\n25612 | | 파티션 < 선언 | BLOCKING | `:51-57` |\n25613 | | 파티션 > 선언 | **ADVISORY** | `:58-66` |\n25614 | | 복제 계수 < 선언 | BLOCKING | `:68-75` |\n25615 | | 필수 설정 불일치/부재 | BLOCKING (`\"unset\"`) | `:77-87` |\n25616 | \n25617 | 부재 시 즉시 반환하는 것도 옳다 — 없는 목적지의 파티션 수를 보고할 이유가 없다.\n25618 | ",
"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