docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-inbound-websocket-c02
|
||||
title: 요청 경로에 있는 것은 stomp 패키지가 참조하는 것뿐이다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-inbound-websocket-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-inbound-websocket-c02
|
||||
file: ../../../final/evidence/rendered/adapter-inbound-websocket-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-inbound-websocket-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/17-adapter-inbound-websocket.md#L251 이다.
|
||||
module: adapter-inbound-websocket
|
||||
---
|
||||
|
||||
# 요청 경로에 있는 것은 stomp 패키지가 참조하는 것뿐이다
|
||||
|
||||
`stomp/WebSocketInboundAuthorizationInterceptor`(53)와 `stomp/AuthenticatedHandshakeInterceptor`(34)만 `WebSocketConfig`에 등록된다. 플랫폼 정책 타입은 `stomp`가 참조하지 않는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 sub-scope에서 실제로 요청 경로에 있는 것은 **`stomp` 패키지가 참조하는 것뿐**이다. `stomp/WebSocketInboundAuthorizationInterceptor`(53)와 `stomp/AuthenticatedHandshakeInterceptor`(34)가 `WebSocketConfig`에 등록되고, 그 둘은 `stomp/WebSocketProperties`를 쓴다.
|
||||
|
||||
## WebSocketConfig 참조 위치
|
||||
|
||||
:::evidence key="adapter-inbound-websocket-c02" alt="코드베이스에서 WebSocketConfig 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="WebSocketConfig 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 요청 경로 밖에 남는 정책 타입
|
||||
|
||||
`security`(4) · `authz`(1) · `idempotency`(4) · `budget`(1)의 플랫폼 정책 타입은 `stomp`가 참조하지 않는다. 즉 **인증 프로파일 · 티켓 · origin 정책 · 메시지 권한 · 연결 예산 · 명령 멱등성이 모두 요청 경로 밖이다.**
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-cache-redis-c13
|
||||
title: 등록되지 않은 스크립트는 서버에 닿을 방법이 없다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-cache-redis-c13
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-cache-redis-c13
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-cache-redis-c13.svg
|
||||
- key: adapter-outbound-cache-redis-c13-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-cache-redis-c13.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-cache-redis-c13.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L701 이다.
|
||||
module: adapter-outbound-cache-redis
|
||||
---
|
||||
|
||||
# 등록되지 않은 스크립트는 서버에 닿을 방법이 없다
|
||||
|
||||
`RedisScriptRegistry`가 등록을 배포 단계로 두고, 트랜잭션 창은 감시 키에서 정한 노드에 고정된다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`RedisScriptRegistry`의 규칙 — "Registration is a deployment step, not a request-time one. A script that was never registered has no digest and therefore **no way to reach the server**, which is what makes 'only reviewed scripts run' a structural property rather than a convention." 같은 identity에 다른 body를 등록하면 거부하고, 실행 시점에도 body가 등록본과 같은지 다시 본다.
|
||||
|
||||
## 등록과 실행의 분리
|
||||
|
||||
:::evidence key="adapter-outbound-cache-redis-c13-diagram" alt="배포 시 스크립트 등록과 digest 대조와 EVALSHA 실행이 왼쪽에서 오른쪽으로 이어지는 구조" caption="등록과 실행의 분리" zoom="false"
|
||||
:::
|
||||
|
||||
README가 주장하는 복구 사슬 `EVALSHA → NOSCRIPT → SCRIPT LOAD → digest verify → EVALSHA`는 **실재한다** — `LettuceRedisScriptOperations:26` javadoc이 "NOSCRIPT is the one failure retried automatically"라고 적고, `:129-133`이 `RedisNoScriptException` 또는 메시지 접두 `NOSCRIPT`를 잡아 `:100`에서 `registry.forget(script.id())`를 호출한다. 다음 호출이 `digest(...)`에서 다시 `SCRIPT LOAD`한다.
|
||||
|
||||
## RedisScriptRegistry 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-cache-redis-c13" alt="코드베이스에서 RedisScriptRegistry 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedisScriptRegistry 코드베이스 검색 — 7줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## Cluster에서 MULTI 창을 노드에 고정하는 방법
|
||||
|
||||
`RedisTransactionRunner`는 Cluster에서의 `MULTI` 문제를 정면으로 다룬다. javadoc이 문제와 해법을 적는다 — 다른 레인은 명령마다 슬롯 소유 노드로 라우팅하는데 "that is exactly what a `MULTI` window must not do: the queued commands would be spread across nodes and none of them would be part of the same window." 해법은 연결이 아니라 **라우팅 결정**이었다 — 감시 키(또는 명시적 슬롯 태그)에서 노드를 정해 레인을 고정한다.
|
||||
|
||||
## 감시 키가 없는 Cluster 트랜잭션을 거부하는 이유
|
||||
|
||||
"the keys the callback will queue are not known until the callback runs, which is after the window is open."
|
||||
|
||||
<!-- body:end -->
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c02
|
||||
title: 능력 어휘와 지원 등급과 실제 보고를 나눈다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c02
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c02.svg
|
||||
- key: adapter-outbound-persistence-jpa-c02-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L126 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 능력 어휘와 지원 등급과 실제 보고를 나눈다
|
||||
|
||||
`JpaCapability` enum은 16개 capability id를 갖고 app-bootstrap의 `capabilities()`도 16개를 선언한다. 능력 이름은 어휘일 뿐이고 `CapabilitySupport`가 등급을 결합하며 composition이 실제 보고를 구성한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
현재 enum은 16개 capability id를 갖는다. app-bootstrap의 `JpaPlatformAutoConfiguration.capabilities()` 역시 16개를 선언하므로 **enum catalog와 current composition count는 일치**한다.
|
||||
|
||||
Stable composition은 대표적으로 transaction retry, completion evidence, keyset pagination, batch, schema gate, runtime-role verification, observability를 기본 지원으로 보고한다. Advanced capability는 PostgreSQL native write/work claim/JSONB/array-range, bulk DML, stateless session, COPY, L2 cache, Envers 등을 constraints와 함께 보고한다.
|
||||
|
||||
## 보고 가능한 계약이 만들어지는 층
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c02-diagram" alt="능력 어휘와 지원 등급 결합과 실제 보고 구성이 위에서 아래로 쌓이고 보고 구성 화살표가 아래로 그려진 구조" caption="보고 가능한 계약이 만들어지는 층" zoom="false"
|
||||
:::
|
||||
|
||||
이 분리는 "classpath에 코드가 있다"와 "현재 composition이 기본 지원한다고 약속한다"를 동일시하지 않는다. capability enum은 vocabulary이고, `CapabilitySupport`가 support level을 결합하며, app-bootstrap composition이 실제 현재 report를 구성한다.
|
||||
|
||||
## CapabilitySupport 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c02" alt="코드베이스에서 CapabilitySupport 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CapabilitySupport 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 생성자가 보장하는 것
|
||||
|
||||
capability non-null, level non-null, constraints list defensive copy, 각 constraint non-null / non-blank. `usableByDefault()`는 STABLE만 true다 — Advanced/Experimental이 "존재하므로 기본 사용 가능"으로 오해되지 않게 support level을 코드에 남긴다.
|
||||
|
||||
## 보고서가 담지 않는 것
|
||||
|
||||
`JpaPlatformReport`는 JDBC URL/user/password/SQL/entity catalog를 필드로 갖지 않도록 설계되어 있고, privilege detail도 boolean으로 축약한다. management endpoint의 reconnaissance surface를 좁히는 방향이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c10
|
||||
title: 두 트랜잭션 경계가 공존하고 문서만 어긋난다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c10
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c10
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c10.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c10.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L763 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 두 트랜잭션 경계가 공존하고 문서만 어긋난다
|
||||
|
||||
`FullTransactionRetryCoordinator`는 현재 runtime bean graph에 포함되는 구현이다. 그러나 그것을 실제 business/application code가 호출하는 경로는 확인되지 않았고, application-core transaction port 쪽이 canonical contract로 쓰인다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
공개 어휘는 `PersistenceOperationName`, `TransactionProfile`, `RetryProfile`, `JpaPersistenceException`, `TransactionCompletionEvidence`다. `JpaPlatformRuntimeAutoConfiguration`은 `PlatformTransactionManager`가 있으면 `SpringJpaTransactionExecutor` bean을 만들고, 그 executor가 있으면 `FullTransactionRetryCoordinator` bean도 만든다. 따라서 source tree 수준에서는 단순 historical class가 아니라 **현재 runtime bean graph에도 포함되는 구현**이다.
|
||||
|
||||
## 그런데 호출하는 곳이 없다
|
||||
|
||||
repository production call search에서는 `FullTransactionRetryCoordinator.execute(...)`를 실제 business/application code가 호출하는 경로가 확인되지 않았다. 반대로 application-core transaction port는 sample/use-case/composition에서 canonical contract로 사용된다.
|
||||
|
||||
## 분석 원문의 경계 서술
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c10" alt="분석 문서 analysis/05-adapter-outbound-persistence-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/05-adapter-outbound-persistence-jpa.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 공존이 결함이 아닌 이유와 실제 문제
|
||||
|
||||
이 공존 자체는 곧바로 defect가 아니다. `api/**`는 intended external surface이므로 fork/application이 이 경로를 programmatically 사용할 수 있다. 문제는 문서가 두 boundary의 관계를 일관되게 설명하지 못하고, 일부 composition helper는 실제 type relationship과 다른 설명을 한다는 점이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c11
|
||||
title: Spring stereotype이 있다고 runtime에 도달하지 않는다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c11
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c11
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c11.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c11.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L788 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# Spring stereotype이 있다고 runtime에 도달하지 않는다
|
||||
|
||||
root `CaSkeletonApplication`이 persistence package를 broad scan에서 의도적으로 제외하므로, adapter leaf 내부의 `@Component`가 자동으로 등록되지 않는다. `SpringTransactionPort`는 `JpaAdapterComponentsConfig`의 narrow component scan으로 등록된다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`SpringTransactionPort`는 `PolicyTransactionPort`를 구현하며 `JpaAdapterComponentsConfig`의 narrow component scan으로 등록된다. 이 wiring은 중요하다 — root `CaSkeletonApplication`은 persistence package를 broad scan에서 의도적으로 제외한다. 그래서 adapter leaf 내부의 `@Component`를 "annotation이 있으니 알아서 등록될 것"이라고 볼 수 없다.
|
||||
|
||||
## SpringTransactionPort 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c11" alt="코드베이스에서 SpringTransactionPort 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringTransactionPort 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 배치를 만든 과거 회귀
|
||||
|
||||
`JpaAdapterComponentsConfig` source에 기록돼 있다. persistence package를 broad scan에서 제외했고, `SpringTransactionPort` 같은 component를 별도 scan하지 않았다. 처음 transaction port가 필요한 capability가 조립될 때 unsatisfied dependency로 드러났고, 해결은 JPA master switch 아래에서만 persistence adapter package를 narrow scan하는 것이었다. current root는 `PersistenceJpaRootAutoConfiguration -> JpaAdapterComponentsConfig -> component scan` 체인을 통해 이를 해결한다.
|
||||
|
||||
## inRootWrite가 inWrite와 다른 지점
|
||||
|
||||
`inWrite`와 `inRootWrite` 둘 다 REQUIRED · READ_COMMITTED · read-only false로 매핑된다. 특히 vendor default isolation에 맡기지 않고 READ_COMMITTED를 명시한다. 그런데 `inRootWrite`는 시작 전에 `TransactionSynchronizationManager.isActualTransactionActive()`를 확인해 ambient physical transaction이 있으면 manager/action 호출 전에 거부한다. "root boundary"를 REQUIRED의 join semantics로 조용히 바꾸지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c14
|
||||
title: SQLSTATE가 retryable이라고 해서 use case를 다시 돌리지 않는다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c14
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c14
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c14.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c14.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L938 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# SQLSTATE가 retryable이라고 해서 use case를 다시 돌리지 않는다
|
||||
|
||||
`TransactionRetryClassifier`의 automatic replay candidate는 `40001`과 `40P01` 둘뿐이고, 그것만으로는 재시도하지 않는다. 업무 side-effect가 replay-safe하다고 application policy가 선언해야 한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`TransactionRetryClassifier`는 cause chain에서 SQLSTATE를 찾지만 automatic replay candidate는 `40001`과 `40P01` 둘뿐이다. `08007` 같은 transaction-resolution-unknown은 candidate가 아니다.
|
||||
|
||||
## TransactionRetryClassifier 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c14" alt="코드베이스에서 TransactionRetryClassifier 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TransactionRetryClassifier 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 분류만으로는 재시도하지 않는다
|
||||
|
||||
`SpringPolicyTransactionPort`는 ordinary command에서 40001이 나더라도 `COMMAND_SERIALIZABLE_REPLAY_SAFE`가 아니면 retry하지 않는다. failure 종류뿐 아니라 **업무 side-effect가 replay-safe하다고 application policy가 선언했는가**가 함께 필요하다. 이것은 "DB가 retryable이라고 말하니 use case를 다시 실행"하는 구조와 다르다.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c15
|
||||
title: 한 번의 물리 시도만 담당하고 재시도는 하지 않는다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c15
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c15
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c15.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c15.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L953 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 한 번의 물리 시도만 담당하고 재시도는 하지 않는다
|
||||
|
||||
`SpringJpaTransactionExecutor`는 attempt boundary를 소유하고 raw provider exception을 `JpaPersistenceException`으로 바꾸는 위치다. 자체 retry는 하지 않는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 executor는 한 번의 physical attempt만 담당한다. 자체 retry는 하지 않는다.
|
||||
|
||||
```text
|
||||
TransactionEvidenceContext.begin(...)
|
||||
-> TransactionTemplate.execute(work)
|
||||
-> success return
|
||||
or
|
||||
-> attempt boundary에서 failure translation
|
||||
-> translated runtime exception rethrow
|
||||
finally
|
||||
-> TransactionEvidenceScope close
|
||||
```
|
||||
|
||||
## SpringJpaTransactionExecutor 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c15" alt="코드베이스에서 SpringJpaTransactionExecutor 를 검색한 출력 26줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="SpringJpaTransactionExecutor 코드베이스 검색 — 26줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 자리가 번역 지점인 이유
|
||||
|
||||
attempt boundary에서 operation, attempt number, elapsed time, reconciliation key를 알고 있으므로 raw provider exception을 `JpaPersistenceException`으로 변환하는 위치로 사용된다. vendor translator가 조립되면 PostgreSQL 40001/40P01 같은 structured SQLSTATE가 stable exception으로 바뀌어 coordinator가 처리할 수 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c16
|
||||
title: 완료 불명과 되돌릴 수 없는 부작용에서는 재시도하지 않는다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c16
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c16
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c16.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c16.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L974 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 완료 불명과 되돌릴 수 없는 부작용에서는 재시도하지 않는다
|
||||
|
||||
coordinator는 `JpaPersistenceException`만 catch하고 retry decision에 따라 새 transaction과 새 persistence context에서 전체 work를 다시 호출한다. 다섯 가지 guard가 그 재호출을 막는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
coordinator는 `JpaPersistenceException`만 catch하고, retry decision에 따라 **새 transaction / 새 persistence context에서 전체 work를 다시 호출**한다. 설계상 중요한 guard가 다섯이다.
|
||||
|
||||
- completion unknown → no retry
|
||||
- irreversible side effect context → no retry
|
||||
- retry budget elapsed → stop
|
||||
- max attempts → stop
|
||||
- backoff interrupt → stop
|
||||
|
||||
retry listener는 observation only다.
|
||||
|
||||
## FullTransactionRetryCoordinator 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c16" alt="코드베이스에서 FullTransactionRetryCoordinator 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="FullTransactionRetryCoordinator 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 모델 자체의 테스트는 강하고 조립에는 결함이 있다
|
||||
|
||||
이 모델 자체의 unit tests는 강하다 — serialization/deadlock retry, exhaustion, completion unknown no-retry, interrupted sleep, irreversible side effect 등을 검증한다. 하지만 current implementation에는 public composition contract와 맞지 않는 별도 defect가 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c18
|
||||
title: 문서가 지목하는 조정 레코드를 쓰는 코드가 없다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c18
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c18
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c18.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c18.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1184 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 문서가 지목하는 조정 레코드를 쓰는 코드가 없다
|
||||
|
||||
`CompletionUnknownRecord`와 `CompletionUnknownRecorder`는 current production에서 자신들의 정의 외 consumer/implementation이 없다. 그런데 문서는 훨씬 강한 계약을 선언한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`CompletionUnknownRecord`와 `CompletionUnknownRecorder`는 current production에서 자신들의 정의 외 consumer/implementation이 없다.
|
||||
|
||||
## CompletionUnknownRecord 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c18" alt="코드베이스에서 CompletionUnknownRecord 를 검색한 출력 10줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="CompletionUnknownRecord 코드베이스 검색 — 10줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 문서는 훨씬 강한 계약을 선언한다
|
||||
|
||||
support matrix는 이렇게 적는다.
|
||||
|
||||
```text
|
||||
Commit completion evidence = Stable
|
||||
Automatic reconciliation unsupported.
|
||||
The platform records; the domain resolves.
|
||||
```
|
||||
|
||||
runbook은 신호로 `jpa.transaction.completion.unknown` 증가와 조정 채널의 `CompletionUnknownRecord`를 들고, 그 record의 `transactionKey`를 사용하라고 한다. 현재 이 record를 실제로 쓰는 production channel은 확인되지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c20
|
||||
title: 참조 수가 0이어도 스캔으로 도달한다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c20
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c20
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c20.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c20.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1334 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 참조 수가 0이어도 스캔으로 도달한다
|
||||
|
||||
`JpaTransactionConfig`는 direct production reference가 거의 없지만 `@Configuration` + `@EnableConfigurationProperties`이고 `JpaAdapterComponentsConfig`가 transaction package를 component scan한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`JpaTransactionConfig`도 direct production reference는 거의 없다. 하지만 이 class는 `@Configuration` + `@EnableConfigurationProperties(JpaTransactionSettings.class)`이고, `JpaAdapterComponentsConfig`가 transaction package를 component scan한다. 따라서 direct Java call/import가 0이어도 runtime reachability가 있다.
|
||||
|
||||
## JpaTransactionConfig 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c20" alt="코드베이스에서 JpaTransactionConfig 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaTransactionConfig 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 클래스가 기록한 이력
|
||||
|
||||
- root `@ConfigurationPropertiesScan`에서 optional persistence tree 제외
|
||||
- JPA on 상태에서도 settings가 아무도 bind하지 않던 문제 발생
|
||||
- transaction port construction 실패
|
||||
- package-local configuration으로 JPA master switch 안에서만 settings enable
|
||||
|
||||
## 참조 수만으로 dead code를 찾으면 안 되는 이유
|
||||
|
||||
이 사례는 mandatory public-reachability probe가 필요한 이유를 잘 보여준다. static reference count만으로 dead code를 찾으면 Spring discovery path를 오탐한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c21
|
||||
title: 이름이 비슷한 두 번역 계열의 출력 계약이 다르다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c21
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c21
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c21.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c21.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1358 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 이름이 비슷한 두 번역 계열의 출력 계약이 다르다
|
||||
|
||||
이 scope에는 이름이 비슷한 두 translation mechanism이 있다. 같은 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 scope에는 이름이 비슷한 두 translation mechanism이 있다.
|
||||
|
||||
`PersistenceFailureTranslatorChain`은 raw persistence/provider failure를 받아 `JpaPersistenceException` 계층을 내놓고, `SpringJpaTransactionExecutor`와 retry semantics가 소비한다. 목적은 SQLSTATE/optimistic conflict를 retry/completion semantics에 필요한 stable persistence failure로 바꾸는 것이다.
|
||||
|
||||
## 분석 원문의 두 계열 비교
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c21" alt="분석 문서 analysis/05-adapter-outbound-persistence-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/05-adapter-outbound-persistence-jpa.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 중복 구현이 아니라 출력 계약이 다르다
|
||||
|
||||
다른 하나의 consumer는 adapter/application error boundary 쪽이다. 따라서 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. **transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c22
|
||||
title: adapter 오류 경계 쪽 번역기는 다른 출력을 낸다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c22
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c22
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c22.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c22.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1384 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# adapter 오류 경계 쪽 번역기는 다른 출력을 낸다
|
||||
|
||||
`failure.PersistenceExceptionTranslator`의 consumer는 adapter/application error boundary 쪽이다. retry algebra 쪽 번역기와 출력 계약이 다르다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`failure.PersistenceExceptionTranslator`의 consumer는 adapter/application error boundary 쪽이다. 따라서 retry algebra 쪽 번역기와 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다.
|
||||
|
||||
## 분석 원문의 소비자 서술
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c22" alt="분석 문서 analysis/05-adapter-outbound-persistence-jpa.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/05-adapter-outbound-persistence-jpa.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 번역기가 나누는 출력 계약
|
||||
|
||||
**transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c23
|
||||
title: 설치되는 bean 셋과 설치되지 않는 구현 셋
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c23
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c23
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c23.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c23.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1416 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 설치되는 bean 셋과 설치되지 않는 구현 셋
|
||||
|
||||
`JpaPlatformRuntimeAutoConfiguration`이 만드는 bean은 셋이고, 문서상 completion-evidence/operability의 핵심인 세 구현은 current root assembly에서 provider가 없다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`JpaPlatformRuntimeAutoConfiguration`이 만드는 것은 셋이다.
|
||||
|
||||
- `SpringJpaTransactionExecutor` — `PlatformTransactionManager`가 있을 때
|
||||
- `FullTransactionRetryCoordinator` — executor가 있을 때
|
||||
- default empty `RetryEventListener`
|
||||
|
||||
## JpaPlatformRuntimeAutoConfiguration 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c23" alt="코드베이스에서 JpaPlatformRuntimeAutoConfiguration 를 검색한 출력 12줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaPlatformRuntimeAutoConfiguration 코드베이스 검색 — 12줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 현재 설치되지 않는 특화 구현 셋
|
||||
|
||||
`EvidenceAwareJpaTransactionManager`, `CompletionUnknownRecorder`, `JpaTransactionObservation` call path. 이 셋은 문서상 completion-evidence/operability의 핵심이지만 current root assembly에서 provider가 없다. 이 sibling comparison으로 "class가 있으니 feature가 있다"는 판단을 피했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c26
|
||||
title: 쓰기 경로마다 트랜잭션 소유가 다르다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c26
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c26
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c26.svg
|
||||
- key: adapter-outbound-persistence-jpa-c26-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c26.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c26.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L1819 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 쓰기 경로마다 트랜잭션 소유가 다르다
|
||||
|
||||
`HibernateBulkDmlExecutor`는 arbitrary JPQL을 아무 데서나 실행하는 helper가 아니고, `HibernateStatelessSessionRunner`는 이 platform에서 transaction ownership 예외를 명시적으로 드러낸다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`HibernateBulkDmlExecutor`는 arbitrary JPQL string을 아무 데서나 실행하는 helper가 아니다. operation name 등록, affected-row expectation, persistence-context cleanup, transaction requirement를 contract로 둔다. bulk DML은 managed entity lifecycle을 우회하므로 ordinary entity save와 같은 audit/lifecycle guarantee를 기대하면 안 된다.
|
||||
|
||||
## 쓰기 경로마다 다른 소유 모델
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c26-diagram" alt="쓰기 경로에서 일반 repository adapter 와 bulk DML 과 StatelessSession runner 로 화살표가 나가고 화살표마다 트랜잭션 소유 방식이 붙은 구조" caption="쓰기 경로마다 다른 소유 모델" zoom="false"
|
||||
:::
|
||||
|
||||
## HibernateBulkDmlExecutor 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c26" alt="코드베이스에서 HibernateBulkDmlExecutor 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HibernateBulkDmlExecutor 코드베이스 검색 — 7줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 사용 중이라고 주장하지 않는 이유
|
||||
|
||||
support matrix도 이를 Advanced capability로 분리한다. 현재 production business consumer는 확인되지 않았고 PostgreSQL integration fixture에서 실제 behavior를 qualification한다. 따라서 "runtime에서 사용 중"이라고 주장하지 않는다.
|
||||
|
||||
## 이름만 row cap 이던 문제는 고쳐져 있다
|
||||
|
||||
`HibernateStatelessSessionRunner`는 오히려 이 platform에서 transaction ownership 예외를 명시적으로 드러낸다 — 일반 repository adapter는 application transaction boundary에 참여하고, StatelessSession runner는 그렇지 않다. 과거 review에서는 caller가 선언한 maxRows가 실제 affected rows와 연결되지 않는 문제가 있었다. 현재는 `StatelessWorkResult(value, affectedRows)`를 요구하고 cap 초과 시 commit 전에 rollback한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c37
|
||||
title: 중복 재생이 owner 검증보다 먼저 저장된 owner를 돌려준다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c37
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c37
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c37.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c37.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L2734 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 중복 재생이 owner 검증보다 먼저 저장된 owner를 돌려준다
|
||||
|
||||
`PostgreSqlSameStoreInboxAdapter`와 `PostgreSqlPollingDeliveryAdapter`는 owner-safe transition contract를 구현하지만 현재 production composition에서 bean construction이나 stereotype이 확인되지 않았다. 따라서 아래는 채택 시 활성화되는 latent defect다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`PostgreSqlSameStoreInboxAdapter`와 `PostgreSqlPollingDeliveryAdapter`는 `application-core`의 owner-safe transition contract를 구현하지만, 현재 production composition에서 bean construction이나 stereotype은 확인되지 않았다. 따라서 아래 finding은 **현재 배포 기본 경로의 즉시 장애가 아니라, 이 candidate adapter를 채택할 때 활성화되는 latent defect**로 분리한다.
|
||||
|
||||
## markProcessing이 검증보다 먼저 owner를 돌려준다
|
||||
|
||||
`markProcessing()`은 같은 `START + operationId`를 발견하면 `classifyMismatch()`보다 먼저 `owner(row)`를 반환한다. 이 때문에 scope/operation id만 맞춘 forged owner로 replay하면 DB에 저장된 실제 owner token을 돌려받을 수 있다. 실제 PostgreSQL probe는 이렇다.
|
||||
|
||||
```text
|
||||
inboxForgedReplay.outcome=PROCESSING_STARTED
|
||||
inboxForgedReplay.returnedActualToken=true
|
||||
inboxForgedReplay.returnedForgedToken=false
|
||||
inboxForgedReplay.completeWithReturnedOwner=COMPLETED
|
||||
```
|
||||
|
||||
즉 duplicate handling이 owner capability recovery oracle처럼 동작한다. 채택 전에는 duplicate replay에서도 persisted owner tuple/revision과 supplied owner를 먼저 검증하도록 고쳐야 한다.
|
||||
|
||||
## PostgreSqlSameStoreInboxAdapter 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c37" alt="코드베이스에서 PostgreSqlSameStoreInboxAdapter 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="PostgreSqlSameStoreInboxAdapter 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## digest가 담지 않는 두 인자
|
||||
|
||||
`markRetryable`/`markDead`의 `retention`은 실제 SQL update에는 들어가지만 transition digest에는 들어가지 않는다. 동일 operation id로 retention만 바꾼 replay가 same-operation으로 흡수된다. retention은 terminal row 보존 기간을 결정하는 semantic argument이므로 digest에 canonical millis를 포함해야 한다.
|
||||
|
||||
`markRetryable()`은 `nextAttemptAt`을 DB에 기록하지만 transition digest는 kind + operation + owner + errorCode만 포함한다. 재시도 시각은 delivery scheduling 자체를 바꾸는 semantic argument다.
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c42
|
||||
title: 여섯 package만 스캔하므로 같은 리프 안에서도 도달성이 다르다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c42
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c42
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c42.svg
|
||||
- key: adapter-outbound-persistence-jpa-c42-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c42.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c42.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L2971 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# 여섯 package만 스캔하므로 같은 리프 안에서도 도달성이 다르다
|
||||
|
||||
`JpaAdapterComponentsConfig`는 adapter 전체를 넓게 scan하지 않고 `audit`·`failure`·`idempotency`·`lock`·`outbox`·`transaction` 여섯만 명시적으로 component scan한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`JpaAdapterComponentsConfig`는 adapter 전체를 넓게 scan하지 않고 `audit`·`failure`·`idempotency`·`lock`·`outbox`·`transaction` 여섯 package만 명시적으로 component scan한다.
|
||||
|
||||
## narrow scan이 닿는 범위
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c42-diagram" alt="스캔 경계 안에 여섯 package 가 두 상자로 들어 있고 두 어댑터가 경계 밖 빗금 상자로 놓인 구조" caption="narrow scan 이 닿는 범위" zoom="false"
|
||||
:::
|
||||
|
||||
`OutboxStoreAdapter`는 baseline scan에 들어가고 `app-bootstrap`의 `OutboxConfig`가 `OutboxStorePort`로 사용한다. `DurableOperationStoreAdapter`, `JpaLiveEventReplayAdapter`는 현재 baseline component scan에 들어가지 않고 별도 production constructor/reference도 확인되지 않았다.
|
||||
|
||||
## JpaAdapterComponentsConfig 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c42" alt="코드베이스에서 JpaAdapterComponentsConfig 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JpaAdapterComponentsConfig 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## default composition에 들어가지 않는 것
|
||||
|
||||
`HibernateCacheGuard`, `HibernateEnversHistoryReader`와 Spring Data auditing candidate도 default composition에 들어가지 않는다. runtime-role verifier 자체는 app-bootstrap bean으로 구성되지만, policy를 적용하는 `requireSafe()` caller가 없다.
|
||||
|
||||
## 판정을 세 등급으로 나누는 이유
|
||||
|
||||
이 차이 때문에 아래 finding은 `production`, `conditional-production`, `latent`를 분리해 판정한다. 정적 composition snapshot은 `evidence/raw/072-baseline-capability-reachability.txt`에 남겼다.
|
||||
|
||||
<!-- body:end -->
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-jpa-c51
|
||||
title: Stable 스캔 목록에 experimental package가 이미 들어 있다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-jpa-c51
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-jpa-c51
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-jpa-c51.svg
|
||||
- key: adapter-outbound-persistence-jpa-c51-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-persistence-jpa-c51.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-jpa-c51.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md#L3609 이다.
|
||||
module: adapter-outbound-persistence-jpa
|
||||
---
|
||||
|
||||
# Stable 스캔 목록에 experimental package가 이미 들어 있다
|
||||
|
||||
현재 production call graph에서 experimental 타입을 조립하는 경로는 없다. 그러나 structural opt-in은 완전히 닫혀 있지 않다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
현재 repository 내부 production call graph에서는 `TenantDataSourceRegistry`, `TenantEntityManagerFactoryRegistry`, `SchemaMultiTenantConnectionProvider`, `ConsistencyAwareDataSourceRouter`, `RlsTenantSessionBinder`, `SchemaTenantMigrationOrchestrator` 등을 app-bootstrap이나 다른 production leaf가 조립하는 경로를 찾지 못했다. `backend.jpa.experimental.*` property도 production configuration에서 읽어 bean을 만드는 경로가 없고, 실제 문자열은 `ExperimentalFeature` enum의 property vocabulary에만 존재한다.
|
||||
|
||||
## TenantDataSourceRegistry 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c51" alt="코드베이스에서 TenantDataSourceRegistry 를 검색한 출력 8줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="TenantDataSourceRegistry 코드베이스 검색 — 8줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## latent로 분류한 이유
|
||||
|
||||
따라서 아래 semantic finding은 **현재 app-bootstrap runtime에서 즉시 활성화된 production defect가 아니라 latent experimental defect**로 분류한다. 이 구분은 중요하다 — public API surface에 올라 있고 같은 artifact에 포함된 library code가 잘못된 것과, 현재 기본 애플리케이션이 그 code를 실제 실행하는 것은 다른 주장이다.
|
||||
|
||||
## Stable 스캔 목록에 들어 있는 것
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-jpa-c51-diagram" alt="Stable EntityScan 목록에서 stable package 목록과 experimental package 로 화살표가 나가고 experimental 쪽만 빗금 상자로 표시된 구조" caption="Stable 스캔 목록에 들어 있는 것" zoom="false"
|
||||
:::
|
||||
|
||||
반면 structural opt-in은 완전히 닫혀 있지 않다. `PersistenceJpaConfig`의 Stable `@EntityScan`과 `@EnableJpaRepositories` 문자열 목록에는 이미 `dev.caskeleton.adapter.outbound.persistence.experimental`이 들어 있다. 현재 experimental package에는 `@Entity`, `@Repository`, `JpaRepository`, `@MappedSuperclass`가 없어서 당장 persistence unit에 들어오는 concrete JPA type은 없지만, 이후 experimental entity/repository 하나가 추가되면 별도 feature condition 없이 Stable persistence unit이 스캔한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: adapter-outbound-persistence-mongo-c03
|
||||
title: 아직 쓰이지 않은 연산에도 같은 불변식이 성립하게 만든 순서
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:adapter-outbound-persistence-mongo-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: adapter-outbound-persistence-mongo-c03
|
||||
file: ../../../final/evidence/rendered/adapter-outbound-persistence-mongo-c03.svg
|
||||
- key: adapter-outbound-persistence-mongo-c03-diagram
|
||||
file: ../../../final/assets/diagrams/adapter-outbound-persistence-mongo-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/adapter-outbound-persistence-mongo-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L586 이다.
|
||||
module: adapter-outbound-persistence-mongo
|
||||
---
|
||||
|
||||
# 아직 쓰이지 않은 연산에도 같은 불변식이 성립하게 만든 순서
|
||||
|
||||
`DefaultMongoImperativeExecutor.executeInternal(...)`이 collection profile 해석 → observation 개시 → consistency 바인딩 → callback 실행 → 실패 번역(최대 한 번) → observation 종료 순서를 고정한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DefaultMongoImperativeExecutor.executeInternal(...)`은 순서를 고정한다 — collection profile 해석 → observation 개시 → consistency 바인딩 → callback 실행 → 실패 번역(최대 한 번) → observation 종료. javadoc이 이유를 적는다: "Fixing it here is what makes the invariants hold for operations nobody has written yet."
|
||||
|
||||
## 실행 scope의 고정된 순서
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-mongo-c03-diagram" alt="profile 해석과 observation 개시와 consistency 바인딩과 callback 실행이 왼쪽에서 오른쪽으로 이어지는 구조" caption="실행 scope 의 고정된 순서" zoom="false"
|
||||
:::
|
||||
|
||||
## 세 가지 방어
|
||||
|
||||
- 이미 번역된 `MongoPersistenceException`은 그대로 통과시킨다. 재번역하면 bulk partial failure나 guardrail 거절처럼 **그것을 던진 계층이 더 잘 아는** category를, driver 코드에서 유도한 일반 category로 덮어쓰게 된다.
|
||||
- Spring이 감싼 driver 예외를 `unwrap(...)`으로 되꺼낸다. Spring의 번역은 error label을 잃는데, label이야말로 replayable transaction과 unknown commit을 가르는 값이다.
|
||||
- `MongoCompletion.successOutcomeFor(operationType)`가 read와 write의 성공 outcome을 나눈다.
|
||||
|
||||
## MongoPersistenceException 참조 위치
|
||||
|
||||
:::evidence key="adapter-outbound-persistence-mongo-c03" alt="코드베이스에서 MongoPersistenceException 를 검색한 출력 28줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MongoPersistenceException 코드베이스 검색 — 28줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 성공 outcome을 read와 write로 나눈 이유
|
||||
|
||||
과거에는 두 executor 모두 성공을 `WRITE_CONFIRMED`로 기록해, "write가 acknowledge되고 있는가"를 답하는 지표가 read 트래픽의 함수가 됐다. `default` 분기가 `READ_CONFIRMED`로 떨어지는 것도 의도적이다 — "the honest answer is the one that claims least".
|
||||
|
||||
## 동적 collection 이름을 우회할 방법이 없는 이유
|
||||
|
||||
`MongoCollectionProfileRegistry`가 "동적 collection 이름 금지"를 강제 가능하게 만드는 지점이다. 애플리케이션은 profile을 부르고 물리 이름은 이 registry만 안다. `ScopedAccess.collection(String)`은 요청된 collection이 scope의 것과 다르면 거부하고, `ScopedMongoOperations`의 어떤 메서드도 collection 인자를 받지 않는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: application-core-c03
|
||||
title: 예외가 발생했다는 것을 rollback으로 단순화하지 않는다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:application-core-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: application-core-c03
|
||||
file: ../../../final/evidence/rendered/application-core-c03.svg
|
||||
- key: application-core-c03-diagram
|
||||
file: ../../../final/assets/diagrams/application-core-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/application-core-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/03-application-core.md#L88 이다.
|
||||
module: application-core
|
||||
---
|
||||
|
||||
# 예외가 발생했다는 것을 rollback으로 단순화하지 않는다
|
||||
|
||||
`TransactionPolicyId`가 Spring propagation 숫자 대신 application semantic ID를 노출하고, `TransactionResult`가 commit 결과를 다섯 상태로 분리한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **legacy storage/notification compatibility surface의 제거 조건 추적**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`TransactionPort`는 `inWrite`, `inRootWrite`, `inRead`, `inNew` 네 개의 framework-neutral boundary를 노출한다. `PolicyTransactionPort`는 기존 surface를 깨지 않고 `TransactionRequest -> TransactionResult` 정책 기반 API를 추가한다.
|
||||
|
||||
## 노출하는 어휘와 감추는 어휘
|
||||
|
||||
:::evidence key="application-core-c03-diagram" alt="application-core 경계 안에 정책 식별자와 결과 대수가 들어 있고 Spring 전파 숫자가 경계 밖 빗금 상자로 놓인 구조" caption="노출하는 어휘와 감추는 어휘" zoom="false"
|
||||
:::
|
||||
|
||||
`TransactionPolicyId`는 Spring propagation 숫자가 아니라 `COMMAND_DEFAULT`, `COMMAND_SERIALIZABLE_REPLAY_SAFE`, `QUERY_PRIMARY`, `QUERY_REPLICA_ELIGIBLE`, `OUTBOX_APPEND`, `INBOX_AND_HANDLER`, `MAINTENANCE_NEW`처럼 application semantic ID를 노출한다. `TransactionRequest` constructor는 read policy의 consistency allowlist, non-read의 readConsistency 금지, operationId-required policy의 stable id 존재를 fail-fast한다.
|
||||
|
||||
## 이 기록이 다루는 파일 범위
|
||||
|
||||
:::evidence key="application-core-c03" alt="코드베이스에서 파일 목록을 만든 출력 25줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 25줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## commit 결과 다섯 상태
|
||||
|
||||
- `Committed`: physical commit을 확인한 결과.
|
||||
- `Participating`: outer transaction에 참여했지만 아직 commit을 주장할 수 없는 결과.
|
||||
- `DeterminateRollback`: rollback이 확정된 실패.
|
||||
- `Indeterminate`: commit 여부를 확정할 수 없는 결과.
|
||||
- `CommittedWithPostCommitFailure`: commit은 됐지만 이후 operational cleanup이 실패한 결과.
|
||||
|
||||
## Indeterminate를 정식 상태로 두는 이유
|
||||
|
||||
이 algebra의 핵심은 "exception이 발생했다 = rollback"으로 단순화하지 않는 것이다. `Indeterminate`는 last observed transaction phase와 optional reconciliation reference를 보존하며, `CompletionResolution`은 `STILL_UNKNOWN`을 정식 상태로 둔다. 불확실한 commit을 임의로 NOT_COMMITTED로 가정해 use case를 재실행하는 것을 피한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: grpc-server-c01
|
||||
title: 승인이 마감보다 먼저이고 멱등이 검증보다 먼저다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:grpc-server-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-server-c01
|
||||
file: ../../../final/evidence/rendered/grpc-server-c01.svg
|
||||
- key: grpc-server-c01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-server-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-server-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-server.md#L53 이다.
|
||||
module: grpc-server
|
||||
---
|
||||
|
||||
# 승인이 마감보다 먼저이고 멱등이 검증보다 먼저다
|
||||
|
||||
인터셉터가 열 단계이고 선언 순서가 계약이다. 각 위치의 이유가 열거형 javadoc에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
열 단계이고 선언 순서가 계약이다. 각 위치의 이유가 열거형 javadoc 에 있다.
|
||||
|
||||
```text
|
||||
EXCEPTION_BOUNDARY → TRACE → AUTHENTICATION → ACTOR_TENANT → AUTHORIZATION
|
||||
→ ADMISSION → DEADLINE_CANCELLATION → IDEMPOTENCY → VALIDATION → SERVICE_ADAPTER
|
||||
```
|
||||
|
||||
## 열 단계 중 안쪽 다섯
|
||||
|
||||
:::evidence key="grpc-server-c01-diagram" alt="승인과 마감 취소와 멱등과 검증과 서비스 어댑터가 왼쪽에서 오른쪽으로 이어지고 화살표마다 넘어가는 값이 붙은 구조" caption="열 단계 중 안쪽 다섯" zoom="false"
|
||||
:::
|
||||
|
||||
- 예외 경계가 가장 바깥 — 이후 단계의 실패가 매핑되지 않은 상태로 새지 않는다
|
||||
- 인증 → 행위자·소속 → 인가 — 각 단계가 앞 단계의 답을 필요로 한다
|
||||
- 승인이 마감보다 먼저 — 부하 중 서버가 일을 쓰기 전에 흘려보낸다
|
||||
- 멱등이 검증보다 먼저 — 재생된 요청이 이미 받아들인 본문을 다시 검증하지 않고 저장된 결과를 돌려준다
|
||||
- 검증이 어댑터 직전 — 사용 사례는 믿을 수 있는 메시지를 받는다
|
||||
|
||||
## 이 기록이 다루는 파일 범위
|
||||
|
||||
:::evidence key="grpc-server-c01" alt="코드베이스에서 파일 목록을 만든 출력 17줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 17줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 선택적인 단계는 하나뿐이다
|
||||
|
||||
필수가 아닌 단계는 멱등 하나다 — 상태 변경 키 메서드가 없는 서버에는 할 일이 없기 때문이다.
|
||||
|
||||
<!-- body:end -->
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-inbox-jdbc-postgresql-c01
|
||||
title: 메커니즘 전체가 하나의 SQL 문장에 있다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-inbox-jdbc-postgresql-c01
|
||||
file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L57 이다.
|
||||
module: messaging-inbox-jdbc-postgresql
|
||||
---
|
||||
|
||||
# 메커니즘 전체가 하나의 SQL 문장에 있다
|
||||
|
||||
`messaging-reliability-api`의 `InboxRepository`·`IdempotentMessageHandler` 포트를 PostgreSQL로 구현한다. 중복 제거 메커니즘 전체가 복합 기본키 하나에 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`messaging-reliability-api`의 `InboxRepository`·`IdempotentMessageHandler` 포트를 PostgreSQL로 구현한다. 이름이 기술을 드러낸다 — `docs/messaging/support-matrix.md`가 그 개명 이유를 적는다(MSG-023).
|
||||
|
||||
**메커니즘 전체가 하나의 SQL 문장에 있다.**
|
||||
|
||||
```sql
|
||||
INSERT INTO messaging_inbox (message_id, consumer_id, processed_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (message_id, consumer_id) DO NOTHING
|
||||
```
|
||||
|
||||
> "Reservation is an `INSERT ... ON CONFLICT DO NOTHING` whose affected-row count is the answer: one means first delivery, zero means already processed. The composite primary key does the work, so there is no read-then-write race — two concurrent deliveries of the same message cannot both see 'not processed' and both proceed."
|
||||
|
||||
## InboxRepository 참조 위치
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-c01" alt="코드베이스에서 InboxRepository 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxRepository 코드베이스 검색 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 실제로 실행되는 레인
|
||||
|
||||
migration이 같은 사실을 반대편에서 적고, `build.gradle` 주석이 테스트 전략을 명시한다. **그리고 실제로 실행된다** — `InboxPostgresIT` 6개가 기본 `test` 태스크에서 통과한다(§10).
|
||||
|
||||
## migration이 같은 사실을 적는다
|
||||
|
||||
복합 기본키가 중복 제거 메커니즘이고, 예약은 핸들러의 부작용과 같은 트랜잭션 안에서 성공하거나 키를 위반하는 INSERT다.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-inbox-jdbc-postgresql-c02
|
||||
title: starter가 셋을 만들고 InboxRepository는 그 목록에 없다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-inbox-jdbc-postgresql-c02
|
||||
file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L98 이다.
|
||||
module: messaging-inbox-jdbc-postgresql
|
||||
---
|
||||
|
||||
# starter가 셋을 만들고 InboxRepository는 그 목록에 없다
|
||||
|
||||
starter의 `MessagingReliabilityAutoConfiguration`이 세 bean을 만드는데 `JdbcInboxRepository`는 그 목록에 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
들어오는 것: `messaging-core-api`(api), `messaging-reliability-api`(api), `spring-jdbc`·`spring-tx`(implementation). 나가는 것: `messaging-spring-boot-starter`.
|
||||
|
||||
## MessagingReliabilityAutoConfiguration 참조 위치
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-c02" alt="코드베이스에서 MessagingReliabilityAutoConfiguration 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingReliabilityAutoConfiguration 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 만들어지는 것과 만들어지지 않는 것
|
||||
|
||||
**배선됨.** starter의 `MessagingReliabilityAutoConfiguration`이 셋을 만든다. `JdbcInboxRepository`는 그 목록에 없다 — `InboxRepository` bean을 누가 만드는지는 starter leaf가 답한다.
|
||||
|
||||
## Spring 타입을 쓰는 두 곳
|
||||
|
||||
`DataSourceUtils`와 `TransactionSynchronizationManager`. 둘 다 `implementation` scope이고 public 시그니처에 나오지 않으므로 vendor `api` 규칙에 맞는다.
|
||||
|
||||
<!-- body:end -->
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-inbox-jdbc-postgresql-c03
|
||||
title: 두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-inbox-jdbc-postgresql-c03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-inbox-jdbc-postgresql-c03
|
||||
file: ../../../final/evidence/rendered/messaging-inbox-jdbc-postgresql-c03.svg
|
||||
- key: messaging-inbox-jdbc-postgresql-c03-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-inbox-jdbc-postgresql-c03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-inbox-jdbc-postgresql-c03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md#L146 이다.
|
||||
module: messaging-inbox-jdbc-postgresql
|
||||
---
|
||||
|
||||
# 두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다
|
||||
|
||||
`requireActiveTransaction`이 이 leaf에서 가장 중요한 안전 장치이고, 이전 결함이 javadoc에 있다. 현재는 interface 메서드가 세 가지를 확인한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **컬럼 폭은 애플리케이션 검증과 짝을 이룬다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 leaf에서 가장 중요한 안전 장치이고 이전 결함이 javadoc에 있다.
|
||||
|
||||
> "Package-private. It used to be public and was the only path that actually joined the caller's transaction, while the interface method — the one `IdempotentConsumer` calls — opened a raw connection that auto-commits. A reservation that commits on its own while the business side effect rolls back is a message that will never be redelivered and whose work never happened."
|
||||
|
||||
**두 개의 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다.**
|
||||
|
||||
## 예약 전에 확인하는 세 가지
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-c03-diagram" alt="requireActiveTransaction 경계 안에 활성 트랜잭션과 읽기 전용 아님과 같은 DataSource 세 상자가 나란히 들어 있는 구조" caption="예약 전에 확인하는 세 가지" zoom="false"
|
||||
:::
|
||||
|
||||
| 검사 | 실패 시 메시지의 핵심 |
|
||||
|---|---|
|
||||
| `isActualTransactionActive()` | "a reservation that commits alone marks a message processed whose work may still roll back" |
|
||||
| `!isCurrentTransactionReadOnly()` | "the current one is read-only" |
|
||||
| `hasResource(dataSource)` | "it is bound to another, so the reservation and the side effect would commit independently" |
|
||||
|
||||
세 번째가 특히 정교하다 — **트랜잭션이 활성이어도 다른 DataSource에 묶여 있으면 거절한다.** 멀티 데이터소스 배포에서 실제로 발생하는 형태이고, 그 경우 예약과 부작용이 서로 다른 트랜잭션에 들어간다. 세 검사 전부 같은 코드 `INBOX_TRANSACTION_REQUIRED`를 쓴다 — 메시지만 다르다.
|
||||
|
||||
## InboxPostgresIT 참조 위치
|
||||
|
||||
:::evidence key="messaging-inbox-jdbc-postgresql-c03" alt="코드베이스에서 InboxPostgresIT 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxPostgresIT 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
**"the path nobody exercises before production"**가 이 leaf의 테스트 전략을 설명한다 — `InboxPostgresIT.aRolledBackTransactionLeavesNoReservationAndNoSideEffect`가 정확히 그 경로를 실 DB에서 돈다.
|
||||
|
||||
## 위임과 검증이 짝을 이룬다
|
||||
|
||||
`TransactionRunner`가 함수형 인터페이스이고 `<T> T inTransaction(Supplier<T> work)` 하나다. 즉 이 leaf는 Spring `@Transactional`에 의존하지 않고 **경계 제공을 호출자에게 위임**한다. `JdbcInboxRepository.requireActiveTransaction`이 그 위임이 지켜졌는지를 런타임에 확인한다.
|
||||
|
||||
## 중복이 정상 결과라는 명시
|
||||
|
||||
"A duplicate is not an error. It is the expected consequence of at-least-once delivery, so the skip path is a normal outcome rather than an exception." 세 금지가 `messaging-reliability-api`의 `TransactionalMessageAction` javadoc이 구현자에게 요구한 것과 대칭이다 — 그쪽은 action에게, 이쪽은 handler에게.
|
||||
|
||||
<!-- body:end -->
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-outbox-jdbc-postgresql-c01
|
||||
title: 모르는 것을 실패로 취급하지 않는다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-c01
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c01.svg
|
||||
- key: messaging-outbox-jdbc-postgresql-c01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-outbox-jdbc-postgresql-c01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L66 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
---
|
||||
|
||||
# 모르는 것을 실패로 취급하지 않는다
|
||||
|
||||
트랜잭셔널 아웃박스의 PostgreSQL 구현이다. 이 리프의 축은 하나다 — 모호한 발행을 같은 메시지 id로 재시도한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
**트랜잭셔널 아웃박스의 PostgreSQL 구현**이다. 비즈니스 트랜잭션이 쓰고 릴레이가 배출한다. 이 리프의 축은 하나다: **"모르는 것을 실패로 취급하지 않는다."**
|
||||
|
||||
> "The relay's correctness rests on one rule: an ambiguous publish is retried **under the same message id**. Minting a new id would turn a possibly-delivered message into a definitely-second message, and no downstream deduplication could recover from it. Marking it failed instead would lose a message the broker may already hold."
|
||||
|
||||
마지막 문장이 중요하다 — 릴레이는 at-least-once publication만 보장한다고 자기 상한을 스스로 명시한다.
|
||||
|
||||
## 이 리프가 아는 것과 모르는 것
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-c01-diagram" alt="리프 경계 안에 발행 포트와 관계형 데이터베이스가 들어 있고 브로커와 스프링 컨텍스트가 경계 밖 점선 상자로 놓인 구조" caption="이 리프가 아는 것과 모르는 것" zoom="false"
|
||||
:::
|
||||
|
||||
경계: 브로커를 모른다(`MessagePublisher` 포트만 안다). 스프링 컨텍스트를 모른다(`spring-jdbc`/`spring-tx` 는 `implementation` 이며 트랜잭션 동기화 조회에만 쓴다). 배선은 starter 몫이다.
|
||||
|
||||
## MessagePublisher 참조 위치
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-c01" alt="코드베이스에서 MessagePublisher 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagePublisher 코드베이스 검색 — 14줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 파괴적 작업 저널이 같이 사는 이유
|
||||
|
||||
`messaging-admin-api` 의 파괴적 작업 저널 구현도 이 리프에 산다. build.gradle 이 그 이유를 적는다 — "The destructive-operation journal lives here because it needs exactly what the outbox needs: one relational database every replica can see, and a migration lane that already exists. The contract it implements belongs to the admin API."
|
||||
|
||||
<!-- body:end -->
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-outbox-jdbc-postgresql-c02
|
||||
title: starter가 만들지 않는 셋은 애플리케이션이 직접 등록해야 한다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-c02
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L102 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
---
|
||||
|
||||
# starter가 만들지 않는 셋은 애플리케이션이 직접 등록해야 한다
|
||||
|
||||
`JdbcOutboxRepository`, `OutboxEnvelopeFactory`, `JdbcAdminOperationJournal` 셋은 starter가 만들지 않는다. 애플리케이션이 `DataSource`/`ProducerId`를 알고 직접 등록해야 한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
testcontainers 주석이 이 리프의 성격을 요약한다 — "신뢰성 패턴은 트랜잭션 경계와 유일성 제약에 대한 주장이고, 그것을 결판낼 수 있는 것은 실제 데이터베이스뿐이다." 그리고 그 레인이 **실제로 돈다**(§10).
|
||||
|
||||
## JdbcOutboxRepository 참조 위치
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-c02" alt="코드베이스에서 JdbcOutboxRepository 를 검색한 출력 5줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="JdbcOutboxRepository 코드베이스 검색 — 5줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## starter가 만들지 않는 셋
|
||||
|
||||
`JdbcOutboxRepository`, `OutboxEnvelopeFactory`, `JdbcAdminOperationJournal`(`EVD-312`). 셋 다 애플리케이션이 `DataSource`/`ProducerId` 를 알고 직접 등록해야 한다.
|
||||
|
||||
## 기본 저널이 프로덕션에서 거부되는 이유
|
||||
|
||||
`AdminOperationJournal` 의 기본값은 `InMemoryAdminOperationJournal` 이며, 프로덕션 프로파일에서는 `MessagingAdminDurabilityValidator` 가 그것을 거부한다(`analysis/messaging/messaging-admin-runtime.md` §4.4 참조).
|
||||
|
||||
<!-- body:end -->
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-outbox-jdbc-postgresql-c04
|
||||
title: 쓰기와 배출과 정리와 저널이 각각 다른 경로다
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-c04
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L430 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
---
|
||||
|
||||
# 쓰기와 배출과 정리와 저널이 각각 다른 경로다
|
||||
|
||||
네 실행 경로가 있고 각각 진입점과 종료 조건이 다르다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
네 실행 경로가 있고 각각 진입점과 종료 조건이 다르다.
|
||||
|
||||
**쓰기** — 비즈니스 트랜잭션 → `append(record)` → 트랜잭션 3중 검사 → `DataSourceUtils.getConnection` → INSERT(22컬럼).
|
||||
|
||||
**배출** — `MessagingOutboxRelayLifecycle` → `worker.start()` → `runPass()` → `relay.runOnce(now)` → 청구/발행/종결 → `scheduler.backoff(unproductive)` → 다음 패스 자기 스케줄링.
|
||||
|
||||
## DataSourceUtils 참조 위치
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-c04" alt="코드베이스에서 DataSourceUtils 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DataSourceUtils 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 정리와 admin 저널
|
||||
|
||||
**정리** — `OutboxCleanupJob.runOnce(now)` → `cutoff = now - retention` → `purgePublishedBefore(cutoff)` **무제한 오버로드** ×(최대 `maxBatches`, 실제로는 2회) → §12.1(a).
|
||||
|
||||
**admin 저널** — `begin` → INSERT ON CONFLICT / TAKE_OVER → `checkpoint` × N → `complete` 또는 `fail`.
|
||||
|
||||
<!-- body:end -->
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: messaging-outbox-jdbc-postgresql-c06
|
||||
title: 만들어졌지만 아무도 부르지 않는다를 이름 붙인 유일한 자리
|
||||
topic: transaction-and-consistency-models
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:messaging-outbox-jdbc-postgresql-c06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-outbox-jdbc-postgresql-c06
|
||||
file: ../../../final/evidence/rendered/messaging-outbox-jdbc-postgresql-c06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-outbox-jdbc-postgresql-c06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-outbox-jdbc-postgresql.md#L767 이다.
|
||||
module: messaging-outbox-jdbc-postgresql
|
||||
---
|
||||
|
||||
# 만들어졌지만 아무도 부르지 않는다를 이름 붙인 유일한 자리
|
||||
|
||||
SQL 마이그레이션과 javadoc이 함께 이력을 이룬다. 여덟 개의 "이전에는 이랬다" 중 마지막 둘이 이 저장소에서 반복되는 결함 계열을 명시적으로 이름 붙인다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
SQL 마이그레이션과 javadoc 이 함께 이력을 이룬다. 여덟 개의 "이전에는 이랬다".
|
||||
|
||||
| 위치 | 기록된 과거 결함 |
|
||||
|---|---|
|
||||
| `V2:3-14` | 리스만으로는 stale relay 가 PUBLISHED 위에 AMBIGUOUS 를 덮어썼다 |
|
||||
| `V2:25-27` | "V1's CHECK listed five states, so writing the sixth failed at the constraint rather than at review" |
|
||||
| `V4:6-9` | 정경 필드가 갈 곳이 없어 유실되거나 `msg.*` 로 밀반입되었다 |
|
||||
| `V4:56-61` | Debezium 키가 `destination` 이라 한 토픽의 모든 메시지가 한 파티션에 몰렸다 |
|
||||
| `JdbcOutboxRepository:205-209` | `append` 가 풀에서 raw 커넥션을 열어 자동 커밋했다 — "a business transaction that rolled back afterwards left the event behind" |
|
||||
| `JdbcOutboxRepository:630-636` | 이스케이프가 역슬래시와 따옴표만 처리해 제어문자가 JSONB 를 깨뜨렸다 |
|
||||
| `OutboxRelay:117-123` | "The scheduler was built by the auto-configuration and handed to nobody" |
|
||||
| `OutboxRelayWorker:18-21` | "The relay, its retry scheduler and the attempt budget all existed and nothing ever called `runOnce`" |
|
||||
|
||||
## 이 기록이 다루는 파일 범위
|
||||
|
||||
:::evidence key="messaging-outbox-jdbc-postgresql-c06" alt="코드베이스에서 파일 목록을 만든 출력 13줄. 이 기록이 다루는 범위가 그 목록이다." caption="코드베이스 파일 목록 — 13줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 결함 계열에 이름이 붙은 유일한 자리
|
||||
|
||||
마지막 두 개(`OutboxRelay:117-123`, `OutboxRelayWorker:18-21`)가 이 저장소 전체에서 반복되는 결함 계열 — **"만들어졌지만 아무도 부르지 않는다"** — 을 명시적으로 이름 붙인 유일한 자리다. 그리고 이 리프에서는 그 둘이 실제로 고쳐졌다. §12.1(c)의 `requireExactlyOneRelay` 만 같은 상태로 남았다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user