Files
llm-wiki/raw/branch-notes/feature-cachestore-multi-backend-router.md
T

201 lines
14 KiB
Markdown

---
title: branch / feature-cachestore-multi-backend-router
source_type: branch-note
status: raw
branch: feature-cachestore-multi-backend-router
parent_branch:
related_projects: [ca-skeleton]
tags: [branch, ca-skeleton, cache, decorator, fail-open, outbound-adapter]
created: 2026-06-12
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-049
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-049
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-024]
contract_packet: 1
contract_packet_sha256: 42ff5787aebde944ffe6a393e9d9f1621e3c75fc5abb1ba1e85d64d6d7c71a77
---
# branch: feature-cachestore-multi-backend-router
> Layer: `raw/branch-notes/` — 캐시 다중 백엔드 라우터 계획의 단계별 결정·진행 기록.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
형제 branch:
- [[raw/branch-notes/feature-cache-consistency-contract]]
## 묶음
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/cache-backend-router-fail-open-decorator-2026-07-02]]
<!-- GENERATED: blog-topics:end -->
> 본 feature branch 는 leaf — 자식 자료 없음. 구현 진행 중에 errors / interview prep 이 누적되면 본 섹션에서 그룹화.
### 오류 기록 (본 feature 작업 중 발생)
- (없음 — Task 2 clean)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- "fail-open 을 왜 별도 데코레이터로 분리했나?" — 정책 드리프트 방지: 새 백엔드가 try/catch 를 직접 구현하면 로그 포맷·로직이 달라질 위험.
- "RedisCacheStore 가 이미 fail-open 이었는데 왜 분리가 필요한가?" — 다음 백엔드(Memcached 등)에 동일 정책을 재사용하기 위해. SRP 적용.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: cache backend 선택·fallback·failure routing과 contract test가 명시된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1` | Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1` | Resilience4j가 default이며 Spring Retry는 simple blocking retry에만 허용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1` | framework는 Spring Boot 3.5.14다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-TEST-001@1` | test framework는 JUnit 5다 | 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 -->
## 목표
`RedisCacheStore` 내부에 고착된 fail-open try/catch 정책을 데코레이터(`FailOpenCacheStore`)로 분리하여, 미래 캐시 백엔드들이 정책 드리프트 없이 동일한 fail-open 계약을 재사용하게 한다.
- 이슈: (내부 계획 — `docs/superpowers/plans/2026-06-12-cachestore-multi-backend-router.md`)
- PR: TBD
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- Task 2: `FailOpenCacheStore` 데코레이터 신규 작성 (정책 공통화)
- `src/adapter-outbound/.../cache/FailOpenCacheStore.java`
- `src/adapter-outbound/.../cache/FailOpenCacheStoreTest.java` (4계약 TDD)
- Task 5: `CacheStoreRouter` 논리명 라우팅 + 부팅 검증 + D4 fail-fast
- `src/adapter-outbound/.../cache/CacheStoreRouter.java`
- `src/adapter-outbound/.../cache/CacheStoreRouterTest.java` (5계약 TDD)
- 향후 Task 3: `RedisCacheStore` 슬림화 (내부 try/catch 제거 → `FailOpenCacheStore` 위임)
- 향후 Task N: 추가 백엔드 바인딩 (Memcached 등)
### 제외 범위
- 이번 Task 에서 `RedisCacheStore`/`RedisCacheAdapterConfig` 수정 없음 (다음 Task 몫).
- Spring `@Configuration` 등록 (다음 Task 몫 — Task 5 에서는 순수 Java 클래스만).
## TODO
> Task 2 완료. Task 5 완료. Task 3 이후는 별도 dispatch.
## 결정 사항 (decisions)
- 2026-06-12: fail-open 정책(try/catch + logFailure → Optional.empty)을 `FailOpenCacheStore` 데코레이터로 추출. 모든 백엔드는 위임으로만 정책을 받는다.
- 2026-06-12: `CacheBackendException` 은 다음 Task 에서 신규 작성. 현재 javadoc 은 `{@code}` 임시 링크.
- 2026-06-12: 데코레이터는 `final` — 서브클래싱 차단으로 정책 드리프트 방지.
- 2026-06-12 (Task 5): `CacheStoreRouter` 는 논리명(`worklog`) → 백엔드 ID(`redis`) 매핑만 담당. Spring 의존 없는 순수 Java. 생성자에서 바인딩-백엔드 정합 검증(startup validation). 미바인딩 접근은 `AdapterDisabledException`(D4 fail-fast). `resolve()``private` — B7 ACL return type 규칙 준수 (`CacheStore` 타입 노출 없음).
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시.
| Decision ID | Decision (요약) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | fail-open 정책을 데코레이터로 분리 (Decorator pattern) | 기존 `RedisCacheStoreTest` 4계약이 동일 logback ListAppender 패턴으로 검증됨 — 패턴 재사용 가능성 확인 | `internally-verified` (ca-tmpl 기존 코드 관찰) | `CacheBackendException` 미존재 — 다음 Task 에서 생성 전까지 javadoc 링크 불완전 |
| D2 | `FailOpenCacheStore``CacheStore` 구현 + `final` | Decorator pattern — GoF 패턴 (UNSUPPORTED_DECISION — 외부 verbatim raw 없음) | `internal-policy` | 서브클래싱 차단이 확장성에 제약이 될 수 있음. 현재 단일 String 타입 캐시만 지원 |
| D3 | `get` 실패 = `Optional.empty()` 반환, `put` 실패 = silent swallow | 기존 `RedisCacheStore` 의 fail-open 계약과 일치 — `CacheStore` 인터페이스 javadoc 에 명시됨 | `internally-verified` | 호출자가 cache-miss 를 source-of-truth fallback 으로 처리해야 함 — 호출 측 계약 별도 확인 필요 |
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| `FailOpenCacheStore` 가 future `RedisCacheStore` 슬림화 후에도 동일 4계약을 보장 | 현재 `RedisCacheStore` 는 수정 미완료 | Task 3 완료 후 `RedisCacheStoreTest` 전체 통과 확인 | `planned` |
| `CacheBackendException` 도입 후 javadoc `{@link}` 복원 시 컴파일 안전 | 다음 Task 에서 생성 예정 | Task 3 에서 `{@code}``{@link}` 교체 + 컴파일 확인 | `planned` |
| `FailOpenCacheStore` 가 미래 백엔드(Memcached 등)에 실제로 재사용 가능 | 현재 String 키/값만 지원 — 타입 파라미터화 필요 여부 미검토 | 다음 백엔드 도입 Task 에서 확인 | `open` |
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| 기존 `RedisCacheStoreTest` (in-repo) | logback ListAppender 패턴 재사용 — D1 |
| `CacheStore` 인터페이스 javadoc (in-repo) | fail-open 계약 정의의 SSOT — D3 |
| [[raw/branch-notes/feature-cache-consistency-contract]] | Redis unavailable degrade 계약의 상위 결정 맥락 |
## 진행 현황
| Task | 상태 | 커밋 |
|---|---|---|
| Task 1: AdapterDisabledException detail 오버로드 (shared-contract) | ✓ 완료 (테스트 5 PASS) | 미커밋 (사용자 git 금지 지시) |
| Task 2: FailOpenCacheStore 데코레이터 | ✓ 완료 (파일 2개 신규, 테스트 4개 PASS) | 미커밋 (사용자 git 금지 지시) |
| Task 3: RedisCacheStore 슬림화 + CacheBackendException | ✓ 완료 (품질리뷰 FIX 포함, RedisCacheStoreTest 5 PASS) | 미커밋 |
| Task 4: CacheBindingSettings (`app.cache.bindings.*`) | ✓ 완료 (테스트 2 PASS) | 미커밋 |
| Task 5: CacheStoreRouter 논리명 라우팅 + D4 fail-fast | ✓ 완료 (파일 2개 신규, 테스트 5개 PASS) | 미커밋 (사용자 git 금지 지시) |
| Task 6: 조립 전환 (sentinel 폐기, `@Bean(name="redis")` 기여, OCP 증명 테스트) | ✓ 완료 (`:adapter-outbound:test` 137 PASS) | 미커밋 |
| Task 7: 문서 정합화 (CacheStore javadoc / adapter-outbound CLAUDE.md / application.yml 주석) | ✓ 완료 (메인 에이전트 직접 수행 — 사용자 지시로 서브에이전트 체인 중단) | 미커밋 |
| Task 8: 전체 가드레일 검증 | ✓ 완료 — `verifyCleanArchitectureDependencies` PASS, ArchUnit `CleanArchitectureTest` PASS, `DisabledCacheStore` 잔존 참조 0건, 전체 `./gradlew check` BUILD SUCCESSFUL (32s) | - |
| 후속 리팩터: `CacheBackend` 마커 인터페이스 (빈이름 매직 제거) + fail-open 중앙화 | ✓ 완료 (사용자 비평 수용, 메인 에이전트 직접) — 기여 계약을 "빈 이름 = backendId" 규약에서 `CacheBackend.backendId()` 타입 명시 계약으로 전환; `FailOpenCacheStore` 합성을 백엔드 Config 관례에서 `CacheRouterConfig` 중앙 적용으로 이동(구조적 보장); 라우터에 중복 backendId 부팅 검증 추가; `RedisCacheAdapterConfig`는 raw `RedisCacheStore` 기여만 하는 얇은 Config로 축소. 캐시 범위 29 tests PASS, ArchUnit PASS(B7: `backendId()`는 String 반환이라 합법), 의존 매트릭스 PASS. (`actually-implemented`, `locally-verified`) | 미커밋 |
- 기록 분산 주의: Task 1·3·4·6 의 상세 구현 기록은 세션이 돌던 git 브랜치명 기준으로 [[raw/branch-notes/feature-domain-event-outbox-contract]] 의 "진행 중 메모"에 적재됨 (2026-06-12 항목들). 본 노트가 이 feature 의 SSOT 이며, 해당 항목들은 이 작업의 기록이다.
## 진행 중 메모
- provider 선택·fallback·실패 routing의 세부 상태는 위 진행 현황과 TODO를 기준으로 추적한다.
## 구현 가이드
- core는 `CacheStore` SPI만 알고 provider registry가 설정값을 실제 adapter로 해석한다.
- 지원하지 않는 provider·중복 key·필수 backend 부재는 startup에서 실패시키고 runtime silent fallback을 만들지 않는다.
- backend별 동일 contract suite로 get·put·evict·timeout 의미를 대조한다.
## 엣지·실패·의존
- provider 이름 오타나 중복 등록은 잘못된 backend 선택으로 이어지므로 fail-fast가 필요하다.
- cache consistency 계약과 runtime configuration 계약에 의존한다.
## 마주친 문제
- backend별 capability 차이를 공통 SPI에 과도하게 노출하면 core가 특정 기술에 결합된다. 공통 최소 계약 밖 기능은 adapter-local로 둔다.
## 관련 일일 노트
- 별도 일일 노트 없음.
## 완료 후 정리
- PR 링크: (미정 — 사용자가 일괄 커밋 예정, 커밋·푸시 전)
- 리뷰 메모: Task 별 ca-architect-sentinel → ca-spec-reviewer → ca-quality-reviewer 체인 수행 (Task 1-6). spec NEEDS_FIX 2건은 모두 선재 working-tree 변경(outbox 작업·세션 이전 javadoc 줄바꿈)으로 판명되어 controller Override. 품질 Important 2건(Task 3 테스트 갭)은 수정 완료. Task 7-8 은 사용자 지시로 메인 에이전트 직접 수행 (소규모 작업에 체인 과잉).
- 머지 결과 / 배포 환경: 미머지 (작업 트리 상태)
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented`: `FailOpenCacheStore` 데코레이터 패턴 + 4계약 TDD
- `actually-implemented`: `CacheStoreRouter` 논리명 라우팅 — startup-time binding validation + D4 fail-fast + B7 ACL 준수 — 5계약 TDD
- `actually-implemented`: `CacheBackend` 타입 명시 기여 모델 — `ObjectProvider<List<CacheBackend>>` 수집으로 OCP 달성 (2번째 백엔드 = 신규 Config 파일만; `OptionalAdapterBeanGatingTest.a_second_backend_plugs_in_...` 이 증명 테스트). 초기 구현은 빈이름=backendId 규약이었으나 사용자 비평(빈이름 매직·무차별 수집·탐색 불가) 수용 후 마커 인터페이스로 교체 — `backendId()` 가 String 반환이라 B7 합법이라는 발견이 전환점
- `actually-implemented`: B7 ArchUnit 제약이 설계를 두 번 바꾼 사례 — (1) 바인딩 record(accessor 가 CacheStore 반환) 폐기 → 빈이름-키 맵 주입, (2) String 반환 메서드는 합법임을 재발견 → `CacheBackend` 마커 인터페이스로 최종 수렴
- **추출하지 않을 항목** (planned / documented-only / abandoned):
- application-core 소비자 포트 (planned — 소비 계층 결정 대기), put TTL 옵션 객체 (planned), L1/L2 컴포지트 (planned)