Cacheable과 CacheEvict 실습

최창효·2024년 1월 24일
post-thumbnail

이 예제에서는 Redis를 캐시저장소로 사용합니다.

CacheConfig

@Configuration
@EnableCaching
public class CacheConfig {
    @Value("${spring.data.redis.port}")
    public int port;

    @Value("${spring.data.redis.host}")
    public String host;
    
    @Value("${spring.data.redis.password}")
    public String password;

	// 레디스 설정
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setPort(port);
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPassword(password);

        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

	// 캐시매니저 등록
	@Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build();
    }

	// 이름을 지정하지 않았을 때의 기본속성을 정의할 수 있다.
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofSeconds(500))
            .disableCachingNullValues()
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
            );
    }

	// 특정 이름의 캐시 속성을 여러개 정의할 수 있다.
    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return (builder) -> builder
            .withCacheConfiguration("cache1",
                RedisCacheConfiguration.defaultCacheConfig()
                    .computePrefixWith(cacheName -> "prefix::" + cacheName + "::")
                    .entryTtl(Duration.ofSeconds(3000))
                    .disableCachingNullValues()
                    .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
                    )
                    .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
                    )
            )
            .withCacheConfiguration("cache2",
                RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(3000))
                    .disableCachingNullValues()
                    .serializeKeysWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
                    )
                    .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
                    )
            );
    }
}

Cacheable

  • Cacheable은 캐시에 값이 존재하면 캐시의 값을 사용하고, 그렇지 않으면 메서드를 실행시킨 뒤 그 결과를 캐시에 저장하고 값을 반환합니다.
		@Cacheable(cacheNames = "basicCache")
    public List<MyEntity> findAll() {
        return myRepository.findAll();
    }
  • cacheNames을 RedisCacheManagerBuilderCustomizer에서 정의한 cache1 또는 cache2로 하면 해당 설정을 적용받습니다. 지금 basicCache는 별도로 정의되어 있는 설정이 없기 때문에 redisCacheConfiguration의 설정을 적용받습니다.
  • cacheNames와 value는 같은 값입니다.
  • Redis에서 key를 확인해보면 basicCache::SimpleKey []라는 이름으로 저장됩니다.

Cacheable With key

		@Cacheable(key = "'zzz'", cacheNames = "cache1")
    public List<MyEntity> findByAge(Integer age) {
        return myRepository.findAllByAge(age);
    }
  • key를 확인해보면 prefix::cache1::zzz라는 이름으로 저장됩니다. prefix가 붙은 이유는 Config에서 .computePrefixWith(cacheName -> "prefix::" + cacheName + "::")를 설정했기 때문입니다.
		@Cacheable(key = "#age", cacheNames = "cache2")
    public List<MyEntity> findByAge(Integer age) {
        return myRepository.findAllByAge(age);
    }
  • key는 SpEL문법으로 동작합니다. 그래서 #매개변수로 매개변수를 가져올 수 있습니다. 매개변수가 객체라면 #객체.변수도 가능합니다.
  • key를 확인해보면 cache2::10라는 이름으로 저장됐습니다(조회할 대 age값을 10으로 줬었다). 즉, cacheNames::key형태로 키가 저장됩니다.

CacheEvict

  • CacheEvict는 해당 캐시를 비웁니다. 보통 값을 delete할 때, 또는 update에 CacheEvict를 걸고 변경이 발생했을 때 실제 DB를 찌르게 해서 항상 최신의 값만 보여주는 용도로 사용됩니다.
		@CacheEvict(key = "'id'", cacheNames = "cache1")
    public void update(Long id){
        MyEntity myEntity = myRepository.findById(id).orElseThrow(RuntimeException::new);
        myEntity.setIsUpdated(true);
    }
  • 해당 cacheNames, key의 캐시를 지웁니다.
		@CacheEvict(cacheNames = "cache1", allEntries = true)
    public void update(Long id){
        MyEntity myEntity = myRepository.findById(id).orElseThrow(RuntimeException::new);
        myEntity.setIsUpdated(true);
    }
  • key를 선언하지 않고 대신 allEntries = true를 주면 해당 cacheNames를 가진 모든 캐시를 지웁니다.

페이징과 캐싱

페이징 데이터는 캐싱을 도입해도 문제가 없는지를 고민해봐야 합니다. 그렇지 않으면 상황에 따라 중복이나 누락이 발생할 수도 있습니다.

  1. 누군가 1페이지를 요청했다. 해당 요청에 의해 1페이지 정보가 캐싱됐다.
  2. 100번 게시물이 갑자기 인기글이 되어 1페이지에 올라왔다. 하지만 이미 1페이지는 캐싱됐기 때문에 100번 게시물이 보이지 않는다.
  3. 100번 게시물이 1페이지에 올라오면서 원래 1페이지 마지막에 있던 5번 게시글이 2페이지로 밀려났다.
  4. 누군가 2페이지를 요청했다. 해당 요청에 의해 2페이지 정보가 캐싱됐다. 그 결과 5번 게시글은 1페이지에서도 보이고, 2페이지에서도 중복되어 보이게 된다.
profile
기록하고 정리하는 걸 좋아하는 백엔드 개발자입니다.

0개의 댓글