feat: add JPA production capability

This commit is contained in:
donghyeon-ka
2026-07-31 23:48:51 +09:00
parent b3add0162d
commit 7eb6af5d5f
141 changed files with 13094 additions and 319 deletions
@@ -0,0 +1,484 @@
# JPA/PostgreSQL Production Capability Implementation Plan
> 상태: Phase 0~3 기반과 Phase 4의 idempotency/outbox polling/inbox 후보 구현 및 전체
> local/real PostgreSQL 검증을 마쳤다. 검증을 통과한 항목은 `implemented-candidate`이며
> immutable 운영 evidence가 없는 항목을 R2로 승격하지 않는다. Phase 5~7은 외부 topology와
> policy prerequisite가 없어 `not-implemented`를 유지한다.
- 작성일: 2026-07-28
- 구현 branch: `codex/jpa-production-capability`
- worktree:
`/home/donghyeon/workspace/clean-architecture-backend-template-jpa`
- 시작 revision: `b3add0162df8d4a0a11e749e514901defe0a62a3`
- 설계 원본:
`/home/donghyeon/workspace/clean-architecture-backend-template/docs/superpowers/specs/2026-07-28-jpa-production-capability-design.md`
- 설계 SHA-256:
`c02eaef2a193a6ca66f4814087cc4d6bce723509aec251f40ea7b029046fd234`
설계 문서는 `main` worktree의 untracked 사용자 변경이므로 stage/commit/copy하지 않는다. 구현
중에는 위 절대 경로와 hash를 승인된 정본 snapshot으로 사용한다. 정본이 바뀌면 hash drift를
먼저 보고하고 해당 task의 설계를 재검토한다.
## 1. 목표와 완료 경계
목표는 JPA/PostgreSQL leaf의 각 capability를 독립적으로 구현·검증하는 것이다.
```text
truthful baseline
-> transaction/failure/deadline
-> entity/query discipline
-> migration/lifecycle/security
-> owner-safe reliability
-> optional replica
-> optional tenant/coordination
-> R3 rehearsal
```
한 phase의 unit test 통과를 전체 JPA R2로 확대하지 않는다. card가 R2가 되려면 설계 §31.3의
prerequisite, real PostgreSQL task, zero-skip sentinel과 immutable evidence manifest를 모두
충족해야 한다.
현재 구현 작업의 완료 경계는 다음과 같다.
1. 독립 worktree와 계획이 존재한다.
2. Phase 0의 SQLState, Duration, OSIV/DDL, machine-readable readiness baseline이
fail-closed한다.
3. named transaction policy, absolute deadline, PostgreSQL local timeout, phase-aware outcome,
bounded serialization/deadlock retry가 구현된다.
4. PostgreSQL 16 real test source set에서 lifecycle/security/migration/transaction/
aggregate/query가 무-skip로 실행된다.
5. owner-safe idempotency V2, immutable outbox storage V2, polling delivery V2, same-store
inbox가 독립 migration stream과 real PostgreSQL concurrency test를 가진다.
6. 외부 CDC, replica, tenant/RLS, R3는 토폴로지/evidence 없이 선택하거나 R2로 광고하지 않는다.
7. 전체 test/check와 Wiki capture 결과를 기록한다.
## 2. 공통 구현 규칙
- `src/config/architecture/modules.json`의 19개 leaf와 edge를 유지한다.
- `domain-core`에는 Spring/JPA/JDBC/PostgreSQL type을 추가하지 않는다.
- application contract에는 framework-neutral Java type만 둔다.
- transaction boundary는 application use case가 `TransactionPort`로 소유한다.
- controller/repository/mapper/configuration에 business policy를 두지 않는다.
- PostgreSQL 전용 code/import는 persistence-jpa leaf의 `.postgresql` package에 둔다.
- 동작 변경은 failing test를 먼저 확인한 뒤 최소 production code를 작성한다.
- applied Flyway V1/V3/V4/V5는 수정하지 않는다.
- agent는 stage/commit/amend/push하지 않는다.
- 다른 worktree의 dirty/untracked 변경을 복사하거나 되돌리지 않는다.
worktree 생성 직후 `src/gradlew.bat`는 CRLF blob과 checkout/attribute line-ending
normalization 차이 때문에 dirty로 표시된다. 비교 결과 의미 있는 텍스트 변경은 없지만 raw
worktree hash와 HEAD blob hash는 EOL 표현 때문에 다르다. targeted restore로도 사라지지 않는
known baseline drift이므로 구현 diff와 완료 판정에서 분리하고 stage하지 않는다.
## 3. Phase 0 — Truthful baseline과 contract freeze
### Task 0.1 SQLState mapping duplicate fail-fast
상태: 2026-07-28 구현 및 focused/architecture 검증 완료.
소유 leaf: `adapter-outbound-persistence-jpa`
파일:
- 수정:
`src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslatorTest.java`
- 수정:
`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslator.java`
- 필요 시 수정:
`src/adapter/outbound/persistence-jpa/README.md`
TDD:
1. 서로 다른 두 `SqlStateErrorMapping`이 같은 exact SQLState에 같은
`OperationalError`를 등록해도 constructor가 실패하는 test를 작성한다.
2. 같은 SQLState에 서로 다른 `OperationalError`를 등록하면 실패하는 test를 작성한다.
3. error message가 raw SQL, credential, endpoint 없이 duplicate SQLState와 mapping
contributor type을 식별하는지 검증한다.
4. focused test를 실행해 RED를 확인한다.
5. `putAll`을 explicit merge로 바꾸고 first/duplicate provenance를 보존한다.
6. null mapping/map/key/value와 `08*` pseudo-entry를 fail-fast할지 현재 SPI 계약에 맞춰
validation test를 추가한다. 이 세부 계약은 범위를 키우지 않고 constructor invariant로
한정한다.
7. focused test를 GREEN으로 만든다.
검증:
```bash
cd src
./gradlew :adapter:outbound:persistence-jpa:test \
--tests 'dev.caskeleton.adapter.outbound.persistence.failure.PersistenceExceptionTranslatorTest' \
--console=plain
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
```
### Task 0.2 Duration/OSIV/DDL production safety
상태: 2026-07-28 strict Duration와 prod DDL guard 구현 완료. OSIV guard는 기존 구현을
재사용하고 함께 회귀 검증했다.
소유 leaf:
- `app-bootstrap`: runtime settings/startup validator
- `adapter-outbound-persistence-jpa`: typed provider settings가 필요할 때만
선행 조사 파일:
- `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/HikariPoolConstraintValidator.java`
- `src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/HikariPoolConstraintValidatorTest.java`
- `src/app-bootstrap/src/main/resources/application.yml`
- `src/app-bootstrap/CLAUDE.md`
TDD:
1. `5s`, `PT5S`, millisecond number의 canonical/legacy 허용 matrix를 test로 고정한다.
2. invalid/unknown Duration을 skip하지 않고 startup failure로 만드는 RED를 확인한다.
3. `spring.jpa.open-in-view=true`를 거절한다.
4. production profile의 `ddl-auto=update|create|create-drop`을 거절한다.
5. local/sample compatibility를 별도 test로 유지한다.
검증:
```bash
cd src
./gradlew :app-bootstrap:test \
--tests 'dev.caskeleton.bootstrap.runtime.HikariPoolConstraintValidatorTest' \
--console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
./gradlew verifyEnvKeys --console=plain
```
### Task 0.3 Machine-readable readiness baseline
상태: 2026-07-28 구현 및 mutation/registry 검증 완료.
파일:
- 추가: `src/config/jpa/readiness-cards.yaml`
- 수정: `src/build.gradle`
- 추가: persistence-jpa readiness registry parser/validation tests
구현:
1. 설계 §31.3의 15 card와 7 owned migration stream을 exact key로 옮긴다.
2. unknown/missing card, duplicate task, cycle, missing prerequisite, duplicate
location/history를 fail-closed한다.
3. 현재 구현되지 않은 task/card는 `not-implemented`로 유지한다.
4. 존재하지 않는 target task를 통과 증거로 만들지 않는다.
5. registry structural verification task를 `check`의 architecture policy chain에 연결하되
real PostgreSQL readiness를 거짓으로 통과시키지 않는다.
## 4. Phase 1 — Transaction/failure/deadline foundation
상태: 2026-07-28 application contract, Spring executor, local timeout, phase-aware outcome,
bounded retry/backoff 후보 구현 완료. commit fault injection과 immutable R2 manifest는 남아 있다.
### Task 1.1 Additive application transaction contract
소유 leaf: `application-core`
예상 파일:
- 추가: `transaction/TransactionPolicy.java`
- 추가: `transaction/CallBudget.java`
- 추가: `transaction/OperationId.java`
- 추가: `transaction/TransactionOutcome.java`
- 추가: `transaction/PolicyTransactionPort.java`
- 수정: `transaction/TransactionPort.java`
- tests: 같은 package의 pure unit tests
계약:
- 기존 `inWrite`, `inRead`, `inNew` source compatibility 유지
- named write policy는 stable operation ID 요구
- legacy facade는 non-replayable/uncorrelated policy로 격리
- absolute deadline과 finite timeout intersection
- core에는 Spring `TransactionDefinition`/`DurationStyle`을 노출하지 않음
### Task 1.2 Spring policy executor와 propagation ownership
소유 leaf: `adapter-outbound-persistence-jpa`
예상 파일:
- 수정: `transaction/SpringTransactionPort.java`
- 추가: `transaction/SpringPolicyTransactionPort.java`
- 추가: transaction phase/outcome collaborator
- tests: unit + real PostgreSQL task
검증:
- REQUIRED physical owner와 participant 구분
- REQUIRES_NEW depth/capacity admission
- read/write route mismatch fail-fast
- commit callback ordering
- locale 없는 `toLowerCase()` 제거
### Task 1.3 Deadline와 PostgreSQL local timeout
- Hikari acquisition은 fixed pool timeout으로 유지
- action 시작 전 remaining budget pre-gate
- first statement 전 `SET LOCAL statement_timeout`, `lock_timeout`
- transaction/statement/lock rounding boundary test
- pool wait 뒤 total budget overshoot negative test
### Task 1.4 Phase-aware failure/retry
- operation/query executor를 모든 production persistence path에 연결
- constraint name allowlist
- begin/action/flush/commit/after-completion phase 분류
- `COMMIT_INDETERMINATE`는 blind retry 금지
- pre-commit + replay-safe + budget 조건에서만 whole-transaction retry
## 5. Phase 2 — Entity/query discipline
상태: production template에 임의 business aggregate를 추가하지 않고 sample의 기존 entity/
mapper/query discipline을 실제 PostgreSQL aggregate CAS와 query-plan fixture로 검증했다.
### Task 2.1 Aggregate persistence baseline
- domain aggregate와 persistence entity 분리
- mapper round-trip과 invariant failure test
- optimistic version/expected-version conflict
- audit creation carry-forward와 bulk DML guard
- bounded persistence-context batch
### Task 2.2 Purpose-built query model
- application projection `*QueryPort`
- allowlisted query ID
- max page/IN bound와 signed/versioned keyset cursor
- N+1 statement budget
- native/JDBC query는 `.postgresql` package
- representative `EXPLAIN` invariant task
## 6. Phase 3 — Migration/lifecycle/security
상태: legacy V1/V3/V4/V5/V6 adoption, independent core stream, PostgreSQL 16 lifecycle/security/
migration/transaction/aggregate/query candidate task와 content-addressed manifest producer 구현
완료. TLS verify-full/role/redaction, pool lifecycle, fresh/interrupted/rolling migration,
transaction concurrency/fault dimension을 실제 PostgreSQL과 transport test로 채웠다. clean CI
provenance와 외부 restore rehearsal이 없으면 R2/R3 aggregation은 계속 fail-closed한다.
### Task 3.1 Legacy adoption과 independent streams
- legacy V1/V3/V4/V5 checksum/object fingerprint
- `capability_schema_registry`
- explicit target stream version-0 adoption command
- core/optional history table ownership
- fresh/LEGACY_ADOPTED/interrupted paths
- old/target dual authority rejection
### Task 3.2 Real PostgreSQL qualification source set
canonical tasks:
```text
postgresqlLifecycleIntegrationTest
postgresqlSecurityBaselineIntegrationTest
postgresqlMigrationIntegrationTest
postgresqlTransactionIntegrationTest
postgresqlAggregateIntegrationTest
postgresqlQueryIntegrationTest
verifyJpaPrimaryFoundationEvidence
```
Docker/Testcontainers가 없으면 R2 lane은 skip이 아니라 fail이다. local optional task와 evidence
producer를 분리한다.
구현된 evidence task:
```text
verifyJpaEvidenceHarnessContract
generateJpaEvidenceManifests
verifyJpaCandidateEvidence
verifyJpaPrimaryFoundationEvidence
```
candidate task는 11개 active card의 exact JUnit selector, zero-skip count, source/이미지/의존성
version과 prerequisite manifest ID를 SHA-256 filename manifest로 남긴다. primary task는
`-PjpaEvidenceProfile=r2`, clean revision, CI job/artifact metadata, 모든 base dimension과
prerequisite R2를 추가로 요구한다.
### Task 3.3 Lifecycle/security
- migration/runtime role 분리
- trusted schema/search_path, `PUBLIC CREATE`/`TEMP` revoke
- TLS verify-full profile
- startup/readiness/shutdown/quiesce
- bounded/redacted metric/trace/log
- restore/forward-recovery runbook
## 7. Phase 4 — Owner-safe same-store reliability
상태: idempotency V2, outbox storage V2, polling delivery V2, inbox V1은 각각
`implemented-candidate`. 네 stream 모두 fresh-disabled/first-enable/disable/re-enable/
interrupted-recovery의 non-destructive lifecycle을 실제 PostgreSQL에서 검증한다. CDC는 external
messaging prerequisite가 없어 `not-implemented`다.
독립 implementation slice:
1. `jpa-idempotency-owner-safe-v2`
2. `jpa-outbox-storage-v2`
3. `jpa-outbox-polling-delivery-v2` 또는 `jpa-outbox-cdc-retention-v1`
4. `jpa-inbox-same-store-v1`
각 slice는 자기 migration stream/task/manifest를 가진다.
outbox storage 구현은:
- V3 `outbox_event`를 수정하지 않음
- `outbox_publication_control_v2`
- `outbox_publication_cutover_v2`
- `outbox_event_identity_v2`
- `outbox_event_log_v2`
- polling 선택 시에만 `outbox_delivery_v2`
- fresh/legacy genesis sentinel
- legacy mutation trigger/ACL fence
- paused old writer와 cutover barrier test
를 포함한다.
## 8. Phase 57
상태: 선택된 replica topology, tenant mode/RLS policy, target-like backup/failover environment가
없으므로 registry에서 `not-implemented`를 유지한다. 로컬 단일 PostgreSQL 테스트를 해당
운영 보장의 대체 evidence로 사용하지 않는다.
### Phase 5 — Primary/replica
- 별도 pool/route context
- explicit `ReadConsistency`
- endpoint-bound lag evidence
- strong/RYW primary default
- failover authority reconciliation
### Phase 6 — Tenant/RLS와 JDBC coordination
- tenant-prefixed unique/FK/query
- missing context fail-closed
- optional FORCE RLS
- runtime role bypass negative test
- JDBC coordination은 `EFFICIENCY_ONLY`
### Phase 7 — R3
- target-like load/capacity
- failover, rolling migration, certificate rotation
- backup/PITR restore
- outbox/idempotency/inbox reconciliation
- measured RPO/RTO와 operator game day
## 9. 공통 verification ladder
변경 leaf focused test부터 실행한다.
```bash
cd src
./gradlew :application-core:test --console=plain
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew verifyPublicPathSnapshot --console=plain
./gradlew verifyEnvKeys --console=plain
```
전체 `test`/`check`와 real PostgreSQL task는 해당 phase가 경계를 실제로 변경하거나 required
task를 추가한 시점에 실행한다. 실행하지 못한 명령은 이유와 남은 위험을 branch-note와 최종
응답에 기록한다.
## 10. Wiki capture
각 의미 있는 slice가 끝날 때 실제 vault의 branch-note:
```text
raw/branch-notes/codex-jpa-production-capability.md
```
에 다음을 누적한다.
- design hash와 plan path
- 변경 파일/decision ID
- RED/GREEN/architecture command와 결과
- 실패/차단/known baseline drift
- evidence grade와 아직 R2가 아닌 이유
- 실제 파생 raw interview/blog/error 판단
canonical 문서는 별도 요청 전 생성하지 않는다.
## 11. 최종 실행 결과
2026-07-28:
- `./gradlew :sample-portfolio:test --console=plain`
→ 성공, 176 tests.
- `./gradlew test --console=plain`
→ 성공, 1m 59s.
- PostgreSQL readiness task 10개
(`lifecycle`, `security`, `migration`, `transaction`, `aggregate`, `query`, `idempotency`,
`outbox-storage`, `outbox-polling`, `inbox`)
→ 성공, 49s. XML 합계 23 tests, `skipped=0`, `failures=0`, `errors=0`.
- `./gradlew check --console=plain`
→ 성공, 2m 9s, 209 actionable tasks. 같은 실행에서 root architecture policy,
Checkstyle, Spotless, SpotBugs와 custom PostgreSQL source set 검증을 통과했다.
- `./gradlew verifyCleanArchitectureDependencies verifyPublicPathSnapshot verifyEnvKeys
verifyJpaReadinessRegistry --console=plain`
→ 성공. 19개 leaf edge, 1개 public path, 113 env keys, exact 15 cards/7 streams 검증.
- `git diff --check`
→ 진단 없음.
- `./gradlew :adapter:outbound:persistence-jpa:verifyJpaPrimaryFoundationEvidence --console=plain`
→ 기존 unconditional sentinel을 제거했다. content-addressed candidate manifest를 검증한 뒤
candidate profile과 observability, TLS/role/redaction, fresh/interrupted/rolling migration,
transaction concurrency 누락을 card별 blocker로 보고 R2를 차단한다.
- `./gradlew :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence --console=plain`
→ 성공, active card 11개 manifest 생성. PostgreSQL 23 tests와 primary base aggregation
7 tests 모두 zero-skip이고 content hash/prerequisite link를 검증했다.
- `bash .github/scripts/verify-gate-matrix.sh`
→ 성공, 21 gates verified. PR candidate evidence job과 conditional R2 workflow가 registry에
반영됐다.
- CI metadata를 주입한
`verifyJpaPrimaryFoundationEvidence -PjpaEvidenceProfile=r2`
→ PostgreSQL 23 tests와 r2-profile manifest 11개 생성 뒤 의도된 실패, 1m 21s.
`worktree-is-dirty`, observability, TLS/roles/redaction, migration
fresh/interrupted/rolling, transaction concurrency를 실제 blocker로 보고했다.
- `./gradlew test --console=plain`
→ 성공, 15s, 78 tasks up-to-date. 직전 evidence lane에서 persistence/app test는 강제
재실행했다.
- `./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --console=plain`
→ 성공, 9s, 230 actionable tasks(37 executed, 193 up-to-date).
전체 test에서 발견한 sample Flyway 회귀는 independent `V1` stream을 broad
`classpath:db/migration`으로 합친 문제와 production/sample `V6` 충돌이었다. sample slice를
legacy PostgreSQL location으로 한정하고 disposable poster migration을 `V7`로 이동했다. 세부
재현·해결 기록은 Wiki
`raw/errors/flyway-independent-stream-broad-root-collision-2026-07-28.md`에 남겼다.
2026-07-29 completion pass:
- primary foundation의 pool lifecycle/observability, TLS verify-full/role/redaction,
fresh/interrupted/rolling migration, transaction concurrency/fault evidence를 추가했다.
- idempotency/outbox storage/outbox polling/inbox 네 독립 stream에
fresh-disabled/first-enable/disable/re-enable/interrupted-recovery 실제 PostgreSQL
lifecycle test를 추가했다.
- `./gradlew :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence --console=plain`
→ **BUILD SUCCESSFUL in 1m 55s**. 11개 manifest 모두 `missing=none`, zero-skip.
PostgreSQL producer 38 tests와 web redaction support 2 tests가 실행됐으며 primary
aggregation은 20 tests다.
- `./gradlew test --console=plain`
→ **BUILD SUCCESSFUL in 55s**, 78 actionable tasks.
- `./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --console=plain`
→ 포맷과 test fixture SQL construction을 수정한 뒤 **BUILD SUCCESSFUL in 12s**,
231 actionable tasks. 19 leaf architecture, Checkstyle, Spotless, SpotBugs, dependency lock,
env/readiness/public-path gate를 통과했다.
- CI 메타데이터 형식만 주입한
`verifyJpaPrimaryFoundationEvidence -PjpaEvidenceProfile=r2`
→ **의도된 BUILD FAILED in 2m 6s**. missing evidence는 없고 root blocker는
`worktree-is-dirty`; 다른 blocker는 prerequisite R2 전파뿐이다.
- `bash .github/scripts/verify-gate-matrix.sh`
→ **OK**, 21 gates/21 verified.
- `git diff --check`
→ 진단 없음.
현재 환경에서 선택된 Phase 0~4 후보의 로컬 구현·검증은 완료됐다. R2 승격은 사람의
commit/push, clean revision에서의 retained CI artifact가 필요하고, R3는 target-like
backup/failover/load/operator rehearsal 환경이 필요하다.