Files
llm-wiki/wiki/concepts/data-layer-persistence-cache-outbound.md
T

129 lines
11 KiB
Markdown

---
title: Data Layer Baseline (Persistence + Cache + Outbound HTTP)
source_type: llm-generated
status: draft
confidence: medium
tags: [persistence, jpa, cache, http-client, resilience]
related_projects: [ca-skeleton]
last_reviewed: 2026-05-22
---
# Data Layer Baseline (Persistence + Cache + Outbound HTTP)
> Layer: `wiki/concepts/` — Phase E Group G-C 합성. 3개 sub-topic(Persistence failure / Cache consistency / Outbound HTTP)을 하나의 baseline canonical로 묶음. 프로젝트 적용 사실은 `wiki/projects/`에 별도 작성하고 본 문서에서는 링크만 둠.
## Summary
Data layer baseline은 세 가지 축으로 구성된다.
- **Persistence**: SQLState 매트릭스로 DB 실패를 분류하고, Spring `DataAccessException` 계층 위에 매핑하여 `PERSISTENCE / CONFLICT / TRANSIENT_DEPENDENCY` 카테고리를 만든다. OSIV는 off가 기본.
- **Cache**: cache-aside default + Caffeine local lock(single-instance) + Redisson `RLock` distributed mutex(multi-instance HPA) + after-commit invalidation + eventual consistency window 5초.
- **Outbound HTTP**: Spring RestClient를 baseline으로 두고, retry/circuit breaker는 Resilience4j로 일원화. timeout default = connect 2s / read 5s / global 10s.
## Standard (공식 정의)
### Persistence — SQLState + Spring DAO hierarchy
- **SQLState** (ISO/IEC 9075): 5-char code로 DB 오류를 표준 분류. `08*` = connection exception, `40001` = serialization failure, `40P01` = deadlock(Postgres), `23xxx` = integrity constraint, `57014` = query canceled.
- **Spring `DataAccessException` hierarchy**: `TransientDataAccessException` / `NonTransientDataAccessException` / `RecoverableDataAccessException`로 retryable/non-retryable 1차 분리. JPA `PersistenceException``JpaSystemException`으로 흡수.
- **OSIV (Open Session In View)**: Hibernate session을 view rendering까지 열어두는 패턴. Vlad Mihalcea가 anti-pattern으로 명시했고 Spring Boot는 활성화 시 startup WARN 로그를 출력. 운영 baseline은 off.
- **HikariCP pool sizing**: 공식 wiki는 `connections = ((core_count * 2) + effective_spindle_count)` 공식과 단일 small pool 권장. pool wait p99 / pool exhaustion이 1차 alert 지표.
### Cache — cache-aside + stampede control
- **Cache-aside** (Microsoft Cloud Design Patterns / AWS ElastiCache): application이 cache miss 시 DB 조회 → cache 채움. invalidation도 application 책임. write-through는 cache layer가 sync 책임, write-behind는 async, read-through는 cache layer가 loader를 안다. 책임 위치가 다름.
- **Caffeine `AsyncLoadingCache` / `@Cacheable(sync = true)`**: 동일 key 동시 miss를 단일 loader 호출로 직렬화 (in-process stampede 방지).
- **Redisson `RLock`**: Redis 기반 reentrant lock + watchdog lease extension. Kleppmann의 Redlock 비판을 회피하기 위해 단일 master 기반 RLock + fence token 사용.
- **after-commit invalidation**: Spring `TransactionSynchronizationManager.registerSynchronization``afterCommit()` hook에서만 cache mutation 수행. tx rollback 시 stale write 차단.
### Outbound HTTP — RestClient + Resilience4j
- **Spring RestClient** (6.1+): `RestTemplate`의 fluent 후속 API. RestTemplate은 Spring 공식 maintenance-only 상태로 신규 기능 추가 없음.
- **Resilience4j**: Netflix Hystrix의 사실상 후속. Hystrix는 2018년 maintenance mode 진입. Retry / CircuitBreaker / TimeLimiter / Bulkhead / RateLimiter를 functional decorator로 제공.
- **Circuit breaker 상태**: `CLOSED``OPEN` (failure rate threshold 초과) → `HALF_OPEN` (probe) → `CLOSED` 복귀. Micrometer로 state transition을 metric으로 노출.
- **Timeout 계층**: connect timeout(소켓 연결) < read timeout(응답 첫 바이트 대기) < global call timeout(전체 호출). 셋 중 하나라도 미설정이면 무한 대기 위험.
## 한계 / 주의점
### Persistence
- SQLState 9-row matrix의 vendor-specific row(PostgreSQL `23505`, `40P01` 등)는 DB 변경 시 재검증 필요. MySQL은 `40001`만 공유하고 `40P01` 대신 다른 코드를 사용.
- OSIV off는 lazy loading exception을 presentation까지 새지 않게 막아주지만, application 경계에서 명시적 fetch 전략(`@EntityGraph`, fetch join, DTO projection)을 강제한다. 익숙하지 않은 팀은 운영 부담이 늘 수 있음.
- R2DBC reactive는 throughput 우위가 있으나 JPA tooling을 포기해야 한다. baseline은 JPA blocking으로 고정한 trade-off의 반대편.
### Cache
- cache-aside의 eventual consistency window가 5초로 잡혀 있어 **strict consistency가 요구되는 use case(잔액, 인증, idempotency 검증)에는 부적합**. 해당 use case는 cache bypass를 명시.
- Caffeine local cache + Redisson 분산 mutex 조합은 노드 간 sync lag이 존재. 한 노드가 invalidation을 발행한 뒤 다른 노드의 local cache가 비워질 때까지 lag 발생.
- Redisson `RLock`도 Kleppmann의 분산 lock 비판에서 완전히 자유롭지 않다. 정확한 fencing을 요구하는 경우 token + DB-level optimistic lock 병행이 필요.
- negative cache(존재하지 않는 row, TTL 60s)는 invalidation 채널 적용 대상에서 제외 — 의도된 분리이지만 row가 실제로 생성된 직후 60초간 stale empty 응답이 나갈 수 있음.
### Outbound HTTP
- RestClient는 Spring 6.1+ 한정. 기존 RestTemplate 코드는 마이그레이션 비용이 따른다.
- WebClient는 reactor event-loop 위에서 동작하므로 MVC(servlet) baseline에 강제 도입하면 blocking risk가 있다. baseline에서는 extension 문서로 분리.
- OpenFeign은 declarative interface로 편리하지만 Spring Cloud 의존이 붙는다. Spring 6.1+ `@HttpExchange`가 framework-level 대안.
- Stripe engineering blog는 retry default-on을 옹호하지만 **이는 idempotency-key 헤더 보장이 전제**. 일반 API에 default-on retry를 적용하면 비-idempotent endpoint의 중복 write 위험이 생긴다.
- Resilience4j는 Spring Boot starter 통합이 매끄럽지만, Spring 외 환경(plain Java, Vert.x 등)에서는 verbose한 functional decorator 작성이 필요. "vendor-neutral"로 단언하기에는 일부 마찰이 있음.
## Project Application
- [[wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound]] — ca-tmpl 의사결정 기록 (현재 `documented-only`, Phase C2 미진입). 실제 구현 여부는 project 문서 참조.
ca-skeleton operational contract와 owning branch-notes:
- [[raw/branch-notes/feature-persistence-failure-baseline]] — SQLState 9-row matrix, Hikari alert threshold, OSIV off 결정
- [[raw/branch-notes/feature-cache-consistency-contract]] — cache-aside default, Caffeine + Redisson, after-commit invalidation, 5s window
- [[raw/branch-notes/feature-outbound-http-client-baseline]] — RestClient baseline, Resilience4j, timeout 2s/5s/10s, shutdown retry suppression
- [[raw/project-notes/ca-skeleton-operational-contract]] — §6 Operational Error Category, §11 Adapter Failure Contract, §29 G-C 외부 근거
## Interview Questions
- SQLState 코드를 어떻게 retryable / non-retryable로 매핑했고 그 분류가 Spring `DataAccessException` hierarchy와 어떻게 정합한가?
- OSIV가 anti-pattern으로 평가되는 이유는 무엇이고 off로 두었을 때 lazy loading은 어떻게 해결하는가?
- cache-aside의 eventual consistency window 5초가 의미하는 바와, 그 안에서 stale read가 허용되지 않는 use case는 어떻게 분리하는가?
- Resilience4j를 Hystrix 대신 선택한 이유와 두 라이브러리의 차이는?
- outbound HTTP timeout을 connect 2s / read 5s / global 10s로 둔 의도와 셋 중 어떤 게 빠지면 어떤 위험이 생기는가?
- after-commit invalidation을 강제하는 이유와, transaction rollback 시 cache 일관성이 어떻게 보장되는가?
## Do Not Overclaim
- "cache-aside면 항상 안전하다" — strict consistency가 요구되는 use case에서는 cache bypass가 필요하다. cache-aside는 eventual consistency 모델이다.
- "Resilience4j는 vendor-neutral이라 어디서나 동일하게 동작" — Spring Boot starter 통합 외 환경에서는 functional decorator를 직접 조립해야 하고 boilerplate가 늘어난다.
- "RestClient가 RestTemplate를 완전히 대체했다" — Spring 6.1+ 한정이고 기존 코드 마이그레이션 비용이 있다.
- "Redisson RLock이면 분산 lock 문제 해결" — Kleppmann 비판은 완화되었지만 fencing token / DB optimistic lock 병행이 필요한 경우가 있다.
- "Stripe처럼 retry default-on이 좋은 패턴이다" — Stripe는 idempotency-key 보장이 전제. 일반 API에 그대로 적용하면 위험하다.
## Sources
### 공식 근거 (Persistence)
- [[raw/official-docs/persistence-spring-dataaccessexception-hierarchy]] — Spring `DataAccessException` 계층 (SQLState 분류의 framework-level anchor)
- [[raw/official-docs/persistence-osiv-antipattern-hibernate-vladmihalcea]] — Hibernate 권위자의 OSIV anti-pattern 명시 + Spring Boot WARN
- [[raw/official-docs/persistence-hikaricp-pool-sizing-wiki]] — pool sizing 공식과 alert threshold 출처
- [[raw/official-docs/persistence-r2dbc-reactive-spring]] — JPA blocking baseline의 trade-off 반대편(R2DBC reactive)
### 공식 근거 (Cache)
- [[raw/official-docs/cache-aside-vs-write-through-aws]] — cache-aside / write-through / write-behind / read-through trade-off 공식 분류
- [[raw/official-docs/cache-caffeine-asyncloadingcache-readme]] — single-instance stampede 방지(`@Cacheable(sync = true)`, `AsyncLoadingCache`) 공식 매핑
- [[raw/official-docs/cache-redisson-rlock-vs-setnx]] — multi-instance HPA에서 RLock 채택 + SETNX/Redlock 배제 (Kleppmann 비판 포함)
### 사례 (Cache)
- [[raw/company-tech-blogs/cache-woowahan-after-commit-invalidation]] — after-commit invalidation의 한국 사례 + Spring `TransactionSynchronizationManager` 강제 근거 (회사 기술블로그 — 사례 취급)
### 공식 근거 (Outbound HTTP)
- [[raw/official-docs/outbound-spring-restclient-baseline]] — RestClient baseline + RestTemplate maintenance-only 명시
- [[raw/official-docs/outbound-resilience4j-vs-spring-retry]] — Resilience4j 채택 + Spring Retry 좁은 예외 허용 + Hystrix 배제
- [[raw/official-docs/outbound-webclient-vs-restclient-spring]] — WebClient baseline 배제 이유(reactor event-loop blocking risk)
- [[raw/official-docs/outbound-openfeign-declarative-client]] — Feign declarative 대안 + maintenance status + Spring 6.1+ `@HttpExchange`
### 사례 (Outbound HTTP)
- [[raw/company-tech-blogs/outbound-stripe-rate-limit-retry-engineering]] — retry + idempotency-key 결합, full-jitter backoff. ca-tmpl default-disabled의 보수성 대비 (회사 기술블로그 — 사례 취급)
### Canonical contract
- [[raw/project-notes/ca-skeleton-operational-contract]] — §6 Operational Error Category, §11 Adapter Failure Contract, §29 Group G-C 외부 근거 인덱스