문제 발생

서비스 구조상 로컬 Caffeine 캐시 + Redis 캐시를 병합해서 사용했는데, 데이터 정합성 문제가 자꾸 발생했다.

  • 로컬 캐시에만 남아 있는 값이 Redis와 달라 오래된 데이터가 제공됨
  • Redis에 업데이트가 발생해도 로컬 캐시 반영이 늦어 사용자마다 결과 불일치
  • 서버 인스턴스가 3대라서, 한 곳에서 무효화해도 나머지 노드가 알지 못하는 상황 발생

멀티 인스턴스 환경에서 캐시 불일치가 터진 것이다.


원인 파악

  • Caffeine은 로컬 JVM 내에서만 동작 → 다른 서버와 캐시 동기화 불가
  • Redis는 중앙 집중 캐시라서 값이 최신이지만, 로컬 Caffeine은 그대로 남아 있음
  • 캐시 정합성 보장을 위해서는 Pub/Sub 기반 무효화가 필요했다

해결

Redis Pub/Sub을 이용해 캐시 무효화 이벤트를 모든 서버에 전파했다.

@PostConstruct
private void initPubSub() {
    clusterPubSubConnection.addListener(new RedisPubSubListener<String, String>() {
        @Override
        public void message(String channel, String message) {
            log.info("[PubSub 구독] 채널={}, 메세지={}", channel, abbrev(message, 70));
            try {
                if (redisInfo.CACHE_INVALIDATION_CHANNEL.equals(channel)) {
                    onInvalidate(message);
                } else if (redisInfo.CACHE_PUT_GOODS_CHANNEL.equals(channel)) {
                    onPutGoods(message);
                }
            } catch (Exception e) {
                log.warn("[PubSub] 핸들 실패: ch={}, head={}", channel, abbrev(message, 200), e);
            }
        }

        @Override
        public void subscribed(String channel, long count) {
            log.info("[PubSub][subscribed] 채널={} (구독 수={})", channel, count);
        }
    });

    pubSubAsync.subscribe(redisInfo.CACHE_INVALIDATION_CHANNEL, redisInfo.CACHE_PUT_GOODS_CHANNEL)
               .toCompletableFuture().join();
}

private void onInvalidate(String key) {
    List<String> keys = Arrays.stream(key.split(","))
            .map(String::trim)
            .filter(s -> !s.isEmpty())
            .toList();
    if (keys.isEmpty()) return;

    asyncStringCaff.synchronous().invalidateAll(keys);
    asyncDynamicStringCaff.synchronous().invalidateAll(keys);
    asyncDynamicStringGoodsCaff.synchronous().invalidateAll(keys);

    keys.forEach(ettlMeta::remove);
    keys.forEach(ttlMap::remove);

    log.info("[PubSub][INVALIDATE] 캐시 갱신 {} keys", keys);
}
  • CACHE_INVALIDATION_CHANNEL → 무효화 이벤트 전파
  • CACHE_PUT_GOODS_CHANNEL → 상품상세 캐시 갱신 이벤트 전파
  • 모든 서버에서 구독해 동시에 캐시를 삭제/갱신 → 데이터 정합성 확보

효과 / 깨달음

  • 로컬 캐시(Caffeine)와 Redis 캐시의 데이터 정합성을 유지할 수 있게 됐다.
  • 서버 인스턴스 3대 모두에서 동시에 캐시 무효화가 반영된다.
  • “로컬 캐시만 보고 Redis 최신값을 놓치는 문제”가 해결됐다.
  • 캐시 계층(L1-L2)을 혼용할 때는 반드시 Pub/Sub 같은 동기화 장치가 필요하다.