{ "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": 31657, "line": 31657 }, "current_section": { "heading": { "line": 31657, "level": 4, "text": "7. 트랜잭션·동시성·수명주기" }, "start_line": 31657, "end_line": 31674, "text": "#### 7. 트랜잭션·동시성·수명주기\n\n**두 가지 커넥션 획득 방식이 공존한다.**\n\n| 메서드 | 획득 | 효과 |\n|---|---|---|\n| `JdbcOutboxRepository.append(record)` | `DataSourceUtils.getConnection` | 호출자 트랜잭션에 합류 |\n| 그 외 전부 (`withConnection`) | `dataSource.getConnection()` + try-with-resources | 풀에서 새 커넥션, 독립 커밋 |\n| `JdbcAdminOperationJournal` 전 메서드 | `DataSourceUtils.getConnection` | 트랜잭션 있으면 합류 |\n\n릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 되므로 `withConnection` 의 선택은 타당하다. 다만 그 판단이 주석으로 남아 있지 않고, 같은 리프의 저널은 반대 방식을 쓴다. §17 P3.\n\n**동시성 제어는 전부 데이터베이스에 있다.** `FOR UPDATE SKIP LOCKED`(청구), 서버측 토큰 증가, 펜싱 술어, `ON CONFLICT DO NOTHING`, 복합 기본키. Java 쪽에 락이 없다.\n\n**수명주기**: `OutboxRelayWorker` 는 데몬 스레드 1개, `setExecuteExistingDelayedTasksAfterShutdownPolicy(false)`, `start()` 멱등, `stop(deadline)` 드레인 후 실패 시 `shutdownNow()`. 셋 다 근거 주석이 있다(`:79-88`, `:92`, `:121-122`).\n\n---\n" }, "previous_section": { "heading": { "line": 31613, "level": 4, "text": "6. 실패 경로와 복구/번역" }, "start_line": 31613, "end_line": 31656, "text": "#### 6. 실패 경로와 복구/번역\n\n| 상황 | 처리 | 위치 |\n|---|---|---|\n| 트랜잭션 없이 append | `OUTBOX_TRANSACTION_REQUIRED` | `JdbcOutboxRepository:228-235` |\n| 읽기 전용 트랜잭션 | 〃 | `:236-239` |\n| 다른 DataSource 의 트랜잭션 | 〃 | `:240-246` |\n| append SQL 실패 | `OUTBOX_APPEND_FAILED` | `:196-199` |\n| 그 밖의 쿼리 실패 | `OUTBOX_QUERY_FAILED` | `:594-600` |\n| 종결 쓰기가 0행 | `OutboxTransitionResult.STALE_LEASE` (예외 아님) | `:413-415` |\n| 미지의 `PublishCompletion` | `IllegalStateException` | `OutboxRelay:216-217` |\n| 리스가 발행 타임아웃보다 짧음 | `OUTBOX_LEASE_TOO_SHORT` | `OutboxProperties:52-58` |\n| 행 헤더에 예약 이름 | `RESERVED_HEADER_IN_OUTBOX_ROW` | `OutboxEnvelopeFactory:70-77` |\n| 승인 이미 실행됨 | `APPROVAL_ALREADY_EXECUTED` | `JdbcAdminOperationJournal:117-124` |\n| 다른 런타임이 실행 중 | `ADMIN_OPERATION_IN_FLIGHT` | `:125-132`, `:181-186` |\n| 리스 상실 후 쓰기 | `ADMIN_OPERATION_LEASE_LOST` | `:263-271` |\n| 저널 도달 불가 | `ADMIN_JOURNAL_UNAVAILABLE` | `:206-208` 등 |\n| 두 릴레이 동시 활성 | `DUPLICATE_OUTBOX_RELAY` | `DebeziumOutboxProfile:54-59` (**호출부 0**) |\n| 릴레이 없음 | `NO_OUTBOX_RELAY` | `:60-65` (**호출부 0**) |\n\n`OutboxRelayWorker` 의 패스 실패 처리가 특히 명시적이다.\n\n```java\n// :184-190\n} catch (RuntimeException passFailed) {\n // A failed pass must not stop the loop: the scheduled task's own exception would cancel every\n // future pass, turning one broker error into a relay that never runs again. The failure is\n // counted and the next pass backs off as if nothing was published, which is true.\n```\n\n종료도 인터럽트가 아니라 드레인이다.\n\n```java\n// :99-106\n/**\n *
Draining rather than interrupting is the whole point. A pass killed between its claim and\n * its terminal write leaves rows {@code IN_FLIGHT} holding a lease, and nothing may touch them\n * until that lease expires — so an orderly shutdown would produce exactly the stall that a crash\n * produces.\n */\n```\n\n---\n" }, "next_section": { "heading": { "line": 31675, "level": 4, "text": "8. 설정·기능 플래그·환경 차이" }, "start_line": 31675, "end_line": 31693, "text": "#### 8. 설정·기능 플래그·환경 차이\n\n| 값 | 출처 | 기본 | 비고 |\n|---|---|---|---|\n| `batchSize` | `OutboxProperties` | 100 | |\n| `leaseDuration` | 〃 | 30초 | `>= publishTimeout × 2` 강제 |\n| `publishTimeout` | 〃 | 5초 | |\n| `pollInterval` | 〃 | 500ms | 백오프의 기준 간격 |\n| `retentionAfterPublish` | 〃 | 3일 | |\n| `maxAttempts` | 〃 | 10 | 청구 술어의 `attempts < ?` |\n| `maxInterval` | starter `:63` | **1분** | `OutboxRetryScheduler.standard()` 는 5분 |\n| `maxBatches` | starter `:141` | **20 하드코딩** | 실질 무의미 (§12.1(a)) |\n| relay owner | `OutboxRelay.defaultOwner()` | `pid@uuid8` | 프로세스당 안정 |\n| CDC 모드 | `DebeziumOutboxProfile` | — | **어떤 프로퍼티에도 연결 안 됨** |\n\n`maxInterval` 이 두 값(1분 / 5분)으로 갈리는 것은 결함이 아니다 — starter 가 명시적으로 넘기고, `standard()` 는 호출자가 정책을 주지 않은 경우의 기본값이다.\n\n---\n" }, "context_range": { "start_line": 31613, "end_line": 31693 }, "context_lines": [ { "line": 31613, "text": "#### 6. 실패 경로와 복구/번역" }, { "line": 31614, "text": "" }, { "line": 31615, "text": "| 상황 | 처리 | 위치 |" }, { "line": 31616, "text": "|---|---|---|" }, { "line": 31617, "text": "| 트랜잭션 없이 append | `OUTBOX_TRANSACTION_REQUIRED` | `JdbcOutboxRepository:228-235` |" }, { "line": 31618, "text": "| 읽기 전용 트랜잭션 | 〃 | `:236-239` |" }, { "line": 31619, "text": "| 다른 DataSource 의 트랜잭션 | 〃 | `:240-246` |" }, { "line": 31620, "text": "| append SQL 실패 | `OUTBOX_APPEND_FAILED` | `:196-199` |" }, { "line": 31621, "text": "| 그 밖의 쿼리 실패 | `OUTBOX_QUERY_FAILED` | `:594-600` |" }, { "line": 31622, "text": "| 종결 쓰기가 0행 | `OutboxTransitionResult.STALE_LEASE` (예외 아님) | `:413-415` |" }, { "line": 31623, "text": "| 미지의 `PublishCompletion` | `IllegalStateException` | `OutboxRelay:216-217` |" }, { "line": 31624, "text": "| 리스가 발행 타임아웃보다 짧음 | `OUTBOX_LEASE_TOO_SHORT` | `OutboxProperties:52-58` |" }, { "line": 31625, "text": "| 행 헤더에 예약 이름 | `RESERVED_HEADER_IN_OUTBOX_ROW` | `OutboxEnvelopeFactory:70-77` |" }, { "line": 31626, "text": "| 승인 이미 실행됨 | `APPROVAL_ALREADY_EXECUTED` | `JdbcAdminOperationJournal:117-124` |" }, { "line": 31627, "text": "| 다른 런타임이 실행 중 | `ADMIN_OPERATION_IN_FLIGHT` | `:125-132`, `:181-186` |" }, { "line": 31628, "text": "| 리스 상실 후 쓰기 | `ADMIN_OPERATION_LEASE_LOST` | `:263-271` |" }, { "line": 31629, "text": "| 저널 도달 불가 | `ADMIN_JOURNAL_UNAVAILABLE` | `:206-208` 등 |" }, { "line": 31630, "text": "| 두 릴레이 동시 활성 | `DUPLICATE_OUTBOX_RELAY` | `DebeziumOutboxProfile:54-59` (**호출부 0**) |" }, { "line": 31631, "text": "| 릴레이 없음 | `NO_OUTBOX_RELAY` | `:60-65` (**호출부 0**) |" }, { "line": 31632, "text": "" }, { "line": 31633, "text": "`OutboxRelayWorker` 의 패스 실패 처리가 특히 명시적이다." }, { "line": 31634, "text": "" }, { "line": 31635, "text": "```java" }, { "line": 31636, "text": "// :184-190" }, { "line": 31637, "text": "} catch (RuntimeException passFailed) {" }, { "line": 31638, "text": " // A failed pass must not stop the loop: the scheduled task's own exception would cancel every" }, { "line": 31639, "text": " // future pass, turning one broker error into a relay that never runs again. The failure is" }, { "line": 31640, "text": " // counted and the next pass backs off as if nothing was published, which is true." }, { "line": 31641, "text": "```" }, { "line": 31642, "text": "" }, { "line": 31643, "text": "종료도 인터럽트가 아니라 드레인이다." }, { "line": 31644, "text": "" }, { "line": 31645, "text": "```java" }, { "line": 31646, "text": "// :99-106" }, { "line": 31647, "text": "/**" }, { "line": 31648, "text": " *
Draining rather than interrupting is the whole point. A pass killed between its claim and" }, { "line": 31649, "text": " * its terminal write leaves rows {@code IN_FLIGHT} holding a lease, and nothing may touch them" }, { "line": 31650, "text": " * until that lease expires — so an orderly shutdown would produce exactly the stall that a crash" }, { "line": 31651, "text": " * produces." }, { "line": 31652, "text": " */" }, { "line": 31653, "text": "```" }, { "line": 31654, "text": "" }, { "line": 31655, "text": "---" }, { "line": 31656, "text": "" }, { "line": 31657, "text": "#### 7. 트랜잭션·동시성·수명주기" }, { "line": 31658, "text": "" }, { "line": 31659, "text": "**두 가지 커넥션 획득 방식이 공존한다.**" }, { "line": 31660, "text": "" }, { "line": 31661, "text": "| 메서드 | 획득 | 효과 |" }, { "line": 31662, "text": "|---|---|---|" }, { "line": 31663, "text": "| `JdbcOutboxRepository.append(record)` | `DataSourceUtils.getConnection` | 호출자 트랜잭션에 합류 |" }, { "line": 31664, "text": "| 그 외 전부 (`withConnection`) | `dataSource.getConnection()` + try-with-resources | 풀에서 새 커넥션, 독립 커밋 |" }, { "line": 31665, "text": "| `JdbcAdminOperationJournal` 전 메서드 | `DataSourceUtils.getConnection` | 트랜잭션 있으면 합류 |" }, { "line": 31666, "text": "" }, { "line": 31667, "text": "릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 되므로 `withConnection` 의 선택은 타당하다. 다만 그 판단이 주석으로 남아 있지 않고, 같은 리프의 저널은 반대 방식을 쓴다. §17 P3." }, { "line": 31668, "text": "" }, { "line": 31669, "text": "**동시성 제어는 전부 데이터베이스에 있다.** `FOR UPDATE SKIP LOCKED`(청구), 서버측 토큰 증가, 펜싱 술어, `ON CONFLICT DO NOTHING`, 복합 기본키. Java 쪽에 락이 없다." }, { "line": 31670, "text": "" }, { "line": 31671, "text": "**수명주기**: `OutboxRelayWorker` 는 데몬 스레드 1개, `setExecuteExistingDelayedTasksAfterShutdownPolicy(false)`, `start()` 멱등, `stop(deadline)` 드레인 후 실패 시 `shutdownNow()`. 셋 다 근거 주석이 있다(`:79-88`, `:92`, `:121-122`)." }, { "line": 31672, "text": "" }, { "line": 31673, "text": "---" }, { "line": 31674, "text": "" }, { "line": 31675, "text": "#### 8. 설정·기능 플래그·환경 차이" }, { "line": 31676, "text": "" }, { "line": 31677, "text": "| 값 | 출처 | 기본 | 비고 |" }, { "line": 31678, "text": "|---|---|---|---|" }, { "line": 31679, "text": "| `batchSize` | `OutboxProperties` | 100 | |" }, { "line": 31680, "text": "| `leaseDuration` | 〃 | 30초 | `>= publishTimeout × 2` 강제 |" }, { "line": 31681, "text": "| `publishTimeout` | 〃 | 5초 | |" }, { "line": 31682, "text": "| `pollInterval` | 〃 | 500ms | 백오프의 기준 간격 |" }, { "line": 31683, "text": "| `retentionAfterPublish` | 〃 | 3일 | |" }, { "line": 31684, "text": "| `maxAttempts` | 〃 | 10 | 청구 술어의 `attempts < ?` |" }, { "line": 31685, "text": "| `maxInterval` | starter `:63` | **1분** | `OutboxRetryScheduler.standard()` 는 5분 |" }, { "line": 31686, "text": "| `maxBatches` | starter `:141` | **20 하드코딩** | 실질 무의미 (§12.1(a)) |" }, { "line": 31687, "text": "| relay owner | `OutboxRelay.defaultOwner()` | `pid@uuid8` | 프로세스당 안정 |" }, { "line": 31688, "text": "| CDC 모드 | `DebeziumOutboxProfile` | — | **어떤 프로퍼티에도 연결 안 됨** |" }, { "line": 31689, "text": "" }, { "line": 31690, "text": "`maxInterval` 이 두 값(1분 / 5분)으로 갈리는 것은 결함이 아니다 — starter 가 명시적으로 넘기고, `standard()` 는 호출자가 정책을 주지 않은 경우의 기본값이다." }, { "line": 31691, "text": "" }, { "line": 31692, "text": "---" }, { "line": 31693, "text": "" } ], "numbered_context": "31613 | #### 6. 실패 경로와 복구/번역\n31614 | \n31615 | | 상황 | 처리 | 위치 |\n31616 | |---|---|---|\n31617 | | 트랜잭션 없이 append | `OUTBOX_TRANSACTION_REQUIRED` | `JdbcOutboxRepository:228-235` |\n31618 | | 읽기 전용 트랜잭션 | 〃 | `:236-239` |\n31619 | | 다른 DataSource 의 트랜잭션 | 〃 | `:240-246` |\n31620 | | append SQL 실패 | `OUTBOX_APPEND_FAILED` | `:196-199` |\n31621 | | 그 밖의 쿼리 실패 | `OUTBOX_QUERY_FAILED` | `:594-600` |\n31622 | | 종결 쓰기가 0행 | `OutboxTransitionResult.STALE_LEASE` (예외 아님) | `:413-415` |\n31623 | | 미지의 `PublishCompletion` | `IllegalStateException` | `OutboxRelay:216-217` |\n31624 | | 리스가 발행 타임아웃보다 짧음 | `OUTBOX_LEASE_TOO_SHORT` | `OutboxProperties:52-58` |\n31625 | | 행 헤더에 예약 이름 | `RESERVED_HEADER_IN_OUTBOX_ROW` | `OutboxEnvelopeFactory:70-77` |\n31626 | | 승인 이미 실행됨 | `APPROVAL_ALREADY_EXECUTED` | `JdbcAdminOperationJournal:117-124` |\n31627 | | 다른 런타임이 실행 중 | `ADMIN_OPERATION_IN_FLIGHT` | `:125-132`, `:181-186` |\n31628 | | 리스 상실 후 쓰기 | `ADMIN_OPERATION_LEASE_LOST` | `:263-271` |\n31629 | | 저널 도달 불가 | `ADMIN_JOURNAL_UNAVAILABLE` | `:206-208` 등 |\n31630 | | 두 릴레이 동시 활성 | `DUPLICATE_OUTBOX_RELAY` | `DebeziumOutboxProfile:54-59` (**호출부 0**) |\n31631 | | 릴레이 없음 | `NO_OUTBOX_RELAY` | `:60-65` (**호출부 0**) |\n31632 | \n31633 | `OutboxRelayWorker` 의 패스 실패 처리가 특히 명시적이다.\n31634 | \n31635 | ```java\n31636 | // :184-190\n31637 | } catch (RuntimeException passFailed) {\n31638 | // A failed pass must not stop the loop: the scheduled task's own exception would cancel every\n31639 | // future pass, turning one broker error into a relay that never runs again. The failure is\n31640 | // counted and the next pass backs off as if nothing was published, which is true.\n31641 | ```\n31642 | \n31643 | 종료도 인터럽트가 아니라 드레인이다.\n31644 | \n31645 | ```java\n31646 | // :99-106\n31647 | /**\n31648 | *
Draining rather than interrupting is the whole point. A pass killed between its claim and\n31649 | * its terminal write leaves rows {@code IN_FLIGHT} holding a lease, and nothing may touch them\n31650 | * until that lease expires — so an orderly shutdown would produce exactly the stall that a crash\n31651 | * produces.\n31652 | */\n31653 | ```\n31654 | \n31655 | ---\n31656 | \n31657 | #### 7. 트랜잭션·동시성·수명주기\n31658 | \n31659 | **두 가지 커넥션 획득 방식이 공존한다.**\n31660 | \n31661 | | 메서드 | 획득 | 효과 |\n31662 | |---|---|---|\n31663 | | `JdbcOutboxRepository.append(record)` | `DataSourceUtils.getConnection` | 호출자 트랜잭션에 합류 |\n31664 | | 그 외 전부 (`withConnection`) | `dataSource.getConnection()` + try-with-resources | 풀에서 새 커넥션, 독립 커밋 |\n31665 | | `JdbcAdminOperationJournal` 전 메서드 | `DataSourceUtils.getConnection` | 트랜잭션 있으면 합류 |\n31666 | \n31667 | 릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 되므로 `withConnection` 의 선택은 타당하다. 다만 그 판단이 주석으로 남아 있지 않고, 같은 리프의 저널은 반대 방식을 쓴다. §17 P3.\n31668 | \n31669 | **동시성 제어는 전부 데이터베이스에 있다.** `FOR UPDATE SKIP LOCKED`(청구), 서버측 토큰 증가, 펜싱 술어, `ON CONFLICT DO NOTHING`, 복합 기본키. Java 쪽에 락이 없다.\n31670 | \n31671 | **수명주기**: `OutboxRelayWorker` 는 데몬 스레드 1개, `setExecuteExistingDelayedTasksAfterShutdownPolicy(false)`, `start()` 멱등, `stop(deadline)` 드레인 후 실패 시 `shutdownNow()`. 셋 다 근거 주석이 있다(`:79-88`, `:92`, `:121-122`).\n31672 | \n31673 | ---\n31674 | \n31675 | #### 8. 설정·기능 플래그·환경 차이\n31676 | \n31677 | | 값 | 출처 | 기본 | 비고 |\n31678 | |---|---|---|---|\n31679 | | `batchSize` | `OutboxProperties` | 100 | |\n31680 | | `leaseDuration` | 〃 | 30초 | `>= publishTimeout × 2` 강제 |\n31681 | | `publishTimeout` | 〃 | 5초 | |\n31682 | | `pollInterval` | 〃 | 500ms | 백오프의 기준 간격 |\n31683 | | `retentionAfterPublish` | 〃 | 3일 | |\n31684 | | `maxAttempts` | 〃 | 10 | 청구 술어의 `attempts < ?` |\n31685 | | `maxInterval` | starter `:63` | **1분** | `OutboxRetryScheduler.standard()` 는 5분 |\n31686 | | `maxBatches` | starter `:141` | **20 하드코딩** | 실질 무의미 (§12.1(a)) |\n31687 | | relay owner | `OutboxRelay.defaultOwner()` | `pid@uuid8` | 프로세스당 안정 |\n31688 | | CDC 모드 | `DebeziumOutboxProfile` | — | **어떤 프로퍼티에도 연결 안 됨** |\n31689 | \n31690 | `maxInterval` 이 두 값(1분 / 5분)으로 갈리는 것은 결함이 아니다 — starter 가 명시적으로 넘기고, `standard()` 는 호출자가 정책을 주지 않은 경우의 기본값이다.\n31691 | \n31692 | ---\n31693 | ",
"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