Files
llm-wiki/raw/branch-notes/feature-persistence-failure-baseline.md
T

359 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-persistence-failure-baseline
source_type: branch-note
status: raw
branch: feature-persistence-failure-baseline
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]
tags: [branch, ca-skeleton, persistence, jpa, database]
created: 2026-05-21
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-006
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-006
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TESTCONTAINERS-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
contract_packet_sha256: de3aae90785a1d43f67d6b179b3372223b788348472b4a1667f38ec217f73eb0
---
# branch: feature-persistence-failure-baseline
> Layer: `raw/branch-notes/` — DB/JPA 실패 분류와 persistence adapter 실패 계약을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: persistence failure mapping과 integration test가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1` | database는 PostgreSQL 16 단일 stack이다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-TESTCONTAINERS-001@1` | Testcontainers는 persistence/outbound integration test에 사용하고 unit·architecture·contract test에서는 금지한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
DB/JPA 실패를 단순히 `DataIntegrityViolationException -> 409`로 끝내면 운영 기준에 부족합니다. connection unavailable, lock, timeout, integrity, query/system failure를 분리하고 presentation까지 JPA 예외가 새지 않게 해야 합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- Spring `DataAccessException` 계열 분류.
- JPA exception mapping.
- DB unavailable/lock/query timeout/data integrity 분류.
- SQL/parameter 로그 금지.
- datasource/pool/timeout/connection exhaustion log field.
- Hikari metric 노출 기준.
- OSIV off 유지 검증.
### 제외 범위
- 특정 DB vendor 최적화.
- migration strategy.
- business transaction 설계.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| [[raw/official-docs/persistence-spring-dataaccessexception-hierarchy]] | SQLState 9-row matrix가 Spring DAO hierarchy(`TransientDataAccessException` / `NonTransientDataAccessExcepti... |
| [[raw/official-docs/persistence-osiv-antipattern-hibernate-vladmihalcea]] | OSIV off 기본값의 외부 근거 |
| [[raw/official-docs/persistence-hikaricp-pool-sizing-wiki]] | Hikari alert threshold (pool wait p99 > 100ms 5분 / exhaustion > 1분 |
| [[raw/official-docs/persistence-r2dbc-reactive-spring]] | R2DBC reactive 대안 |
## 외부 근거 (Group G-C — Persistence failure)
ca-tmpl 결정의 backbone과 대안 비교 자료. 각 raw는 별도 파일에서 trade-off를 정리.
- 채택 결정의 공식 근거:
- [[raw/official-docs/persistence-spring-dataaccessexception-hierarchy]] — SQLState 9-row matrix가 Spring DAO hierarchy(`TransientDataAccessException` / `NonTransientDataAccessException` / `RecoverableDataAccessException`)와 정합인 근거.
- [[raw/official-docs/persistence-osiv-antipattern-hibernate-vladmihalcea]] — OSIV off 기본값의 외부 근거. Hibernate 권위 + Spring Boot WARN 메시지.
- [[raw/official-docs/persistence-hikaricp-pool-sizing-wiki]] — Hikari alert threshold (pool wait p99 > 100ms 5분 / exhaustion > 1분) 의 metric 출처.
- 대안 비교:
- [[raw/official-docs/persistence-r2dbc-reactive-spring]] — R2DBC reactive 대안. JPA blocking baseline을 택한 trade-off 반대편.
검색 키워드 기록: `Spring DataAccessException hierarchy`, `OSIV anti-pattern Vlad Mihalcea`, `HikariCP about pool sizing`, `R2DBC vs JDBC reactive`.
## TODO
> TODO drained — 결정은 아래 표/결정 사항 참조.
## Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
| --------------------------- | ----------- | --------------------------------------------------- |
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
## 진행 중 메모
- persistence failure는 infrastructure에서 operational error로 변환되어야 합니다.
## 결정 사항 (decisions)
- 2026-05-21: JPA/Spring exception은 presentation까지 노출하지 않음.
- 2026-05-22: disaster recovery는 backup 존재가 아니라 restore drill 통과를 기준으로 판단. 기본은 분기 1회 staging/local restore smoke.
- 2026-05-22: read replica는 기본 미사용. 활성화 시 max replica lag threshold와 stale-read 허용 endpoint를 명시.
- 2026-05-22: OSIV는 off가 기본이며 lazy loading으로 presentation에서 DB 접근이 발생하면 계약 위반.
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. Decision ID 는 본 branch-note 안에서 안정적으로 유지. Claim ID 는 cited raw 의 `## Claims Extracted` 표에서 verbatim 확인된 것만 사용. 회사 기술블로그는 `company-case-study` 로만 라벨 (best practice 단정 금지).
> ⚠️ **CATEGORY_DRIFT (정합 권고)**: 아래 D4 의 `PERSISTENCE/DB_UNAVAILABLE` · D5 의 `DATA_INTEGRITY_VIOLATION` 표기는 `Category.java` 10-value enum / `error-codes.yaml` 의 실제 값과 어긋난다. 권위 SSOT 값은 §SQLState → Error Code Matrix 와 §Audit & Findings 참조 — `PERSISTENCE` 카테고리는 enum 에 존재하지 않음. 사용자 결정 영역이라 자동 rewrite 보류, 정합 권고만 남긴다.
| Decision ID | Decision (요약) | Supporting Claims | Evidence Strength | Open Risk |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| D1 | JPA/Spring exception 은 presentation 까지 노출하지 않음 (3-way classifier: TransientDataAccessException / NonTransientDataAccessException / RecoverableDataAccessException 위에 SQLState matrix) | `raw/official-docs/persistence-spring-dataaccessexception-hierarchy.md#SDA-EX-C1`, `#SDA-EX-C2`, `#SDA-EX-C3`, `#SDA-EX-C5` | `official-vendor-doc + official-reference` | SQLState ↔ Spring exception class 의 vendor 매핑 (8\*, 23\*, 40001, 40P01, 23505) 은 `#SDA-EX-C6`/`#SDA-EX-C7` 이 `needs-confirmation` — ca-tmpl 의 9-row matrix 는 `sql-error-codes.xml` 직접 검증 전까지 vendor 정당성 미확정 |
| D2 | OSIV 는 off 가 기본 — lazy loading 으로 presentation 에서 DB 접근 발생 시 계약 위반 | `raw/official-docs/persistence-osiv-antipattern-hibernate-vladmihalcea.md#OSIV-AP-C1`, `#OSIV-AP-C2`, `#OSIV-AP-C3`, `#OSIV-AP-C4` | `official-vendor-doc` (Spring Boot WARN, C4) + `engineering-blog` (Vlad Mihalcea, C1~C3 — Hibernate developer advocate 의 권위 있는 분석이지만 Hibernate User Guide 자체의 anti-pattern 선언 verbatim 미확보) | Hibernate ORM User Guide 자체에서 OSIV deprecation 또는 anti-pattern 선언 verbatim 확보 필요 (현재 vladmihalcea.com WebFetch 차단으로 재검증 보류) |
| D3 | Hikari pool wait p99 > 100ms 5분 → P2, pool exhaustion (active = max) > 1분 → P1 | `raw/official-docs/persistence-hikaricp-pool-sizing-wiki.md#HIKARI-POOL-C1`, `#HIKARI-POOL-C5` (MBean attribute: `ThreadsAwaitingConnection`, `ActiveConnections`, `TotalConnections`) | `official-vendor-doc` (HIKARI-POOL-C1/C5 — pool axiom + MBean attribute 존재) | 구체적 threshold 수치 (100ms / 5분 / 1분) 는 HikariCP 가 정의하지 않은 운영자 SLO — UNSUPPORTED_THRESHOLD (HikariCP 공식 권고가 아님, ca-tmpl 내부 결정). Micrometer metric 이름 (`hikaricp.connections.acquire`, `.pending`) 은 `#HIKARI-POOL-C6` 이 `needs-confirmation` — Micrometer / Spring Boot Actuator 측 별도 raw 필요 (registry 는 `.acquire`/`.usage`/`.active` 사용 — §Audit METRIC_NAME_DRIFT) |
| D4 | DB unavailable →`PERSISTENCE/DB_UNAVAILABLE`, HTTP 503, retryable true | `raw/official-docs/persistence-spring-dataaccessexception-hierarchy.md#SDA-EX-C3` (TransientDataAccessException 의 top-level 3-way 분류 존재) | `official-reference` (3-way 분류 존재만 보장) | SQLState 08\* → `DataAccessResourceFailureException` 의 직접 매핑은 `#SDA-EX-C7` `needs-confirmation` — vendor 별 `sql-error-codes.xml` 검증 전까지 ca-tmpl `DB_UNAVAILABLE` 매핑 정당성 미확정. ⚠️ `PERSISTENCE` 카테고리는 enum 부재 — registry 실제값 `TRANSIENT_DEPENDENCY` (§Audit CATEGORY_DRIFT) |
| D5 | integrity violation →`DATA_INTEGRITY_VIOLATION`, HTTP 409, retryable false; 23505 unique violation 은 별도 code (`DB_UNIQUE_VIOLATION`) | `raw/official-docs/persistence-spring-dataaccessexception-hierarchy.md#SDA-EX-C3` (NonTransientDataAccessException top-level 존재) | `official-reference` (3-way 분류만 보장) | 23\* → `DataIntegrityViolationException`, 23505 → `DuplicateKeyException` 의 위계는 `#SDA-EX-C7` `needs-confirmation` — `DuplicateKeyException` 의 직접 부모가 `DataIntegrityViolationException` 임은 javadoc 별도 확인 필요. ⚠️ `DATA_INTEGRITY_VIOLATION` 코드는 registry 부재 — 실제값 `DB_NULL_VIOLATION`/`DB_FK_VIOLATION`/`DB_CHECK_VIOLATION`(category `DATA_INTEGRITY`) + 23505→`DB_UNIQUE_VIOLATION`(category `CONFLICT`) (§Audit CATEGORY_DRIFT) |
| D6 | optimistic lock conflict 409 / deadlock·serialization 은 retryable by policy (40001, 40P01) | `raw/official-docs/persistence-spring-dataaccessexception-hierarchy.md#SDA-EX-C3` (TransientDataAccessException top-level 존재), `#SDA-EX-C5` (optimistic locking failure 예시 명시) | `official-reference` | 40001 →`ConcurrencyFailureException`, 40P01 → 같은 계열의 매핑은 `#SDA-EX-C7` `needs-confirmation` — vendor `sql-error-codes.xml` 확인 필요 |
| D7 | disaster recovery 는 backup 존재가 아니라 restore drill 통과를 기준. 기본 분기 1회 staging/local restore smoke | (UNSUPPORTED_DECISION — cited raw 4종 중 어디에도 restore drill 권고 verbatim claim 없음. AWS / Postgres 운영 가이드 별도 raw 필요) | `internal-policy` | restore drill 주기 (분기 1회) 는 ca-tmpl 내부 운영 정책 — 외부 권위 근거 미수집. ⚠️ 본 branch In-scope 밖 — §Audit OUT_OF_BRANCH_SCOPE 후보 |
| D8 | read replica 기본 미사용. 활성화 시 max replica lag threshold + stale-read 허용 endpoint 명시 | (UNSUPPORTED_DECISION — cited raw 4종에 replica lag 관련 verbatim claim 없음.`persistence-r2dbc-reactive-spring` 도 reactive 대안 자료이지 replica lag 자료 아님) | `internal-policy` | Postgres streaming replication 또는 vendor 별 replica lag 권고 raw 별도 수집 필요. ⚠️ 본 branch In-scope 밖 — §Audit OUT_OF_BRANCH_SCOPE 후보 |
## Decisionized Work Items
| item | Decision | Allowed | Forbidden | Required test |
| ------------------- | ------------------------------------------------------------- | ------------------------------------- | ------------------------------- | ---------------------- |
| DB unavailable | `PERSISTENCE/DB_UNAVAILABLE`, HTTP 503, retryable true | degraded read-only mode with runbook | generic 500 | DB unavailable mapping |
| integrity violation | `DATA_INTEGRITY_VIOLATION`, HTTP 409, retryable false | domain pre-check can produce conflict | raw constraint name in response | integrity mapping |
| lock/deadlock | optimistic conflict 409, deadlock/timeout retryable by policy | explicit pessimistic lock use case | all lock errors same code | lock mapping |
| restore drill | quarterly smoke default | monthly for critical service | backup with no restore evidence | restore checklist |
| read replica | primary read default | replica with max lag threshold | silent stale reads | replica lag contract |
> ⚠️ 위 `PERSISTENCE/DB_UNAVAILABLE` · `DATA_INTEGRITY_VIOLATION` 표기도 §Audit CATEGORY_DRIFT 정합 권고 대상 — registry 실제값은 §SQLState → Error Code Matrix.
## SQLState → Error Code Matrix
> ✅ 본 표가 registry(`ca-tmpl/docs/registries/error-codes.yaml` L230354, owner_branch=feature-persistence-failure-baseline)와 1:1 정합인 **권위 매핑**. 카테고리는 모두 `Category.java` 10-value enum 의 실존 값.
| SQLState | Vendor | category | error.code | retryable |
| -------- | -------------- | -------------------- | ------------------------ | ------------------------ |
| 08* | all | TRANSIENT_DEPENDENCY | DB_UNAVAILABLE | true |
| 40001 | Postgres/MySQL | CONFLICT | DB_SERIALIZATION_FAILURE | true |
| 40P01 | Postgres | CONFLICT | DB_DEADLOCK | true (backoff) |
| 23502 | Postgres | DATA_INTEGRITY | DB_NULL_VIOLATION | false |
| 23503 | Postgres | DATA_INTEGRITY | DB_FK_VIOLATION | false |
| 23505 | Postgres | CONFLICT | DB_UNIQUE_VIOLATION | false (business mapping) |
| 23514 | Postgres | DATA_INTEGRITY | DB_CHECK_VIOLATION | false |
| 25P03 | Postgres | TRANSIENT_DEPENDENCY | DB_IDLE_IN_TX_TIMEOUT | true |
| 57014 | Postgres | TRANSIENT_DEPENDENCY | DB_QUERY_CANCELED | false |
### Hikari Alert Threshold
- pool wait p99 > 100ms 5분 지속 → P2
- pool exhaustion (active = max) > 1분 → P1
### Mapping Ownership
- constraint name → business error 변환 owner: persistence adapter layer.
- mapping table은 application port 인접 위치에 둔다.
## 구현 가이드
> *결정 (Decisions)* 이 "*무엇* 을 할 것인가" 라면, 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세. ca-tmpl 의 실제 클래스/registry 를 anchor 로 쓰되, 코드로 미확인 항목은 `planned` 로 표기. 계약값 SSOT: `Category.java` (enum) + `error-codes.yaml`/`metrics.yaml`/`env-keys.yaml` (registry).
### 1. SQLState 분류 어댑터 (adapter-persistence)
> **Trace**: D1 + `#SDA-EX-C1`/`C2`/`C3`/`C5`; D4·D5·D6; §SQLState → Error Code Matrix 9-row. 카테고리 SSOT = `shared-contract/src/main/java/dev/caskeleton/shared/error/Category.java` (10-value), code/category/http/retryable SSOT = `error-codes.yaml` L230354 (owner_branch=feature-persistence-failure-baseline).
>
> - **UNSUPPORTED_IMPL_DECISION**: 변환기 클래스 명명·위치(예: `PersistenceExceptionTranslator`)와 Spring `SQLErrorCodeSQLExceptionTranslator` 재사용 vs 커스텀 SQLState 매핑 중 택일은 cited raw 가 권고하지 않음 — §Claims To Verify 1번(`sql-error-codes.xml` 대조) 해소 후 확정. trade-off: 재사용=vendor xml 의존/유지보수 적음, 커스텀=9-row 정확 제어/구현 비용.
| 구현 항목 | 위치 (ca-tmpl) | 상태 | 근거 |
| --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------ |
| Category enum (10-value,`PERSISTENCE` 없음) | `shared-contract/.../error/Category.java` | `actually-implemented` | grep 확인 |
| DB_* error code 9종 (code/category/http/retryable/runbook) | `docs/registries/error-codes.yaml` L230354 | `actually-implemented` (registry, owner=this) | registry |
| code→category 계약 테스트 (DB_NULL_VIOLATION→DATA_INTEGRITY, DB_UNIQUE_VIOLATION→CONFLICT, DB_SERIALIZATION_FAILURE/DB_DEADLOCK→CONFLICT) | `app-bootstrap/.../contract/BusinessRuleValidationContractTest.java` L100105 | `actually-implemented` (contract test) | 코드 |
| SQLState→Spring exception→error.code 런타임 변환 어댑터 | `adapter-persistence/.../failure/PersistenceExceptionTranslator.java` (custom SQLState 매핑, 9-row + 08* prefix, fallback=empty) | `actually-implemented` (2026-06-09, Phase C2) | 코드 + `PersistenceExceptionTranslatorTest` (16 case) |
| DB_* 코드 9종 enum 표현 (carrier 반환 타입) | `shared-contract/.../error/OperationalError.java` (DB_* 9종 추가) + `PersistenceFailureException` carrier | `actually-implemented` (2026-06-09) | 코드 + `ErrorCodeRegistryMappingTest` 가 enum↔registry http_status 정합 검증 |
| presentation 매핑 (HTTP status·response envelope) | `adapter-web/.../error/GlobalExceptionHandler#handlePersistenceFailure` (carrier→envelope, category-derived safe message) | `actually-implemented` (2026-06-09) | `GlobalExceptionHandlerTest` (3 case, leak-free) |
### 2. OSIV off 강제 (startup)
> **Trace**: D2 + `#OSIV-AP-C1`~`C4`.
>
> - **UNSUPPORTED_IMPL_DECISION**: `spring.jpa.open-in-view=false` 를 startup *fail-fast assertion* 으로 추가 강제할지 vs env 기본값 + Spring Boot WARN 에 의존할지 — `#OSIV-AP-C4` 는 WARN 만 보장(자동 disable 아님). trade-off: assertion=명시적 계약 위반 차단, default-only=설정 override 시 silent OSIV on.
| 구현 항목 | 위치 (ca-tmpl) | 상태 | 근거 |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------- |
| `APP_DATASOURCE_OPEN_IN_VIEW` env key (default `false`) | `env-keys.yaml` L445 (owner=feature-env-driven-runtime-configuration) → `application.yml` L43 `open-in-view: ${...}` | `actually-implemented` (config-level) | grep 확인 |
| OSIV off 강제 startup fail-fast assertion | `app-bootstrap/.../runtime/OpenInViewSafetyValidator.java` (SmartInitializingSingleton, `spring.jpa.open-in-view=true` → boot fail) + `RuntimeSafetyConfig` bean | `actually-implemented` (2026-06-09, Phase C2) | 코드 + `OpenInViewSafetyValidatorTest` (3 case) |
### 3. Hikari pool 관측 (metrics)
> **Trace**: D3 + `#HIKARI-POOL-C1`/`C5`; `metrics.yaml` (owner 공유 `feature-metrics-alerting-contract`).
| metric | registry 상태 | 비고 |
| -------------------------------- | ---------------------------------------------------- | ----------------------------------- |
| `hikaricp.connections.acquire` | registered (`metrics.yaml` L158, p99>100ms 5m→P2) | `actually-implemented` (registry) |
| `hikaricp.connections.usage` | registered (L178) | |
| `hikaricp.connections.active` | registered (L194, exhaustion 1m→P1) | |
- **UNSUPPORTED_THRESHOLD**: 100ms/5분/1분 수치는 HikariCP 비권고 내부 SLO (D3 Open Risk).
- **METRIC_NAME_DRIFT**: D3 이 `hikaricp.connections.pending` 인용했으나 registry 는 `.usage`/`.active` 사용 → §Audit.
### 4. datasource/pool env 계약
> **Trace**: In-scope "datasource/pool/timeout/connection exhaustion log field" + `env-keys.yaml` (owner 공유 `feature-env-driven-runtime-configuration`).
`APP_DATASOURCE_URL`/`_USERNAME`/`_PASSWORD`/`_POOL_MAX_SIZE`/`_POOL_MIN_IDLE`/`_CONNECTION_TIMEOUT` registered (`env-keys.yaml` L307373) → `actually-implemented` (registry). SQL/parameter 로그 금지(In-scope)는 `APP_DATASOURCE_SHOW_SQL` default `false` (`env-keys.yaml` L417 → `application.yml` L41 `show-sql: ${...}`, `_FORMAT_SQL` L431 동반) 로 config-level `actually-implemented`; 위반 시 실패하는 contract test 는 `planned` (§테스트 계약). 위 env 키 owner 는 모두 [[raw/branch-notes/feature-env-driven-runtime-configuration]] (delegated).
## 엣지·실패·의존
> R4(깊이 게이트) 캡처용. 정상 경로 외 실패/엣지/다른 계약 의존을 미리 열거.
- **실패·엣지 경로**:
- **9-row 밖 미지의 SQLState**: fallback 은 `INTERNAL` category + generic 메시지, raw exception/SQL 비노출. (planned — translator 부재)
- **pool acquire timeout**: connection 미확보 → `DB_UNAVAILABLE`(503, retryable) 분류 + `hikaricp.connections.acquire{outcome=timeout}` 증가. pool exhaustion(active=max) 1분 → P1.
- **OSIV off + lazy access**: presentation 에서 `LazyInitializationException` 발생 시 D2 계약 위반. fetch graph(`@EntityGraph`/`JOIN FETCH`/DTO projection) 누락 → N+1 (§Claims To Verify 5번).
- **23505 unique**: persistence adapter 가 business conflict(`CONFLICT/DB_UNIQUE_VIOLATION`)로 변환, constraint name 응답 비노출.
- **transient vs integrity 혼동**: deadlock/serialization(retryable CONFLICT)과 integrity(non-retryable DATA_INTEGRITY)가 같은 code 로 뭉개지면 실패(§테스트 계약).
- **다른 계약 의존**:
- [[raw/branch-notes/feature-operational-error-observability-foundation]] 의 `Category.java` 10-value enum(D10) — 본 branch 9 코드가 이 enum 으로 분류. enum 변경 시 본 매핑 영향.
- [[raw/branch-notes/feature-metrics-alerting-contract]] — hikari metric 명/threshold 공동 소유. metric 명 변경 시 D3 영향.
- [[raw/branch-notes/feature-env-driven-runtime-configuration]] — `APP_DATASOURCE_*` env 키 공동 소유.
- adapter-web `GlobalExceptionHandler` — presentation 매핑 소유(persistence 가 category-correct code 제공, web 이 HTTP envelope 변환).
## 테스트 계약
- JPA exception class name이 API response에 나오면 실패.
- SQL/parameter가 log에 남으면 실패.
- DB unavailable은 retryable dependency failure로 분류되어야 함.
- integrity violation과 transient lock failure가 같은 code로 뭉개지면 실패.
- read replica lag threshold 없이 replica read가 활성화되면 실패.
## 검증해야 할 주장
> 공식 문서 인용이 결정의 근거이지만, ca-tmpl 자체 구현에서의 동작은 별도 검증이 필요한 주장.
| Claim | Why uncertain | How to verify | Status |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| ca-tmpl 9-row SQLState matrix 의 vendor 매핑 (08\*→`DataAccessResourceFailureException`, 40001→`ConcurrencyFailureException`, 23\*→`DataIntegrityViolationException`, 23505→`DuplicateKeyException`) 이 Spring `sql-error-codes.xml` 의 PostgreSQL/MySQL section 과 일치 | `#SDA-EX-C7` `needs-confirmation` — Spring Framework Reference dao.html landing 에 verbatim 미등장 | `SQLErrorCodeSQLExceptionTranslator` Javadoc + `sql-error-codes.xml` source 를 별도 raw 로 수집 후 1:1 대조 | `needs-confirmation` |
| `spring.jpa.open-in-view=false` 가 ca-tmpl startup assertion 으로 강제됨 | `OSIV-AP-C4` 는 Spring Boot 가 WARN 만 출력함을 보장하며 자동 disable 은 안 함 | `application.yml` + `JpaBaseConfiguration` startup assertion 코드 검증, integration test 에서 property 값 `false` 단언 | `planned` |
| Vlad Mihalcea 의 OSIV anti-pattern 권위 있는 verbatim 재확인 + Hibernate ORM User Guide 의 OSIV 관련 직접 인용 확보 | `OSIV-AP-C1`~`C3` 의 strength 가 `engineering-blog` 으로 제한, Hibernate 공식 verbatim 미확보 | vladmihalcea.com 재시도 (다음 세션) + hibernate.org User Guide §Transactions WebFetch 재시도 | `needs-confirmation` |
| Hikari `pool wait p99 > 100ms 5분` / `pool exhaustion > 1m` threshold 가 ca-tmpl SLA 와 일치하며 측정 가능 | `HIKARI-POOL-C1`~`C5` 는 axiom + MBean attribute 존재만 보장. 정확한 SLO 수치는 HikariCP 가 정의하지 않음 | k6 부하 테스트로 p99 wait time 측정 +`hikaricp.connections.acquire`/`.usage` Micrometer metric 노출 확인 | `planned` |
| Micrometer metric name (registry 는 `hikaricp.connections.acquire`/`.usage`/`.active` — 노트 D3 의 `.pending` 과 불일치) 의 정확한 정의 | `HIKARI-POOL-C6` `needs-confirmation` + registry drift — HikariCP wiki 본문에는 metric 명 직접 없음 | Spring Boot Actuator / Micrometer reference 의 HikariCP metric 섹션 raw 수집 후 metric 명 확정 + D3 정합 | `needs-confirmation` |
| OSIV off 상태에서 service layer 가 fetch graph (`@EntityGraph`/`JOIN FETCH`/DTO projection) 를 일관성 있게 적용 | `OSIV-AP-C1`~`C3` 의 권고는 도구 사용을 강제하지 않음 | ArchUnit 또는 Hibernate statistics 로 N+1 발생 시 fail 하는 contract test | `planned` |
| disaster recovery restore drill 의 효과성 (D7 의 운영 정책) | D7 은 cited raw 외부 근거 없음 — 내부 정책 | 분기 1회 staging restore smoke test 실행 결과 (RTO / RPO 측정) | `planned` |
| read replica 도입 시 max lag threshold 의 적절한 값 (D8) | D8 은 cited raw 외부 근거 없음 — 내부 정책 | Postgres streaming replication 모니터링 + 도메인별 stale-read SLA 정의 | `planned` |
## Audit & Findings
> §2 ca-tmpl ground truth 대조에서 발견한 drift / scope 이슈. 사용자 결정 영역은 자동 rewrite 하지 않고 *정합 권고만* 남긴다 (CLAUDE.md §11, branch-spec §2).
- **CATEGORY_DRIFT** (🔴 정합 권고): §Decision Evidence Map **D4** `PERSISTENCE/DB_UNAVAILABLE` · **D5** `DATA_INTEGRITY_VIOLATION`, §Decisionized Work Items 동일 표기가 코드/registry SSOT 와 어긋남.
- `Category.java` 10-value enum = {VALIDATION, AUTH, AUTHZ, NOT_FOUND, CONFLICT, RATE_LIMIT, TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY, DATA_INTEGRITY, INTERNAL} — **`PERSISTENCE` 없음**.
- `error-codes.yaml` 실제값: `DB_UNAVAILABLE`→`TRANSIENT_DEPENDENCY`(503); integrity→`DB_NULL_VIOLATION`/`DB_FK_VIOLATION`/`DB_CHECK_VIOLATION`(`DATA_INTEGRITY`,409); 23505→`DB_UNIQUE_VIOLATION`(`CONFLICT`,409). 코드 `DATA_INTEGRITY_VIOLATION` 은 registry 부재.
- §SQLState → Error Code Matrix 는 이미 정합. **drift 전파 경로**: project-note §6 → governing canonical `data-layer-persistence-cache-outbound.md` L51/L89(`PERSISTENCE / CONFLICT / TRANSIENT_DEPENDENCY` 3-category) → 본 노트 D4/D5. `error-codes.yaml` L580 주석에도 stale `persistence→PERSISTENCE/CONFLICT` 잔존.
- **권고**: D4/D5 + Decisionized Work Items 의 `PERSISTENCE/`·`DATA_INTEGRITY_VIOLATION` 표기 + governing canonical 의 3-category 문구를 registry 값으로 정합. (사용자 결정 영역 → 본 명령은 정합 권고만, 자동 rewrite 보류.)
- **METRIC_NAME_DRIFT** (🟡): D3 이 `hikaricp.connections.pending` 인용 → `metrics.yaml` 는 `.acquire`/`.usage`/`.active` 사용(`.pending` 미등록). 권고: D3·§Claims metric 명을 registry 와 정합.
- **OUT_OF_BRANCH_SCOPE 후보** (D7, D8 — deferred): restore drill cadence(D7) + read replica lag(D8) 는 본 branch In-scope("DataAccessException 분류 / JPA mapping / pool / OSIV") 밖. 둘 다 UNSUPPORTED_DECISION(내부 RTO/RPO·SLA 정책, 외부 권위 근거 없음). **자동조사 보류 사유**: 내부 운영 SLA 는 외부 공식 문서가 권위적으로 결정하지 않음(회사 블로그→공식 승격 금지). **추적 (2026-06-09)**: 부모 [[raw/project-notes/ca-skeleton-operational-contract]] §11 Persistence "추후 branch 분해 대상" 에 deferred 로 기록됨 → 착수 시 `feature-disaster-recovery-restore-drill` / `feature-read-replica-lag-contract` 로 전개.
- **IMPL_STATUS reconciliation** (2026-06-09 갱신): SQLState→exception 런타임 변환 어댑터 `PersistenceExceptionTranslator` 가 adapter-persistence `failure/` 에 **구현됨** → 런타임 translator `actually-implemented` (Phase C2). DB_* 9 코드는 registry 에서 `OperationalError` enum 으로도 승격되어 `ErrorCodeRegistryMappingTest` 가 enum↔registry http_status 정합을 강제. presentation 매핑 (`GlobalExceptionHandler#handlePersistenceFailure`) + OSIV fail-fast (`OpenInViewSafetyValidator`) + SQL-log 금지 contract test (`SqlLoggingForbiddenContractTest`) 도 `actually-implemented`. 잔여 `planned`: runbook `runbook://db/*` 파일 부재(`docs/runbooks/`), N+1 fetch-graph ArchUnit, k6 pool-wait 부하측정, D7/D8(OUT_OF_BRANCH_SCOPE).
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> governing: [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]] (§Persistence). `/coverage` 가 최종 갱신 — 아래는 branch-spec 1차 seed.
| 관심사 | 상태 | owner | 심각도 | 근거 |
| --------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------ |
| SQLState 9-row classifier → DataAccessException hierarchy 매핑 | covered-here | — | — | D1, D4, D5, D6 + §SQLState Matrix |
| Hibernate OSIV off baseline | covered-here | — | — | D2 |
| HikariCP pool wait/exhaustion alert | covered-here | feature-metrics-alerting-contract (metric 공동) | — | D3 + §Audit 위임 |
| SQL/parameter 로그 금지 | covered-here | feature-env-driven-runtime-configuration (`APP_DATASOURCE_SHOW_SQL`/`_FORMAT_SQL` 소유) | — | policy 본 branch; config `show-sql=false` default. contract test `planned` |
| datasource/pool/timeout/connection exhaustion log field | covered-here | feature-env-driven-runtime-configuration (`APP_DATASOURCE_URL`/`_USERNAME`/`_PASSWORD`/`_POOL_MAX_SIZE`/`_POOL_MIN_IDLE`/`_CONNECTION_TIMEOUT`/`_OPEN_IN_VIEW` 소유) | — | §구현 가이드 4 항목별 위임 명시 |
| read replica lag threshold | delegated | (제안) `[[raw/branch-notes/feature-read-replica-*]]` | 🟡 Should-fix | D8 — §Audit OUT_OF_BRANCH_SCOPE (위임 브랜치 미생성) |
| disaster recovery restore drill | delegated | (제안) `[[raw/branch-notes/feature-disaster-recovery-*]]` | 🟡 Should-fix | D7 — §Audit OUT_OF_BRANCH_SCOPE (위임 브랜치 미생성) |
## 마주친 문제
- (Phase C2 2026-06-09) 없음 — TDD 로 각 레이어 red→green, `./gradlew check` 전체 통과. IDE diagnostics 의 "DB_* cannot be resolved" 는 shared-contract 미재컴파일로 인한 stale 신호였고 gradle 빌드에서는 정상 해소.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/persistence-hikaricp-pool-sizing-wiki]]
- [[raw/official-docs/persistence-osiv-antipattern-hibernate-vladmihalcea]]
- [[raw/official-docs/persistence-r2dbc-reactive-spring]]
- [[raw/official-docs/persistence-spring-dataaccessexception-hierarchy]]
<!-- GENERATED: sources:end -->
> Phase C2 실 코드 작성 단계 (2026-06-09) 진입 — derived 후보 아래 정리. errors 노트는 불필요(클린 사이클).
### 오류 기록 (본 feature 작업 중 발생)
- (없음 — TDD 사이클이 깔끔하게 통과, 별도 raw/errors 노트 불필요)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- "adapter-web 이 adapter-persistence 를 의존할 수 없는데 persistence 의 `DataAccessException` 분류 결과를 어떻게 presentation 까지 leak 없이 전달하는가?" → shared-contract 의 framework-neutral carrier(`PersistenceFailureException`) + `OperationalError` DB_* 코드, web 은 category 별 고정 safe message. (raw/interviews 승급 후보 — Phase D)
- "JPA 예외를 SQLState 로 분류할 때 Spring `SQLErrorCodeSQLExceptionTranslator`(vendor xml) 재사용 vs 커스텀 매핑 trade-off?" → 9-row 정확 제어 위해 커스텀 채택, SQLState 문자열 기반이라 Spring subtype 이 coarse 해도(23505/23502 둘 다 `DataIntegrityViolationException`) CONFLICT/DATA_INTEGRITY 로 정확 분기.
- "OSIV off 를 Spring Boot WARN 에만 의존하지 않고 startup fail-fast 로 강제한 이유?" → WARN 은 deploy 를 막지 못하므로 `SmartInitializingSingleton` hard stop.
### Blog topics
- (별도 topic 없음 — branch note + interview prep 로 충분)
## 관련 일일 노트
- 2026-06-09: Phase C2 실 구현 — translator/carrier/enum/web-handler/OSIV-validator/contract-test 6종 추가, `./gradlew check` 통과.
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):