--- 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/` — 캐시 다중 백엔드 라우터 계획의 단계별 결정·진행 기록. ## 부모 (필수) - **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]] 형제 branch: - [[raw/branch-notes/feature-cache-consistency-contract]] ## 묶음 - [[raw/blog-topics/cache-backend-router-fail-open-decorator-2026-07-02]] > 본 feature branch 는 leaf — 자식 자료 없음. 구현 진행 중에 errors / interview prep 이 누적되면 본 섹션에서 그룹화. ### 오류 기록 (본 feature 작업 중 발생) - (없음 — Task 2 clean) ### 면접 준비 (이 작업에서 나올 수 있는 면접 질문) - "fail-open 을 왜 별도 데코레이터로 분리했나?" — 정책 드리프트 방지: 새 백엔드가 try/catch 를 직접 구현하면 로그 포맷·로직이 달라질 위험. - "RedisCacheStore 가 이미 fail-open 이었는데 왜 분리가 필요한가?" — 다음 백엔드(Memcached 등)에 동일 정책을 재사용하기 위해. SRP 적용. ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: cache backend 선택·fallback·failure routing과 contract test가 명시된다 ### 상속한 프로젝트 결정 | 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]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 목표 `RedisCacheStore` 내부에 고착된 fail-open try/catch 정책을 데코레이터(`FailOpenCacheStore`)로 분리하여, 미래 캐시 백엔드들이 정책 드리프트 없이 동일한 fail-open 계약을 재사용하게 한다. - 이슈: (내부 계획 — `docs/superpowers/plans/2026-06-12-cachestore-multi-backend-router.md`) - PR: TBD ## 범위 ### 포함 범위 - 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>` 수집으로 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)