Files
llm-wiki/raw/branch-notes/feature-cache-consistency-contract.md

264 lines
19 KiB
Markdown

---
title: branch / feature-cache-consistency-contract
source_type: branch-note
status: raw
branch: feature-cache-consistency-contract
related_projects: [ca-skeleton]
tags: [branch, ca-skeleton, cache, redis, consistency]
created: 2026-05-22
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-024
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-024
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-DATABASE-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
parent_branch:
contract_packet_sha256: 6d4978b50ddf3cab75e6803a198f9ca53753f9812e8f3a158f96c3544c843008
---
# branch: feature-cache-consistency-contract
> Layer: `raw/branch-notes/` — cache consistency와 Redis/cache adapter 기준을 정의합니다.
> [!important] 주도권 이전 고지 (2026-07-28)
> 아래 관심사의 **owner 가 신규 branch 로 이동**했다. 근거·절차: `docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md` §5.
>
> | 이전 ID | 대상 D-row | 이전한 관심사 | 신규 owner |
> |---|---|---|---|
> | H3 | D3·D4·D5·D6·D7·D8·D9 | cache 전략 — stampede·TTL·직렬화·negative·cluster | [[raw/branch-notes/feature-cache-strategy-registry-contract]] |
>
> **본문은 아직 제거하지 않았다.** 신규 branch 는 현재 스캐폴딩 상태(D-row 없음)이므로, 지금 본문을 지우면 결정 근거가 소실된다. 위 D-row 는 신규 branch 의 `/branch-spec` 이 재판정을 마칠 때까지 **잠정 근거**로만 유효하며, 그 시점에 설계 §5.2 6단계에 따라 포인터(`superseded → [[<신규 branch>]] D<m>`)로 치환한다.
>
> 이 기간 중 새 결정을 본 branch 에 추가하지 않는다 — 신규 owner 에 작성한다.
<!-- 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: sources:start -->
- [[raw/company-tech-blogs/cache-woowahan-after-commit-invalidation]]
- [[raw/official-docs/cache-aside-vs-write-through-aws]]
- [[raw/official-docs/cache-caffeine-asyncloadingcache-readme]]
- [[raw/official-docs/cache-redisson-rlock-vs-setnx]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/cache-consistency-after-commit-stampede-contract-2026-07-02]]
<!-- GENERATED: blog-topics:end -->
> 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (본 feature 작업 중 발생)
- (없음 — 현재 documented-only 단계)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase C2 실 구현 단계에 누적)
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: after-commit invalidation·stampede failure fixture가 통과한다
<!-- 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]] |
<!-- 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 -->
## 목표
cache miss/unavailable만으로는 cache 운영 기준이 부족합니다. stale cache, stampede, key naming, TTL, invalidation 실패를 skeleton 기준에 포함해야 합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- cache aside 기준.
- stale cache 허용 범위.
- cache stampede 방지 기준.
- cache key naming.
- TTL 기준.
- invalidation 실패 분류.
- Redis unavailable degrade 기준과 연결.
### 제외 범위
- business-specific cache policy.
- distributed lock 기본 구현.
- Redis cluster 운영 설정.
## 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` 승급 위치 |
## 진행 중 메모
- cache consistency는 optional adapter이지만, 붙였을 때 같은 실패 계약을 따라야 합니다.
## 결정 사항 (decisions)
- 2026-05-22: cache consistency를 Redis adapter 내부 세부사항으로만 두지 않음.
- 2026-05-22: core는 single-instance/local cache policy만 완결. distributed cache lock/stampede 방지는 multi-instance package 활성화 시 필요.
- 2026-05-22: Redis cluster 운영은 out of core이나 cluster mode 활성화 시 key hash/tag policy와 failover runbook이 필요.
- 2026-05-22: cache invalidation = after-commit only. Spring `TransactionSynchronizationManager.registerSynchronization`으로 강제. tx 내부 또는 tx 미참여 상태에서의 cache mutation은 forbidden.
- 2026-05-22: stampede 방지 default = single-instance Caffeine local lock, multi-instance HPA 시 Redisson RLock distributed mutex. application 별 override 금지.
- 2026-05-22: cache value serialization = JSON (Jackson). 스키마 변경 시 key version suffix(`v{n}` 접미사) 필수.
- 2026-05-22: negative cache 정책 = 존재하지 않는 row는 짧은 TTL(60s) 캐싱 허용. application별 override 가능.
- 2026-05-22: eventual consistency window default = 5s (TTL과는 별개로 invalidation propagation 허용 한계).
- 2026-05-22: negative cache TTL(60s)와 invalidation propagation window(5s)는 독립 축. negative cache는 invalidation 채널 적용 대상에서 제외 (적용 시 정상화). 두 수치는 의도된 분리.
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 는 `company-case-study` 로만 라벨 (best practice 단정 금지).
| Decision ID | Decision (요약) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | cache pattern default = cache-aside (write-through without consistency contract 는 forbidden) | `raw/official-docs/cache-aside-vs-write-through-aws.md#CACHE-PAT-C1`, `#CACHE-PAT-C2`, `#CACHE-PAT-C3` | `official-vendor-doc` (AWS + Redis 공식 — lazy caching 정의 + application 책임 + write-through latency tradeoff) | "write-through 가 결제/주문 도메인에 부적합" 은 cited raw 가 직접 prescribe 안 함 — ca-tmpl 내부 결정. write-behind 의 data loss 메커니즘 (`#CACHE-PAT-C5`) 은 `needs-confirmation` — AWS Database Blog 또는 Redis docs 별도 raw 필요 |
| D2 | cache invalidation = after-commit only. Spring `TransactionSynchronizationManager.registerSynchronization` 으로 강제. tx 내부/tx 미참여 상태 cache mutation forbidden | `raw/company-tech-blogs/cache-woowahan-after-commit-invalidation.md` (company-case-study — 우아한형제들 한국 사례) | `company-case-study` (NOT official best practice) | Spring `TransactionSynchronizationManager` 공식 reference 의 after-commit hook 시맨틱 verbatim 미수집 — 별도 official-doc raw 필요. 우아한형제들 사례는 한 회사의 결정이며 official-standard 가 아님 |
| D3 | stampede 방지 default = single-instance Caffeine local lock, multi-instance HPA 시 Redisson RLock distributed mutex | `raw/official-docs/cache-redisson-rlock-vs-setnx.md#LOCK-C1` (Redis SET NX PX 단순 패턴, 단일 인스턴스 efficiency lock), `#LOCK-C4` (Kleppmann: efficiency vs correctness lock 분리), `raw/official-docs/cache-caffeine-asyncloadingcache-readme.md` (single-instance LoadingCache stampede 방지) | `official-vendor-doc` (LOCK-C1 — Redis 공식 verbatim 확인) + `engineering-blog` (LOCK-C4 — Kleppmann 비판, WebFetch 차단으로 재확인 보류) | LOCK-C3 (Redisson RLock watchdog + `j.u.c.locks.Lock` 호환) 은 `needs-confirmation` — redisson.org → redisson.pro redirect 차단. Redisson Javadoc 직접 다운로드 필요. Caffeine raw 의 claim ID 매핑 미확인 (본 세션 mandatory read 범위 밖) |
| D4 | core 는 single-instance/local cache policy 만 완결. distributed cache lock/stampede 방지는 multi-instance package 활성화 시 필요 | `raw/official-docs/cache-redisson-rlock-vs-setnx.md#LOCK-C4` (efficiency vs correctness lock 분리 — cache stampede = efficiency lock) | `engineering-blog` (Kleppmann, 재확인 보류) | "stampede = efficiency lock" 의 분류가 모든 cache 시나리오 (token bucket, rate limit 등) 에 적용되는지 미검증 — correctness 가 필요한 endpoint 식별 필요 |
| D5 | cache value serialization = JSON (Jackson). 스키마 변경 시 key version suffix (`v{n}`) 필수 | (UNSUPPORTED_DECISION — cited raw 4종 중 직접 verbatim claim 없음. Jackson docs / Redis serialization 공식 raw 별도 필요) | `internal-policy` | schema versioning 컨벤션은 ca-tmpl 내부 결정 — 외부 권위 근거 미수집 |
| D6 | negative cache 정책 = 존재하지 않는 row 는 짧은 TTL(60s) 캐싱 허용 | (UNSUPPORTED_DECISION — cited raw 4종에 negative cache TTL verbatim 없음) | `internal-policy` | 60s 값은 ca-tmpl 내부 SLO — 외부 근거 미수집 |
| D7 | eventual consistency window default = 5s (TTL 과는 별개로 invalidation propagation 허용 한계) | (UNSUPPORTED_DECISION — cited raw 4종에 propagation window 수치 verbatim 없음) | `internal-policy` | 5s 값은 ca-tmpl 내부 SLO — 외부 근거 미수집 |
| D8 | negative cache TTL(60s) 와 invalidation propagation window(5s) 는 독립 축 (D6/D7 분리) | (UNSUPPORTED_DECISION — 위 두 값 자체가 internal policy) | `internal-policy` | 두 수치 모두 외부 근거 없음 |
| D9 | Redis cluster 운영은 out of core. cluster mode 활성화 시 key hash/tag policy + failover runbook 필요 | (UNSUPPORTED_DECISION — cited raw 4종에 Redis cluster key hashtag 시맨틱 verbatim 없음. Redis 공식 cluster spec 별도 필요) | `internal-policy` | Redis cluster keyspace 분배 권고 (hashtag `{}`) verbatim raw 별도 수집 필요 |
## 검증해야 할 주장
> 공식 문서 인용이 결정의 근거이지만, ca-tmpl 자체 구현에서의 동작은 별도 검증이 필요한 주장.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| LOCK-C3 (Redisson RLock watchdog + `j.u.c.locks.Lock` 호환) 의 verbatim 재확인 | 1차 URL redisson.org → redisson.pro 301 redirect, redirect 호스트 호출 차단으로 verbatim 재확인 불가 | Redisson Javadoc 직접 다운로드 또는 archive.org 스냅샷으로 verbatim 격상 | `needs-confirmation` |
| LOCK-C4 (Kleppmann fencing token) verbatim 재확인 | martin.kleppmann.com WebFetch permission denied | archive.org Kleppmann "How to do distributed locking" 스냅샷 verbatim 확보 | `needs-confirmation` |
| stampede 방지 contract test (ArchUnit `methodsThat().areAnnotatedWith(@Cacheable)... `withAttribute("sync", "true")` 또는 `AsyncLoadingCache` 또는 `RLock` wrap) 가 실제로 위반 검출 | cited raw 는 stampede 방지 도구 비교까지만 보장 — ArchUnit rule 동작은 별도 | ArchUnit test 작성 + 의도적 위반 case (sync=false 한 `@Cacheable` 추가) 로 fail 확인 | `planned` |
| multi-instance cache claim consistency (env `APP_MULTI_INSTANCE_ENABLED=true` + `APP_CACHE_REDIS_ENABLED=true` 시 Redisson bean 등록 + 모든 hot cache 메서드 RLock wrap) | LOCK-C3 가 `needs-confirmation` 인 상태에서 RLock wrap 의 실제 효과 미보증 | `MultiInstanceCacheStampedeContractTest` 작성 + 두 flag true 일 때 Redisson bean verify + 동시 cache miss 1회 backend 호출 확인 | `planned` |
| after-commit invalidation 이 tx rollback 시 cache 에 stale write 를 남기지 않음 | 우아한형제들 사례 (D2) 는 company-case-study — 우리 환경에서의 동작 별도 보장 필요 | TransactionTemplate rollback 시나리오 integration test + Redis key 미존재 단언 | `planned` |
| Redis unavailable 시 degrade 가능 endpoint 가 declared 된 경우에만 fail-fast 회피 (generic INTERNAL 금지) | cited raw 는 degrade 정책 자체를 prescribe 안 함 — 내부 결정 | contract test: Redis down 시 declared degrade endpoint 는 fallback 응답, undeclared 는 503/`CACHE_UNAVAILABLE` 반환 | `planned` |
| negative cache TTL 60s + invalidation propagation 5s 값의 적절성 (D6/D7) | UNSUPPORTED_DECISION — 외부 근거 없음 | 도메인별 stale tolerance SLO 측정 + p99 user-visible staleness 추적 | `planned` |
| Spring `TransactionSynchronizationManager.registerSynchronization` 의 after-commit hook 시맨틱 (D2 메커니즘) | 공식 reference verbatim raw 미수집 | Spring Framework Reference §Transaction Synchronization raw 수집 후 `afterCommit` hook 보장 verbatim 확인 | `needs-confirmation` |
## Decisionized Work Items
| item | Decision | Allowed | Forbidden | Required test |
| --- | --- | --- | --- | --- |
| cache pattern | cache-aside default | read-through if adapter owns it | write-through without consistency contract | cache behavior test |
| TTL | explicit per key family | no-cache for sensitive data | immortal cache | TTL test |
| stampede | local lock in single-instance | distributed lock for HPA | hot key without guard | stampede test |
| key scope | app/profile/operation/tenant-if-enabled | hash compact key | PII/raw user id | key naming test |
| Redis unavailable | degrade only if declared | fail-fast for required cache | generic INTERNAL | unavailable mapping |
## 테스트 계약
- cache key naming에 operation/tenant/profile 기준이 없으면 실패.
- invalidation 실패가 조용히 무시되면 실패.
- stampede 방지 검증: `@Cacheable`이 적용된 모든 메서드는 (a) `sync=true` 명시 또는 (b) Caffeine의 `AsyncLoadingCache` 사용 또는 (c) Redisson `RLock` wrap 중 하나여야 함. 측정 방법: ArchUnit `methodsThat().areAnnotatedWith(@Cacheable).should().beAnnotatedWith(@Cacheable.class).withAttribute("sync", "true")` 또는 동등 reflection check. 위반 시 fail.
- Redis unavailable이 degrade 가능 여부 없이 INTERNAL로 처리되면 실패.
- multi-instance cache claim consistency: env property `APP_MULTI_INSTANCE_ENABLED=true`이고 `APP_CACHE_REDIS_ENABLED=true`이면 Redisson `RedissonClient` bean이 등록되어 있고 모든 hot cache 메서드가 RLock으로 wrap되어 있어야 함. 측정 방법: 두 flag 모두 true일 때 contract test `MultiInstanceCacheStampedeContractTest.java`에서 Redisson bean verify + RLock 사용 검증.
- tx rollback 시 cache에 stale write가 남으면 실패.
- 동일 key에 대해 동시 cache miss 시 backend 호출이 1회로 제한되는지 verify (stampede). 미충족 시 실패.
## 마주친 문제
- 아직 없음.
## 근거 (필수, 최소 1개+)
> 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/cache-aside-vs-write-through-aws]] | cache-aside default 채택의 trade-off 표 + AWS 공식 분류 |
| [[raw/official-docs/cache-caffeine-asyncloadingcache-readme]] | single-instance stampede 방지를 `LoadingCache` / `@Cacheable(sync=true |
| [[raw/official-docs/cache-redisson-rlock-vs-setnx]] | multi-instance HPA에서 Redisson RLock 채택 + SETNX/Redlock 배제 근거 (Kleppmann 비판 포함 |
| [[raw/company-tech-blogs/cache-woowahan-after-commit-invalidation]] | after-commit invalidation 결정의 한국 사례 + Spring `TransactionSynchronizationManager` 강제 근거 |
## 외부 근거 (Group G-C — Cache consistency)
ca-tmpl cache 결정 backbone + stampede 방지 도구 비교 자료.
- 채택 결정의 공식 근거:
- [[raw/official-docs/cache-aside-vs-write-through-aws]] — cache-aside default 채택의 trade-off 표 + AWS 공식 분류.
- [[raw/official-docs/cache-caffeine-asyncloadingcache-readme]] — single-instance stampede 방지를 `LoadingCache` / `@Cacheable(sync=true)`에 매핑하는 공식 근거.
- [[raw/official-docs/cache-redisson-rlock-vs-setnx]] — multi-instance HPA에서 Redisson RLock 채택 + SETNX/Redlock 배제 근거 (Kleppmann 비판 포함).
- 사례 / 한국 도메인:
- [[raw/company-tech-blogs/cache-woowahan-after-commit-invalidation]] — after-commit invalidation 결정의 한국 사례 + Spring `TransactionSynchronizationManager` 강제 근거. (회사 기술블로그 — 사례 취급)
검색 키워드 기록: `cache-aside vs write-through trade-off`, `Caffeine AsyncLoadingCache stampede`, `Redisson RLock vs SETNX`, `Kleppmann Redlock unsafe`, `우아한형제들 캐시 무효화 트랜잭션`.
## 구현 가이드
- write transaction commit 이후에만 invalidate하고 rollback 시 cache를 변경하지 않는다.
- TTL·key namespace·stampede 방지 정책은 registry 값으로 고정하고 backend adapter가 적용한다.
- hit·miss·eviction·fallback을 contract test와 metric으로 함께 검증한다.
## 엣지·실패·의존
- commit 전 eviction은 rollback 뒤 stale miss를, eviction 실패 무시는 stale read를 만들 수 있다.
- database transaction·multi-backend router·runtime context 계약에 의존한다.
## 관련 일일 노트
- 별도 일일 노트 없음.
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):