{ "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": 24574, "line": 24574 }, "current_section": { "heading": { "line": 24546, "level": 5, "text": "4.5 실행 저널 — 리스와 펜싱 토큰" }, "start_line": 24546, "end_line": 24598, "text": "##### 4.5 실행 저널 — 리스와 펜싱 토큰\n\n```java\n// AdminOperationJournal.java:7-18\n/**\n *
The implementation this replaced was a {@code ConcurrentHashMap} registered by the starter as\n * the default. Two consequences followed, and both are worse than having no store at all because\n * the map made the platform look protected. A restart forgot every claim, so the same approval\n * could be executed again by the same process. And two replicas each had their own map, so both\n * could claim the same approval at the same moment and each believe it was the only one.\n *\n *
Implementations must therefore be shared and durable, must enforce uniqueness on {@code\n * (approvalTicket, planDigest)}, and must hand out leases with a monotonic fencing token so a\n * process that stalled past its lease cannot write over the replica that took over from it.\n */\n```\n\n키를 `(approvalTicket, planDigest)` 로 잡은 이유:\n\n```java\n// AdminOperationRecord.java:9-12\n/**\n *
Keyed by {@code (approvalTicket, planDigest)} rather than by the ticket alone, because the\n * ticket alone cannot distinguish \"this approval already ran\" from \"this approval is being reused\n * for a different plan\" — and those need opposite answers.\n */\n```\n\n리스가 불리언이 아니라 `resumeFrom` 을 나르는 이유:\n\n```java\n// AdminOperationLease.java:8-11\n/**\n *
{@code resumeFrom} is the whole reason a lease is handed out rather than a boolean. A retry\n * after a crash is not a new execution of the approval — it is the same operation continuing, and\n * treating it as new is what republishes the items the first attempt already moved.\n */\n```\n\n상태 세 개(`STARTED`/`COMPLETED`/`FAILED`)를 만든 이유:\n\n```java\n// AdminOperationState.java:5-9\n/**\n *
The store this replaces recorded one fact — \"this approval was claimed\" — and recorded it\n * before any work happened. A run that died halfway had consumed its approval, left no record of\n * how far it got, and offered the operator two equally bad choices: request a fresh approval and\n * redo work that may already have been done, or leave the operation half-applied.\n */\n```\n\n`isDurable()` 을 인터페이스에 둔 이유도 명시적이다 — \"Declared rather than inferred so the starter can refuse to run a production profile on an in-memory journal instead of discovering the gap during an incident.\" 이 선언은 실제로 배선되어 있다(§12.2 의 대조군).\n" }, "previous_section": { "heading": { "line": 24490, "level": 5, "text": "4.4 계획 → 승인된 계획: 생성자에서 네 가지, 실행 직전에 세 가지" }, "start_line": 24490, "end_line": 24545, "text": "##### 4.4 계획 → 승인된 계획: 생성자에서 네 가지, 실행 직전에 세 가지\n\n`ApprovedReplayPlan` 생성자(`:19-52`):\n\n| 검사 | 실패 코드 |\n|---|---|\n| `approval.grant().planDigest() == plan.digest()` | `APPROVAL_PLAN_MISMATCH` |\n| `operation == REPLAY` | `APPROVAL_OPERATION_MISMATCH` |\n| `grant().source() == plan.request().destination()` | `APPROVAL_SOURCE_MISMATCH` |\n| `plan.estimatedMessages() <= grant().maxImpact()` | `APPROVAL_IMPACT_EXCEEDED` |\n\n`requireExecutable(now, currentTopologyVersion)`(`:54-85`):\n\n| 검사 | 실패 코드 |\n|---|---|\n| 승인 윈도우 | `APPROVAL_EXPIRED` |\n| `grant().topologyVersion() == 현재` | `TOPOLOGY_CHANGED_SINCE_APPROVAL` |\n| `plan.topologyVersion() == 현재` | `TOPOLOGY_CHANGED_SINCE_APPROVAL` |\n\n토폴로지를 **두 번** 보는 이유가 주석에 있다.\n\n```java\n// ApprovedReplayPlan.java:76-84\nif (!plan.topologyVersion().equals(currentTopologyVersion)) {\n // Every number in the plan was computed against the old topology, so the approver agreed to\n // an impact estimate that no longer describes what would happen.\n```\n\n승인이 서명된 토폴로지와 계획이 계산된 토폴로지가 다를 수 있으므로 둘 다 현재와 대조한다.\n\n`ApprovedRedrivePlan` 은 같은 네 검사에 더해 `loopAcknowledged` 를 별도 필드로 갖는다.\n\n```java\n// ApprovedRedrivePlan.java:8-19\n/**\n *
Carries {@code loopAcknowledged} separately from the approval itself. Approving a redrive of\n * 900 parked messages and approving a redrive that will re-fail 400 of them are different\n * decisions, and the second one needs the approver to have seen the number …\n */\n```\n\n그리고 `requireExecutable` 의 마지막 분기가 그것을 강제한다.\n\n```java\n// :87-93\nif (plan.risksALoop() && !loopAcknowledged) {\n throw new MessageAuthorizationException(\n \"REDRIVE_LOOP_NOT_ACKNOWLEDGED\",\n \"%d of the %d candidates already failed a previous redrive; re-running them without \"\n .formatted(plan.alreadyRedrivenCandidates(), plan.candidates())\n + \"fixing the cause produces a loop that looks like progress\");\n}\n```\n\n\"진행처럼 보이는 루프\" 는 이 리프에서 반복되는 관점이다 — 대시보드에서 옳아 보이는 실패를 타입으로 막는다.\n" }, "next_section": { "heading": { "line": 24599, "level": 5, "text": "4.6 토폴로지 — 선언과 실측을 다른 타입으로" }, "start_line": 24599, "end_line": 24640, "text": "##### 4.6 토폴로지 — 선언과 실측을 다른 타입으로\n\n```java\n// DestinationTopology.java:6-11\n/**\n * What the broker actually reports for one destination.\n *\n *
The counterpart to {@link TopologyManifest}: the manifest is what was declared, this is what\n * exists. Kept as a separate type rather than reusing the manifest so that a comparison cannot\n * accidentally compare a manifest with itself and report success.\n */\n```\n\n\"자기 자신과 비교해서 성공을 보고하는 것\" 을 타입으로 막았다 — `messaging-testkit` 의 인증 행렬이 고친 결함과 정확히 같은 형태이며, 여기서는 처음부터 타입으로 예방했다.\n\n심각도를 finding 에 붙인 이유:\n\n```java\n// TopologyIssue.java:8-12\n/**\n *
Severity is part of the finding because the two kinds behave differently at startup. A {@link\n * Severity#BLOCKING} issue means the destination cannot deliver its declared guarantee —\n * replication factor 1 on a destination promising durability is not a warning, it is a promise the\n * platform cannot keep — so the context refuses to start. …\n */\n```\n\n전부 모아 보고하는 이유:\n\n```java\n// TopologyValidationReport.java:10-12\n/**\n *
Reports both severities together rather than failing on the first blocking issue. An operator\n * fixing a topology wants the whole list — fixing one attribute, redeploying, and discovering the\n * next one is how a ten-minute fix becomes an afternoon.\n */\n```\n\n두 문장 모두 \"기동을 거부한다\" 를 전제한다. 그 전제가 배선되지 않았다 — §12.1.\n\n---\n" }, "context_range": { "start_line": 24490, "end_line": 24640 }, "context_lines": [ { "line": 24490, "text": "##### 4.4 계획 → 승인된 계획: 생성자에서 네 가지, 실행 직전에 세 가지" }, { "line": 24491, "text": "" }, { "line": 24492, "text": "`ApprovedReplayPlan` 생성자(`:19-52`):" }, { "line": 24493, "text": "" }, { "line": 24494, "text": "| 검사 | 실패 코드 |" }, { "line": 24495, "text": "|---|---|" }, { "line": 24496, "text": "| `approval.grant().planDigest() == plan.digest()` | `APPROVAL_PLAN_MISMATCH` |" }, { "line": 24497, "text": "| `operation == REPLAY` | `APPROVAL_OPERATION_MISMATCH` |" }, { "line": 24498, "text": "| `grant().source() == plan.request().destination()` | `APPROVAL_SOURCE_MISMATCH` |" }, { "line": 24499, "text": "| `plan.estimatedMessages() <= grant().maxImpact()` | `APPROVAL_IMPACT_EXCEEDED` |" }, { "line": 24500, "text": "" }, { "line": 24501, "text": "`requireExecutable(now, currentTopologyVersion)`(`:54-85`):" }, { "line": 24502, "text": "" }, { "line": 24503, "text": "| 검사 | 실패 코드 |" }, { "line": 24504, "text": "|---|---|" }, { "line": 24505, "text": "| 승인 윈도우 | `APPROVAL_EXPIRED` |" }, { "line": 24506, "text": "| `grant().topologyVersion() == 현재` | `TOPOLOGY_CHANGED_SINCE_APPROVAL` |" }, { "line": 24507, "text": "| `plan.topologyVersion() == 현재` | `TOPOLOGY_CHANGED_SINCE_APPROVAL` |" }, { "line": 24508, "text": "" }, { "line": 24509, "text": "토폴로지를 **두 번** 보는 이유가 주석에 있다." }, { "line": 24510, "text": "" }, { "line": 24511, "text": "```java" }, { "line": 24512, "text": "// ApprovedReplayPlan.java:76-84" }, { "line": 24513, "text": "if (!plan.topologyVersion().equals(currentTopologyVersion)) {" }, { "line": 24514, "text": " // Every number in the plan was computed against the old topology, so the approver agreed to" }, { "line": 24515, "text": " // an impact estimate that no longer describes what would happen." }, { "line": 24516, "text": "```" }, { "line": 24517, "text": "" }, { "line": 24518, "text": "승인이 서명된 토폴로지와 계획이 계산된 토폴로지가 다를 수 있으므로 둘 다 현재와 대조한다." }, { "line": 24519, "text": "" }, { "line": 24520, "text": "`ApprovedRedrivePlan` 은 같은 네 검사에 더해 `loopAcknowledged` 를 별도 필드로 갖는다." }, { "line": 24521, "text": "" }, { "line": 24522, "text": "```java" }, { "line": 24523, "text": "// ApprovedRedrivePlan.java:8-19" }, { "line": 24524, "text": "/**" }, { "line": 24525, "text": " *
Carries {@code loopAcknowledged} separately from the approval itself. Approving a redrive of" }, { "line": 24526, "text": " * 900 parked messages and approving a redrive that will re-fail 400 of them are different" }, { "line": 24527, "text": " * decisions, and the second one needs the approver to have seen the number …" }, { "line": 24528, "text": " */" }, { "line": 24529, "text": "```" }, { "line": 24530, "text": "" }, { "line": 24531, "text": "그리고 `requireExecutable` 의 마지막 분기가 그것을 강제한다." }, { "line": 24532, "text": "" }, { "line": 24533, "text": "```java" }, { "line": 24534, "text": "// :87-93" }, { "line": 24535, "text": "if (plan.risksALoop() && !loopAcknowledged) {" }, { "line": 24536, "text": " throw new MessageAuthorizationException(" }, { "line": 24537, "text": " \"REDRIVE_LOOP_NOT_ACKNOWLEDGED\"," }, { "line": 24538, "text": " \"%d of the %d candidates already failed a previous redrive; re-running them without \"" }, { "line": 24539, "text": " .formatted(plan.alreadyRedrivenCandidates(), plan.candidates())" }, { "line": 24540, "text": " + \"fixing the cause produces a loop that looks like progress\");" }, { "line": 24541, "text": "}" }, { "line": 24542, "text": "```" }, { "line": 24543, "text": "" }, { "line": 24544, "text": "\"진행처럼 보이는 루프\" 는 이 리프에서 반복되는 관점이다 — 대시보드에서 옳아 보이는 실패를 타입으로 막는다." }, { "line": 24545, "text": "" }, { "line": 24546, "text": "##### 4.5 실행 저널 — 리스와 펜싱 토큰" }, { "line": 24547, "text": "" }, { "line": 24548, "text": "```java" }, { "line": 24549, "text": "// AdminOperationJournal.java:7-18" }, { "line": 24550, "text": "/**" }, { "line": 24551, "text": " *
The implementation this replaced was a {@code ConcurrentHashMap} registered by the starter as" }, { "line": 24552, "text": " * the default. Two consequences followed, and both are worse than having no store at all because" }, { "line": 24553, "text": " * the map made the platform look protected. A restart forgot every claim, so the same approval" }, { "line": 24554, "text": " * could be executed again by the same process. And two replicas each had their own map, so both" }, { "line": 24555, "text": " * could claim the same approval at the same moment and each believe it was the only one." }, { "line": 24556, "text": " *" }, { "line": 24557, "text": " *
Implementations must therefore be shared and durable, must enforce uniqueness on {@code" }, { "line": 24558, "text": " * (approvalTicket, planDigest)}, and must hand out leases with a monotonic fencing token so a" }, { "line": 24559, "text": " * process that stalled past its lease cannot write over the replica that took over from it." }, { "line": 24560, "text": " */" }, { "line": 24561, "text": "```" }, { "line": 24562, "text": "" }, { "line": 24563, "text": "키를 `(approvalTicket, planDigest)` 로 잡은 이유:" }, { "line": 24564, "text": "" }, { "line": 24565, "text": "```java" }, { "line": 24566, "text": "// AdminOperationRecord.java:9-12" }, { "line": 24567, "text": "/**" }, { "line": 24568, "text": " *
Keyed by {@code (approvalTicket, planDigest)} rather than by the ticket alone, because the" }, { "line": 24569, "text": " * ticket alone cannot distinguish \"this approval already ran\" from \"this approval is being reused" }, { "line": 24570, "text": " * for a different plan\" — and those need opposite answers." }, { "line": 24571, "text": " */" }, { "line": 24572, "text": "```" }, { "line": 24573, "text": "" }, { "line": 24574, "text": "리스가 불리언이 아니라 `resumeFrom` 을 나르는 이유:" }, { "line": 24575, "text": "" }, { "line": 24576, "text": "```java" }, { "line": 24577, "text": "// AdminOperationLease.java:8-11" }, { "line": 24578, "text": "/**" }, { "line": 24579, "text": " *
{@code resumeFrom} is the whole reason a lease is handed out rather than a boolean. A retry" }, { "line": 24580, "text": " * after a crash is not a new execution of the approval — it is the same operation continuing, and" }, { "line": 24581, "text": " * treating it as new is what republishes the items the first attempt already moved." }, { "line": 24582, "text": " */" }, { "line": 24583, "text": "```" }, { "line": 24584, "text": "" }, { "line": 24585, "text": "상태 세 개(`STARTED`/`COMPLETED`/`FAILED`)를 만든 이유:" }, { "line": 24586, "text": "" }, { "line": 24587, "text": "```java" }, { "line": 24588, "text": "// AdminOperationState.java:5-9" }, { "line": 24589, "text": "/**" }, { "line": 24590, "text": " *
The store this replaces recorded one fact — \"this approval was claimed\" — and recorded it" }, { "line": 24591, "text": " * before any work happened. A run that died halfway had consumed its approval, left no record of" }, { "line": 24592, "text": " * how far it got, and offered the operator two equally bad choices: request a fresh approval and" }, { "line": 24593, "text": " * redo work that may already have been done, or leave the operation half-applied." }, { "line": 24594, "text": " */" }, { "line": 24595, "text": "```" }, { "line": 24596, "text": "" }, { "line": 24597, "text": "`isDurable()` 을 인터페이스에 둔 이유도 명시적이다 — \"Declared rather than inferred so the starter can refuse to run a production profile on an in-memory journal instead of discovering the gap during an incident.\" 이 선언은 실제로 배선되어 있다(§12.2 의 대조군)." }, { "line": 24598, "text": "" }, { "line": 24599, "text": "##### 4.6 토폴로지 — 선언과 실측을 다른 타입으로" }, { "line": 24600, "text": "" }, { "line": 24601, "text": "```java" }, { "line": 24602, "text": "// DestinationTopology.java:6-11" }, { "line": 24603, "text": "/**" }, { "line": 24604, "text": " * What the broker actually reports for one destination." }, { "line": 24605, "text": " *" }, { "line": 24606, "text": " *
The counterpart to {@link TopologyManifest}: the manifest is what was declared, this is what" }, { "line": 24607, "text": " * exists. Kept as a separate type rather than reusing the manifest so that a comparison cannot" }, { "line": 24608, "text": " * accidentally compare a manifest with itself and report success." }, { "line": 24609, "text": " */" }, { "line": 24610, "text": "```" }, { "line": 24611, "text": "" }, { "line": 24612, "text": "\"자기 자신과 비교해서 성공을 보고하는 것\" 을 타입으로 막았다 — `messaging-testkit` 의 인증 행렬이 고친 결함과 정확히 같은 형태이며, 여기서는 처음부터 타입으로 예방했다." }, { "line": 24613, "text": "" }, { "line": 24614, "text": "심각도를 finding 에 붙인 이유:" }, { "line": 24615, "text": "" }, { "line": 24616, "text": "```java" }, { "line": 24617, "text": "// TopologyIssue.java:8-12" }, { "line": 24618, "text": "/**" }, { "line": 24619, "text": " *
Severity is part of the finding because the two kinds behave differently at startup. A {@link" }, { "line": 24620, "text": " * Severity#BLOCKING} issue means the destination cannot deliver its declared guarantee —" }, { "line": 24621, "text": " * replication factor 1 on a destination promising durability is not a warning, it is a promise the" }, { "line": 24622, "text": " * platform cannot keep — so the context refuses to start. …" }, { "line": 24623, "text": " */" }, { "line": 24624, "text": "```" }, { "line": 24625, "text": "" }, { "line": 24626, "text": "전부 모아 보고하는 이유:" }, { "line": 24627, "text": "" }, { "line": 24628, "text": "```java" }, { "line": 24629, "text": "// TopologyValidationReport.java:10-12" }, { "line": 24630, "text": "/**" }, { "line": 24631, "text": " *
Reports both severities together rather than failing on the first blocking issue. An operator" }, { "line": 24632, "text": " * fixing a topology wants the whole list — fixing one attribute, redeploying, and discovering the" }, { "line": 24633, "text": " * next one is how a ten-minute fix becomes an afternoon." }, { "line": 24634, "text": " */" }, { "line": 24635, "text": "```" }, { "line": 24636, "text": "" }, { "line": 24637, "text": "두 문장 모두 \"기동을 거부한다\" 를 전제한다. 그 전제가 배선되지 않았다 — §12.1." }, { "line": 24638, "text": "" }, { "line": 24639, "text": "---" }, { "line": 24640, "text": "" } ], "numbered_context": "24490 | ##### 4.4 계획 → 승인된 계획: 생성자에서 네 가지, 실행 직전에 세 가지\n24491 | \n24492 | `ApprovedReplayPlan` 생성자(`:19-52`):\n24493 | \n24494 | | 검사 | 실패 코드 |\n24495 | |---|---|\n24496 | | `approval.grant().planDigest() == plan.digest()` | `APPROVAL_PLAN_MISMATCH` |\n24497 | | `operation == REPLAY` | `APPROVAL_OPERATION_MISMATCH` |\n24498 | | `grant().source() == plan.request().destination()` | `APPROVAL_SOURCE_MISMATCH` |\n24499 | | `plan.estimatedMessages() <= grant().maxImpact()` | `APPROVAL_IMPACT_EXCEEDED` |\n24500 | \n24501 | `requireExecutable(now, currentTopologyVersion)`(`:54-85`):\n24502 | \n24503 | | 검사 | 실패 코드 |\n24504 | |---|---|\n24505 | | 승인 윈도우 | `APPROVAL_EXPIRED` |\n24506 | | `grant().topologyVersion() == 현재` | `TOPOLOGY_CHANGED_SINCE_APPROVAL` |\n24507 | | `plan.topologyVersion() == 현재` | `TOPOLOGY_CHANGED_SINCE_APPROVAL` |\n24508 | \n24509 | 토폴로지를 **두 번** 보는 이유가 주석에 있다.\n24510 | \n24511 | ```java\n24512 | // ApprovedReplayPlan.java:76-84\n24513 | if (!plan.topologyVersion().equals(currentTopologyVersion)) {\n24514 | // Every number in the plan was computed against the old topology, so the approver agreed to\n24515 | // an impact estimate that no longer describes what would happen.\n24516 | ```\n24517 | \n24518 | 승인이 서명된 토폴로지와 계획이 계산된 토폴로지가 다를 수 있으므로 둘 다 현재와 대조한다.\n24519 | \n24520 | `ApprovedRedrivePlan` 은 같은 네 검사에 더해 `loopAcknowledged` 를 별도 필드로 갖는다.\n24521 | \n24522 | ```java\n24523 | // ApprovedRedrivePlan.java:8-19\n24524 | /**\n24525 | *
Carries {@code loopAcknowledged} separately from the approval itself. Approving a redrive of\n24526 | * 900 parked messages and approving a redrive that will re-fail 400 of them are different\n24527 | * decisions, and the second one needs the approver to have seen the number …\n24528 | */\n24529 | ```\n24530 | \n24531 | 그리고 `requireExecutable` 의 마지막 분기가 그것을 강제한다.\n24532 | \n24533 | ```java\n24534 | // :87-93\n24535 | if (plan.risksALoop() && !loopAcknowledged) {\n24536 | throw new MessageAuthorizationException(\n24537 | \"REDRIVE_LOOP_NOT_ACKNOWLEDGED\",\n24538 | \"%d of the %d candidates already failed a previous redrive; re-running them without \"\n24539 | .formatted(plan.alreadyRedrivenCandidates(), plan.candidates())\n24540 | + \"fixing the cause produces a loop that looks like progress\");\n24541 | }\n24542 | ```\n24543 | \n24544 | \"진행처럼 보이는 루프\" 는 이 리프에서 반복되는 관점이다 — 대시보드에서 옳아 보이는 실패를 타입으로 막는다.\n24545 | \n24546 | ##### 4.5 실행 저널 — 리스와 펜싱 토큰\n24547 | \n24548 | ```java\n24549 | // AdminOperationJournal.java:7-18\n24550 | /**\n24551 | *
The implementation this replaced was a {@code ConcurrentHashMap} registered by the starter as\n24552 | * the default. Two consequences followed, and both are worse than having no store at all because\n24553 | * the map made the platform look protected. A restart forgot every claim, so the same approval\n24554 | * could be executed again by the same process. And two replicas each had their own map, so both\n24555 | * could claim the same approval at the same moment and each believe it was the only one.\n24556 | *\n24557 | *
Implementations must therefore be shared and durable, must enforce uniqueness on {@code\n24558 | * (approvalTicket, planDigest)}, and must hand out leases with a monotonic fencing token so a\n24559 | * process that stalled past its lease cannot write over the replica that took over from it.\n24560 | */\n24561 | ```\n24562 | \n24563 | 키를 `(approvalTicket, planDigest)` 로 잡은 이유:\n24564 | \n24565 | ```java\n24566 | // AdminOperationRecord.java:9-12\n24567 | /**\n24568 | *
Keyed by {@code (approvalTicket, planDigest)} rather than by the ticket alone, because the\n24569 | * ticket alone cannot distinguish \"this approval already ran\" from \"this approval is being reused\n24570 | * for a different plan\" — and those need opposite answers.\n24571 | */\n24572 | ```\n24573 | \n24574 | 리스가 불리언이 아니라 `resumeFrom` 을 나르는 이유:\n24575 | \n24576 | ```java\n24577 | // AdminOperationLease.java:8-11\n24578 | /**\n24579 | *
{@code resumeFrom} is the whole reason a lease is handed out rather than a boolean. A retry\n24580 | * after a crash is not a new execution of the approval — it is the same operation continuing, and\n24581 | * treating it as new is what republishes the items the first attempt already moved.\n24582 | */\n24583 | ```\n24584 | \n24585 | 상태 세 개(`STARTED`/`COMPLETED`/`FAILED`)를 만든 이유:\n24586 | \n24587 | ```java\n24588 | // AdminOperationState.java:5-9\n24589 | /**\n24590 | *
The store this replaces recorded one fact — \"this approval was claimed\" — and recorded it\n24591 | * before any work happened. A run that died halfway had consumed its approval, left no record of\n24592 | * how far it got, and offered the operator two equally bad choices: request a fresh approval and\n24593 | * redo work that may already have been done, or leave the operation half-applied.\n24594 | */\n24595 | ```\n24596 | \n24597 | `isDurable()` 을 인터페이스에 둔 이유도 명시적이다 — \"Declared rather than inferred so the starter can refuse to run a production profile on an in-memory journal instead of discovering the gap during an incident.\" 이 선언은 실제로 배선되어 있다(§12.2 의 대조군).\n24598 | \n24599 | ##### 4.6 토폴로지 — 선언과 실측을 다른 타입으로\n24600 | \n24601 | ```java\n24602 | // DestinationTopology.java:6-11\n24603 | /**\n24604 | * What the broker actually reports for one destination.\n24605 | *\n24606 | *
The counterpart to {@link TopologyManifest}: the manifest is what was declared, this is what\n24607 | * exists. Kept as a separate type rather than reusing the manifest so that a comparison cannot\n24608 | * accidentally compare a manifest with itself and report success.\n24609 | */\n24610 | ```\n24611 | \n24612 | \"자기 자신과 비교해서 성공을 보고하는 것\" 을 타입으로 막았다 — `messaging-testkit` 의 인증 행렬이 고친 결함과 정확히 같은 형태이며, 여기서는 처음부터 타입으로 예방했다.\n24613 | \n24614 | 심각도를 finding 에 붙인 이유:\n24615 | \n24616 | ```java\n24617 | // TopologyIssue.java:8-12\n24618 | /**\n24619 | *
Severity is part of the finding because the two kinds behave differently at startup. A {@link\n24620 | * Severity#BLOCKING} issue means the destination cannot deliver its declared guarantee —\n24621 | * replication factor 1 on a destination promising durability is not a warning, it is a promise the\n24622 | * platform cannot keep — so the context refuses to start. …\n24623 | */\n24624 | ```\n24625 | \n24626 | 전부 모아 보고하는 이유:\n24627 | \n24628 | ```java\n24629 | // TopologyValidationReport.java:10-12\n24630 | /**\n24631 | *
Reports both severities together rather than failing on the first blocking issue. An operator\n24632 | * fixing a topology wants the whole list — fixing one attribute, redeploying, and discovering the\n24633 | * next one is how a ten-minute fix becomes an afternoon.\n24634 | */\n24635 | ```\n24636 | \n24637 | 두 문장 모두 \"기동을 거부한다\" 를 전제한다. 그 전제가 배선되지 않았다 — §12.1.\n24638 | \n24639 | ---\n24640 | ",
"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