{ "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": 33986, "line": 33986 }, "current_section": { "heading": { "line": 33986, "level": 5, "text": "4.3 `OutboxStatus` — 여섯 상태와 두 개의 구분" }, "start_line": 33986, "end_line": 34015, "text": "##### 4.3 `OutboxStatus` — 여섯 상태와 두 개의 구분\n\n`PENDING` → `IN_FLIGHT` → `PUBLISHED` / `AMBIGUOUS` / `FAILED` / `EXHAUSTED`.\n\n**두 쌍의 구분이 각각 이유를 갖는다.**\n\n`AMBIGUOUS` vs `FAILED`:\n\n```java\n// :6-9\n *
{@link #AMBIGUOUS} is a distinct state rather than a flavour of failure. A record whose\n * publish timed out may already be on the broker; retrying it is correct, but only under the same\n * logical message id, and an operator looking at the table needs to be able to tell those rows\n * apart from ones that definitely never landed.\n```\n\n`EXHAUSTED` vs `FAILED`:\n\n```java\n// :31-34\n *
Distinct from {@link #FAILED}, which means the broker refused the message: this one means\n * nobody ever got an answer. Collapsing the two loses the difference between \"this message is\n * invalid\" and \"the broker was unreachable for an hour\", and those need different operator\n * actions — the first a fix, the second a redrive.\n```\n\n`OutboxRepository.markExhausted`의 javadoc이 같은 말을 반복한다 — \"The first needs a fix, the second a redrive.\"\n\n**`FAILED`의 의미가 애플리케이션 쪽 동명 enum과 반대다.** `CleanArchitectureTest.APPLICATION_DOES_NOT_DEPEND_ON_THE_MESSAGING_PLATFORM`의 `.because(...)`가 그것을 ArchUnit 규칙의 근거로 든다 — \"its `OutboxStatus.FAILED` means the opposite of the legacy `OutboxEventStatus.FAILED`, so the two models cannot be mixed by name without inverting retryable and terminal.\" 즉 **이 enum의 의미가 저장소 규칙 하나의 존재 이유다.**\n" }, "previous_section": { "heading": { "line": 33966, "level": 5, "text": "4.2 `OutboxTransitionResult` — void가 삼킨 것" }, "start_line": 33966, "end_line": 33985, "text": "##### 4.2 `OutboxTransitionResult` — void가 삼킨 것\n\n```java\n// :5-9\n *
The transitions returned {@code void}, so an update that matched zero rows was\n * indistinguishable from one that matched one. That is precisely the stale-lease case: the relay\n * believes it recorded the outcome, the row still says something else, and nothing anywhere counts\n * the disagreement.\n```\n\n두 값이고 `STALE_LEASE`의 javadoc이 운영 의미까지 적는다.\n\n```java\n *
Another relay claimed it after the lease expired. Not an error to throw — the message is\n * being handled by somebody else — but never a success either: it is the signal that this\n * worker's publish attempt may have produced a duplicate, and it belongs on a metric.\n```\n\n**\"belongs on a metric\"** — 그 메트릭이 존재하는지는 outbox leaf가 답한다.\n" }, "next_section": { "heading": { "line": 34016, "level": 5, "text": "4.4 `InboxResult` — 두 개가 아니라 세 개" }, "start_line": 34016, "end_line": 34037, "text": "##### 4.4 `InboxResult` — 두 개가 아니라 세 개\n\n```java\n// :6-9\n *
Three outcomes, not two. Collapsing {@link #ALREADY_APPLIED} and {@link #CLAIMED_ELSEWHERE}\n * into a single \"duplicate\" would settle a message whose effect is still only half-written by\n * another instance: if that instance then rolls back, the effect is lost and the broker will never\n * redeliver, because this instance already acknowledged it.\n```\n\n`safeToSettle` 플래그가 상수에 붙어 있다.\n\n| 값 | safeToSettle | 뜻 |\n|---|:---:|---|\n| `APPLIED` | true | 이 트랜잭션에서 효과 실행 |\n| `ALREADY_APPLIED` | true | 커밋된 예약 존재 — 이미 실행됨 |\n| `CLAIMED_ELSEWHERE` | **false** | 다른 인스턴스가 **미커밋** 예약 보유 |\n\n세 번째의 javadoc이 결론을 적는다 — \"Do *not* settle. The other transaction may still roll back, and this delivery is the only remaining copy that could re-apply the effect.\"\n\n**세 값 모두 필요한 이유가 명확하고, `isSafeToSettle()`이 그 판단을 하나로 모은다.**\n" }, "context_range": { "start_line": 33966, "end_line": 34037 }, "context_lines": [ { "line": 33966, "text": "##### 4.2 `OutboxTransitionResult` — void가 삼킨 것" }, { "line": 33967, "text": "" }, { "line": 33968, "text": "```java" }, { "line": 33969, "text": "// :5-9" }, { "line": 33970, "text": " *
The transitions returned {@code void}, so an update that matched zero rows was" }, { "line": 33971, "text": " * indistinguishable from one that matched one. That is precisely the stale-lease case: the relay" }, { "line": 33972, "text": " * believes it recorded the outcome, the row still says something else, and nothing anywhere counts" }, { "line": 33973, "text": " * the disagreement." }, { "line": 33974, "text": "```" }, { "line": 33975, "text": "" }, { "line": 33976, "text": "두 값이고 `STALE_LEASE`의 javadoc이 운영 의미까지 적는다." }, { "line": 33977, "text": "" }, { "line": 33978, "text": "```java" }, { "line": 33979, "text": " *
Another relay claimed it after the lease expired. Not an error to throw — the message is" }, { "line": 33980, "text": " * being handled by somebody else — but never a success either: it is the signal that this" }, { "line": 33981, "text": " * worker's publish attempt may have produced a duplicate, and it belongs on a metric." }, { "line": 33982, "text": "```" }, { "line": 33983, "text": "" }, { "line": 33984, "text": "**\"belongs on a metric\"** — 그 메트릭이 존재하는지는 outbox leaf가 답한다." }, { "line": 33985, "text": "" }, { "line": 33986, "text": "##### 4.3 `OutboxStatus` — 여섯 상태와 두 개의 구분" }, { "line": 33987, "text": "" }, { "line": 33988, "text": "`PENDING` → `IN_FLIGHT` → `PUBLISHED` / `AMBIGUOUS` / `FAILED` / `EXHAUSTED`." }, { "line": 33989, "text": "" }, { "line": 33990, "text": "**두 쌍의 구분이 각각 이유를 갖는다.**" }, { "line": 33991, "text": "" }, { "line": 33992, "text": "`AMBIGUOUS` vs `FAILED`:" }, { "line": 33993, "text": "" }, { "line": 33994, "text": "```java" }, { "line": 33995, "text": "// :6-9" }, { "line": 33996, "text": " *
{@link #AMBIGUOUS} is a distinct state rather than a flavour of failure. A record whose" }, { "line": 33997, "text": " * publish timed out may already be on the broker; retrying it is correct, but only under the same" }, { "line": 33998, "text": " * logical message id, and an operator looking at the table needs to be able to tell those rows" }, { "line": 33999, "text": " * apart from ones that definitely never landed." }, { "line": 34000, "text": "```" }, { "line": 34001, "text": "" }, { "line": 34002, "text": "`EXHAUSTED` vs `FAILED`:" }, { "line": 34003, "text": "" }, { "line": 34004, "text": "```java" }, { "line": 34005, "text": "// :31-34" }, { "line": 34006, "text": " *
Distinct from {@link #FAILED}, which means the broker refused the message: this one means" }, { "line": 34007, "text": " * nobody ever got an answer. Collapsing the two loses the difference between \"this message is" }, { "line": 34008, "text": " * invalid\" and \"the broker was unreachable for an hour\", and those need different operator" }, { "line": 34009, "text": " * actions — the first a fix, the second a redrive." }, { "line": 34010, "text": "```" }, { "line": 34011, "text": "" }, { "line": 34012, "text": "`OutboxRepository.markExhausted`의 javadoc이 같은 말을 반복한다 — \"The first needs a fix, the second a redrive.\"" }, { "line": 34013, "text": "" }, { "line": 34014, "text": "**`FAILED`의 의미가 애플리케이션 쪽 동명 enum과 반대다.** `CleanArchitectureTest.APPLICATION_DOES_NOT_DEPEND_ON_THE_MESSAGING_PLATFORM`의 `.because(...)`가 그것을 ArchUnit 규칙의 근거로 든다 — \"its `OutboxStatus.FAILED` means the opposite of the legacy `OutboxEventStatus.FAILED`, so the two models cannot be mixed by name without inverting retryable and terminal.\" 즉 **이 enum의 의미가 저장소 규칙 하나의 존재 이유다.**" }, { "line": 34015, "text": "" }, { "line": 34016, "text": "##### 4.4 `InboxResult` — 두 개가 아니라 세 개" }, { "line": 34017, "text": "" }, { "line": 34018, "text": "```java" }, { "line": 34019, "text": "// :6-9" }, { "line": 34020, "text": " *
Three outcomes, not two. Collapsing {@link #ALREADY_APPLIED} and {@link #CLAIMED_ELSEWHERE}" }, { "line": 34021, "text": " * into a single \"duplicate\" would settle a message whose effect is still only half-written by" }, { "line": 34022, "text": " * another instance: if that instance then rolls back, the effect is lost and the broker will never" }, { "line": 34023, "text": " * redeliver, because this instance already acknowledged it." }, { "line": 34024, "text": "```" }, { "line": 34025, "text": "" }, { "line": 34026, "text": "`safeToSettle` 플래그가 상수에 붙어 있다." }, { "line": 34027, "text": "" }, { "line": 34028, "text": "| 값 | safeToSettle | 뜻 |" }, { "line": 34029, "text": "|---|:---:|---|" }, { "line": 34030, "text": "| `APPLIED` | true | 이 트랜잭션에서 효과 실행 |" }, { "line": 34031, "text": "| `ALREADY_APPLIED` | true | 커밋된 예약 존재 — 이미 실행됨 |" }, { "line": 34032, "text": "| `CLAIMED_ELSEWHERE` | **false** | 다른 인스턴스가 **미커밋** 예약 보유 |" }, { "line": 34033, "text": "" }, { "line": 34034, "text": "세 번째의 javadoc이 결론을 적는다 — \"Do *not* settle. The other transaction may still roll back, and this delivery is the only remaining copy that could re-apply the effect.\"" }, { "line": 34035, "text": "" }, { "line": 34036, "text": "**세 값 모두 필요한 이유가 명확하고, `isSafeToSettle()`이 그 판단을 하나로 모은다.**" }, { "line": 34037, "text": "" } ], "numbered_context": "33966 | ##### 4.2 `OutboxTransitionResult` — void가 삼킨 것\n33967 | \n33968 | ```java\n33969 | // :5-9\n33970 | *
The transitions returned {@code void}, so an update that matched zero rows was\n33971 | * indistinguishable from one that matched one. That is precisely the stale-lease case: the relay\n33972 | * believes it recorded the outcome, the row still says something else, and nothing anywhere counts\n33973 | * the disagreement.\n33974 | ```\n33975 | \n33976 | 두 값이고 `STALE_LEASE`의 javadoc이 운영 의미까지 적는다.\n33977 | \n33978 | ```java\n33979 | *
Another relay claimed it after the lease expired. Not an error to throw — the message is\n33980 | * being handled by somebody else — but never a success either: it is the signal that this\n33981 | * worker's publish attempt may have produced a duplicate, and it belongs on a metric.\n33982 | ```\n33983 | \n33984 | **\"belongs on a metric\"** — 그 메트릭이 존재하는지는 outbox leaf가 답한다.\n33985 | \n33986 | ##### 4.3 `OutboxStatus` — 여섯 상태와 두 개의 구분\n33987 | \n33988 | `PENDING` → `IN_FLIGHT` → `PUBLISHED` / `AMBIGUOUS` / `FAILED` / `EXHAUSTED`.\n33989 | \n33990 | **두 쌍의 구분이 각각 이유를 갖는다.**\n33991 | \n33992 | `AMBIGUOUS` vs `FAILED`:\n33993 | \n33994 | ```java\n33995 | // :6-9\n33996 | *
{@link #AMBIGUOUS} is a distinct state rather than a flavour of failure. A record whose\n33997 | * publish timed out may already be on the broker; retrying it is correct, but only under the same\n33998 | * logical message id, and an operator looking at the table needs to be able to tell those rows\n33999 | * apart from ones that definitely never landed.\n34000 | ```\n34001 | \n34002 | `EXHAUSTED` vs `FAILED`:\n34003 | \n34004 | ```java\n34005 | // :31-34\n34006 | *
Distinct from {@link #FAILED}, which means the broker refused the message: this one means\n34007 | * nobody ever got an answer. Collapsing the two loses the difference between \"this message is\n34008 | * invalid\" and \"the broker was unreachable for an hour\", and those need different operator\n34009 | * actions — the first a fix, the second a redrive.\n34010 | ```\n34011 | \n34012 | `OutboxRepository.markExhausted`의 javadoc이 같은 말을 반복한다 — \"The first needs a fix, the second a redrive.\"\n34013 | \n34014 | **`FAILED`의 의미가 애플리케이션 쪽 동명 enum과 반대다.** `CleanArchitectureTest.APPLICATION_DOES_NOT_DEPEND_ON_THE_MESSAGING_PLATFORM`의 `.because(...)`가 그것을 ArchUnit 규칙의 근거로 든다 — \"its `OutboxStatus.FAILED` means the opposite of the legacy `OutboxEventStatus.FAILED`, so the two models cannot be mixed by name without inverting retryable and terminal.\" 즉 **이 enum의 의미가 저장소 규칙 하나의 존재 이유다.**\n34015 | \n34016 | ##### 4.4 `InboxResult` — 두 개가 아니라 세 개\n34017 | \n34018 | ```java\n34019 | // :6-9\n34020 | *
Three outcomes, not two. Collapsing {@link #ALREADY_APPLIED} and {@link #CLAIMED_ELSEWHERE}\n34021 | * into a single \"duplicate\" would settle a message whose effect is still only half-written by\n34022 | * another instance: if that instance then rolls back, the effect is lost and the broker will never\n34023 | * redeliver, because this instance already acknowledged it.\n34024 | ```\n34025 | \n34026 | `safeToSettle` 플래그가 상수에 붙어 있다.\n34027 | \n34028 | | 값 | safeToSettle | 뜻 |\n34029 | |---|:---:|---|\n34030 | | `APPLIED` | true | 이 트랜잭션에서 효과 실행 |\n34031 | | `ALREADY_APPLIED` | true | 커밋된 예약 존재 — 이미 실행됨 |\n34032 | | `CLAIMED_ELSEWHERE` | **false** | 다른 인스턴스가 **미커밋** 예약 보유 |\n34033 | \n34034 | 세 번째의 javadoc이 결론을 적는다 — \"Do *not* settle. The other transaction may still roll back, and this delivery is the only remaining copy that could re-apply the effect.\"\n34035 | \n34036 | **세 값 모두 필요한 이유가 명확하고, `isSafeToSettle()`이 그 판단을 하나로 모은다.**\n34037 | ",
"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