359 lines
46 KiB
Markdown
359 lines
46 KiB
Markdown
---
|
||
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` 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 빌드에서는 정상 해소.
|
||
|
||
## 묶음
|
||
|
||
<!-- 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):
|