Skip to content

feat(spec-006): 사용자 통계 캐시 stale-cache 해소 (통계 버전 기반 캐시 키)#4

Open
simhani1 wants to merge 7 commits into
devfrom
006-statistics-cache-versioning
Open

feat(spec-006): 사용자 통계 캐시 stale-cache 해소 (통계 버전 기반 캐시 키)#4
simhani1 wants to merge 7 commits into
devfrom
006-statistics-cache-versioning

Conversation

@simhani1

@simhani1 simhani1 commented Jun 5, 2026

Copy link
Copy Markdown
Owner

개요

마이페이지 통계(GET /api/v1/members/mypage) 캐시가 24h TTL 동안 stale 해지는 문제를 캐시 키에 "통계 버전"을 포함해 해소한다. 버전은 직전에 완료된 progressCalculation 배치의 기준일(yyyy-MM-dd, Asia/Seoul)이며, 배치가 완료된 후에만 전환되어 자정 직후 중간 상태가 새 버전으로 캐싱되지 않는다.

Spec: specs/006-statistics-cache-versioning/ (spec·plan·research·data-model·contracts·tasks)

핵심 설계

  • 버전 = 직전 완료 배치 기준일: JobLog 에서 MAX(endTime) where jobType='progressCalculation' and endTime is not null (QueryDSL 스칼라). 완료 0건이면 오늘(콜드스타트). 매 요청 DB 파생(버전 캐시 없음) → stale 창 0 수렴
  • 완료 후에만 전환: production 은 finish()(endTime set) 후에만 JobLog 저장 → 진행 중 row 미영속 → 새 날 배치 완료 전까지 버전이 어제로 유지(중간 상태 미캐싱)
  • Caffeine 로컬 → Redis 공유 캐시 이관: 다중 인스턴스 동일 버전 키 공유, 전역 1회 재계산. 키 challenge-avg::{memberId}:{version}, TTL 25h
  • single-flight: lockingRedisCacheWriter + @Cacheable(sync=true) → 동시 첫 조회 Flask 1회 수렴
  • 실패 비캐싱: Flask 예외 전파 → 미저장 → 다음 요청 재계산
  • 신규 build.gradle 의존성 0, 신규 테이블 0 (JobLog(job_type, end_time) 인덱스만 추가)

검증 (Testcontainers MySQL+Redis)

  • ./gradlew check GREEN — 179 tests, 0 fail
  • verifySecretLogScan clean (헌법 V)
  • 인수 테스트 8/8 (헌법 VII — 인수기준=자동테스트):
인수기준 테스트 메서드
SC-001 배치 완료 후 갱신값 100% should_recompute_on_first_read_after_version_transition_and_hit_within_same_version
SC-002 중간 상태 고정 0건 should_not_cache_intermediate_state_while_batch_in_progress
SC-003 사용자별 evict 0회 should_keep_old_version_key_without_evict_on_version_transition
SC-004 동시 첫 조회 Flask 1회 should_converge_flask_call_to_once_on_concurrent_first_reads
SC-005 과거 버전 TTL 만료 should_set_ttl_around_25h_on_cache_key
FR-007 실패 비캐싱·재계산 should_not_cache_failure_and_allow_retry_on_next_request
FR-008 전역 일관(결정성) should_return_deterministic_version_for_same_db_state
콜드스타트(Q3) should_use_today_as_version_on_cold_start_with_no_completed_batch

헌법 7원칙

원칙 결과
I Testcontainers ✅ 8 인수 테스트 IntegrationTest(MySQL+Redis) 확장, Flask만 @MockBean
II 어댑터 격리 ✅ 버전=JPA(DB), 캐시=@Cacheable 추상화. 도메인 RedisTemplate 직접접근 0
III QueryDSL MAX(endTime) 스칼라 집계
IV Outbox ➖ N/A (발행 없음)
V 시크릿 로그 ✅ 신규 로깅 0, scan clean
VI 듀얼 리뷰 ⏳ 아래 체크리스트
VII 인수=테스트 ✅ 8종 1:1 박제

듀얼 AI 리뷰 (헌법 VI)

  • Claude 리뷰specs/006-statistics-cache-versioning/_harness/claude-review.md (최종 권고: ✅ MERGE, P1=0/P2=0)
  • Codex 리뷰 — 첨부 예정 (각 코멘트 채택/기각+사유 명시)

잔여 백로그 (머지 비차단 P3)

  • P3-1 직렬화 @class 결속(GenericJackson2JsonRedisSerializer) — DTO 리네임/이동 시 TTL 내 잔존 캐시 역직렬화 500 위험. 후속 "역직렬화 실패→miss" 복원력 과제로 추적.

🤖 Generated with Claude Code

simhani1 and others added 7 commits June 6, 2026 03:54
통계 버전 기반 캐시 키로 stale cache 해소. 배치 완료 후에만
버전 전환(US2)해 중간 상태 캐싱 방지. 전수 evict 없이 TTL 자연 만료.
실제 구현(challenge-avg Caffeine 로컬, progressCalculation 배치)과의
차이는 Assumptions에 기록.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Q1: 다중 인스턴스 가정, 공유 분산 캐시(Redis) + 공유 버전 소스
- Q2: 통계 버전 = 직전 완료 배치의 데이터 기준일(영업일)
- Q3: 콜드스타트(완료 배치 0건)는 오늘 기준일 잠정 버전 사용

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 버전 = 직전 완료 progressCalculation JobLog endTime의 기준일(DB 파생, 헌법 II)
- 캐시 Caffeine→Redis 공유 이관, 키=memberId:version, TTL 25h
- single-flight=lockingRedisCacheWriter+sync, 실패 비캐싱
- 헌법 게이트 전원 통과, 신규 의존성/테이블 없음

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
조기 최적화 회피. 버전 해석을 인덱스된 MAX(endTime) 1건 읽기로 매 요청
직접 수행. (job_type, end_time) 복합 인덱스 추가. stale 창·동시조회
레이스 경계가 메모 만료 instant → DB 커밋 instant(마이크로초)로 축소.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setup→Foundational(버전해석+Redis 버전키 캐시)→US1(SC-001 MVP)
→US2(SC-002)→US3(SC-003/005)→Polish(SC-004/FR-007/콜드스타트/게이트).
신규 의존성 없음. 통합테스트는 IntegrationTest(MySQL+Redis) 확장.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundational+US1(MVP): JobLog (job_type,end_time) 인덱스 + QueryDSL
MAX(endTime) 버전 쿼리, StatisticsVersionProvider(매 요청 DB 파생,
Asia/Seoul yyyy-MM-dd, 콜드스타트=오늘), CacheConfig Caffeine→Redis
이관(TTL 25h, GenericJackson2Json, lockingRedisCacheWriter),
MemberStatisticsCacheService(@Cacheable key=memberId:version, sync),
MemberServiceImpl 버전 위임(자기호출 회피).

테스트: SC-001(버전 전환 재계산) + FR-008(결정성) 통합테스트,
StatisticsCacheTestSupport 헬퍼. MemberIntegrationTest 회귀 2건
새 설계(버전키/분산캐시)에 맞게 갱신.

verify: ./gradlew check GREEN 173 tests, verifySecretLogScan clean.
review: 헌법 7원칙 PASS, P1=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng 단일화

US2/US3/Polish 인수 기준 박제(6개 통합 테스트):
- SC-002 진행중(완료 row 부재) 중간상태 미캐싱
- SC-003 버전 전환 시 구 키 잔존(evict 0회)
- SC-004 single-flight Flask 1회 수렴
- SC-005 캐시 키 TTL 25h 근사
- FR-007 실패 비캐싱·재시도 가능
- 콜드스타트 오늘 기준일 잠정 버전

P3-2: PlanetrushApplication 중복 @EnableCaching 제거(RedisConfig 단일화).

verify: ./gradlew check GREEN 179 tests(인수 8/8), verifySecretLogScan clean.
review: 전체 헌법 7원칙 PASS, P1=0/P2=0. claude-review.md(헌법 VI) 생성.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant