--- 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 실패 계약을 정의합니다. ## 부모 (필수) - **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]] > ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다. ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: persistence failure mapping과 integration test가 통과한다 ### 상속한 프로젝트 결정 | 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]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 목표 DB/JPA 실패를 단순히 `DataIntegrityViolationException -> 409`로 끝내면 운영 기준에 부족합니다. connection unavailable, lock, timeout, integrity, query/system failure를 분리하고 presentation까지 JPA 예외가 새지 않게 해야 합니다. - 이슈: - PR: ## 범위 ### 포함 범위 - 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` L230–354, 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` L230–354 (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` L230–354 | `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` L100–105 | `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` L307–373) → `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 빌드에서는 정상 해소. ## 묶음 - [[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]] > 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):