[BE-03] FeatureFlag Redis 캐시 + pub/sub 전파 + 로컬 스냅샷

작업 내용 (설계 의도)

변경 사항

변경 전파 방안 A(Redis 캐시 look-aside + pub/sub 브로드캐스트 + 인스턴스 로컬 인메모리 스냅샷)의 인프라 구현. Redis pub/sub는 신규 인프라(RedisMessageListenerContainer 레포 0건). 근거 TDD: ../TDD.md “방안 비교 — 변경 전파”·Sequence(변경 전파). pub/sub 리스너는 도메인 비즈니스 로직이 아닌 캐시 코히런스 메커니즘이므로 infrastructure에만 둔다(Kafka Consumer의 presentation 규칙 비적용 — 명시적 근거는 TDD 서버 토폴로지).

  • RedisFeatureFlagCacheStore (FeatureFlagCacheStore 구현): 키 featureflag:flag:{key}, JSON 값(FeatureFlagSnapshot), TTL 10분(안전망). put/get/evict. StringRedisTemplate.
  • RedisFeatureFlagChangeBroadcaster (FeatureFlagChangeBroadcaster 구현): 채널 featureflag:changes에 변경 flagKey를 convertAndSend.
  • FeatureFlagRedisPubSubConfig (infrastructure/config, 신규 파일): RedisMessageListenerContainer 빈 + MessageListener 등록(featureflag:changes 구독).
  • FeatureFlagChangeSubscriber (MessageListener): 수신 시 LocalFeatureFlagStore.refresh(key) 호출(멱등 — 재조회 후 덮어쓰기). 도메인/UseCase 미경유(캐시 갱신 전용). 수신 시각과 메시지의 변경 occurredAt 차이를 전파 지연으로 기록(feature_flag_propagation_lag_seconds — BE-12가 게이지화).
  • LocalFeatureFlagStore: ConcurrentHashMap 기반 L1 + 마지막 성공 스냅샷. refresh(key)(캐시→미스 시 Repository 재조회 후 로컬·캐시 채움), get(key), 기동 시 findAllActive로 부트스트랩 워밍, @Scheduled(30초) 전체 리프레시(pub/sub 유실 안전망). Redis 채널 계약은 REDIS-01(redis-implementer 계약 문서) 준수.
  • 지표 발신(senior-pm 정합 #2): 캐시 접근마다 Micrometer counter feature_flag_cache_access_total{layer=local|redis,result=hit|miss} 증가. Redis 접근 실패(DataAccessException)를 예외 전파 없이 삼키고 redis-degraded(warning, source=feature-flag) 이벤트를 발신한다.

의존

  • BE-01 (Gateway 인터페이스·FeatureFlagSnapshot)
  • REDIS-01 (키·채널 계약 — redis-implementer, 병행 선행)
  • (런타임) BE-02 Repository — 부트스트랩/미스 폴백 시. 컴파일은 domain interface(BE-01)만.

다이어그램

처리 흐름

sequenceDiagram
    participant W as 쓰기 인스턴스
    participant R as Redis(채널 featureflag:changes)
    participant S as FeatureFlagChangeSubscriber
    participant L as LocalFeatureFlagStore
    W->>R: broadcast(key)
    R-->>S: onMessage(key)
    S->>L: refresh(key)
    L->>R: cacheStore.get(key)
    L-->>L: 로컬 스냅샷 갱신

테스트 케이스 (Testcontainers Redis)

  • cacheStore.put 후 get이 동일 스냅샷을 반환하고 TTL이 설정된다
  • broadcast(key) 발행 시 구독 중인 subscriber가 수신해 LocalStore.refresh를 호출한다
  • 동일 key를 pub/sub로 2회 수신해도 로컬 상태가 동일하다(멱등)
  • LocalStore 부트스트랩은 findAllActive 결과를 전부 로컬에 적재한다
  • 로컬 미스 시 refresh가 Redis 캐시를 먼저 조회하고, 미스면 Repository로 폴백한다
  • evict(key) 후 get은 null을 반환한다
  • Redis 연결 실패 시 cacheStore.get이 예외를 전파하지 않고 null로 처리되며 redis-degraded 경보가 발신된다(평가 폴백 전제)
  • 캐시 히트/미스 시 layer·result 태그가 붙은 카운터가 각각 증가한다
  • subscriber 수신 시 변경 occurredAt과 수신 시각의 전파 지연이 기록된다