1. 서비스 요구사항


왜 캐시 이중화가 필요할까?


<aside>

앞서 캐시를 서비스에서 어떻게 사용하고 있는지 설명했다

하지만 중요한거는 레디스에 대한 장애 조치가 되어 있지 않다는 점이다.

처음에는 레디스 장애시 기본적으로 데이터베이스를 조회하도록 구현했지만 해당 방식보다 로컬 캐시를 활용하는 것이 성능상 더 좋다고 판단했다

따라서 캐시를 이중화하여 기본적으로 레디스 캐시를 사용하고 만약 장애 발생 시 로컬 캐시를 사용한다

</aside>

<aside>

프로세스


  1. 조회 발생 시 1차적으로 레디스 조회를 한다. 만약 데이터가 없을 시 이를 데이터베이스에서 조회하고 레디스 캐시에 저장한다.
  2. 만약 레디스 장애가 발생하면 로컬 캐시를 확인한다.
  3. 로컬 캐시에 데이터가 없을 시 데이터베이스에서 조회 후 로컬 캐시에 저장한다. </aside>

카페인을 선택한 이유


<aside>

로컬 캐시 라이브러리로 카페인을 선택하였다. 그 이유를 아래에서 확인해보자

</aside>

<aside>

캐시 정책


카페인은 W-TinyLFU(Window TinyLfu)라는 고급 캐시 제거 정책을 사용한다. 이는 LRU와 LFU 알고리즘의 장점을 결합하여 기존 LRU보다 높은 적중률을 달성한다.

즉 높은 적중률을 통해 메모리 관리 시 캐시 미스가 발생하는 빈도를 줄여주는 것이다.

</aside>

<aside>

ConcurrentHashMap의 단점


ConcurrentHashMap의 경우 구현이 쉽고 빠르지만, 메모리 관리를 우리가 직접해야한다는 점에서 문제가 있다. 즉, TTL 정책이나 캐시 정책같은 것들에 대한 관리가 쉽지 않다.

</aside>

<aside>

스프링의 캐싱 추상화와의 통합


선언적 캐싱 애노테이션 사용할 수 있다. 예를 들어 @Cacheable, @CachePut, @CacheEvict와 같은 스프링의 캐싱 애노테이션은 카페인과 함께 원활하게 사용할 수 있다.

</aside>

2. 캐시 이중화 코드


1️⃣ 통합 캐시 매니저 빈 등록


<aside>

  1. 레디스 캐시 정책을 가진 캐시 매니저와 카페인 캐시 정책을 가진 캐시 매니저를 생성한다.
  2. 이후 TwoLevelCacheManager를 만들고 이 것을 빈으로 등록한다.
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory cf) {
    RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofDays(7))
            .serializeValuesWith(
                    RedisSerializationContext.SerializationPair
                            .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    return RedisCacheManager.builder(cf).cacheDefaults(cacheConfiguration).build();
}

@Bean
public CaffeineCacheManager caffeineCacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager(
            PAGE_CACHE_KEY, SUB_ITEM_CACHE_KEY
    );

    manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(60, TimeUnit.MINUTES));

    return manager;
}

@Bean
@Primary
CacheManager cacheManager(
        RedisCacheManager redisCacheManager,
        CaffeineCacheManager caffeineCacheManager
) {
    return new TwoLevelCacheManager(redisCacheManager, caffeineCacheManager);
}

</aside>

2️⃣ 캐시 조회