Spring에 Redis를 이용해서 캐싱을 적용하는 방법을 알아봅시다. Redis 의존성이나 연결 설정은 모두 되었다는 가정하에 진행하겠습니다.
Spring에서 캐싱과 관련된 기능은 Spring 3.1 부터 기본적으로 내장이 되어 있으므로 별도의 라이브러리 의존성을 추가할 필요는 없습니다. 다만 스케쥴링 기능 등과 마찬가지로 특정 어노테이션으로 해당 기능을 활성화 시켜주어야 합니다. 그 어노테이션이 @EnableCaching입니다.
@Configuration이 붙은 설정 클래스에 다음과 같이 붙여주면 됩니다.
@Configuration
@EnableCaching
public class CacheConfig {}
추가로 해 주어야 할 게 CacheManager를 Bean으로 등록해주어야 합니다. CacheManager 캐싱과 관련된 전반적인 설정을 하는 클래스로 이 클래스가 Bean으로 등록이 되어있어야지 이어서 살펴볼 캐싱 기능들이 정상적으로 동작합니다! 주의해야 할 점이 @EableCaching이 붙은 설정 클래스에서 등록을 해주어야 합니다.
@Configuration
@EnableCaching
public class CacheConfig {
// `RedisCacheManager`는 CacheManager`의 자식 클래스로 Redis를 이용하여 캐싱을 수행 할 것이기에 해당 클래스를 Bean으로 등록.
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
// 캐시 값에 Null 허용.
.disableCachingNullValues()
// TTL(Time to Live)를 120초 설정.
.entryTtl(Duration.ofSeconds(120))
// 캐시에 붙을 prefix를 설정. `CacheKeyPrefix.simple()`의 경우 `::`로 설정.
.computePrefixWith(CacheKeyPrefix.simple())
// 캐시에 저장할 값을 어떻게 직렬화 할 것인지. RedisSerializer.java() 사용.
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java())
);
return RedisCacheManager
.builder(connectionFactory)
// 모든 캐시에 기본적으로 저장.
.cacheDefaults(config)
.build();
}
}
위와 같이 CacheManger 인스턴스를 만들어 Bean으로 등록해주면 됩니다.
해당 어노테이션을 통해 데이터를 캐시에 저장하거나 읽어옵니다. 다음 메서드를 봅시다.
@Cacheable(cacheNames = "itemCache", key = "args[0]")
public ItemDto readOne(Long id) {
log.info("Read One: {}", id);
return itemRepository.findById(id)
.map(ItemDto::fromEntity)
.orElseThrow(() ->
new ResponseStatusException(HttpStatus.NOT_FOUND));
}
id를 통해 Respository에서 단건 조회를 해오는 메서드입니다. 해당 메서드에 @Cacheable를 붙여주었습니다.
cacheNames: 캐시의 이름을 지정해줍니다. 앞서 캐시 설정에서 지정한 prefix인 ::와 합쳐져 name::key를 키로 캐시가 저장됩니다.key: name::key에서 key에 해당할 값을 지정해줍니다. args[0]을 하면 매개변수 중 첫번째 값이 되고 #id와 같이 매개변수의 명을 활용할 수도 있습니다.이 어노테이션의 정확한 동작은 다음과 같습니다. 주어진 파라미터로 캐시를 조회하고 해당 값이 존재하면 메서드를 실행하지 않고 캐싱된 값을 반환, 해당 값이 존재하지 않으면 메서드를 실행하고 결과를 캐시에 저장한 후 반환합니다.
해당 메서드로 캐싱을 수행하면 Redis에 다음과 같이 저장이 됩니다.

해당 어노테이션은 메서드의 실행 결과를 항상 캐시에 저장하고 이미 캐싱된 값이 있다면 덮어씌웁니다.
@CachePut(cacheNames = "itemCache", key = "#result.id")
public ItemDto create(ItemDto dto) {
return ItemDto.fromEntity(itemRepository.save(Item.builder()
.name(dto.getName())
.description(dto.getDescription())
.price(dto.getPrice())
.build()));
}
위 메서드는 데이터를 DB에 저장하는 함수입니다. 즉, 해당 메서드의 결과가 항상 캐시에 저장되어야 합니다. 이런 경우에 @CachePut를 사용합니다.
key의 값을 #result.id 지정해주었습니다. 여기서 #result는 메서드의 반환 값이고 그 값 객체의 id를 key로 지정한다는 뜻입니다.
캐시에서 값을 제거하는 어노테이션입니다.
@CacheEvict(cacheNames = "itemCache", key = "args[0]")
public void delete(Long id) {
itemRepository.deleteById(id);
마찬가지로 cacheNames와 key를 제공 받아 해당하는 캐시를 제거합니다. DB에서 데이터가 지워지면 역시나 캐시에서도 지워져야 함으로 해당 메서드에 붙여주었습니다.
DB에 데이터를 업데이트 하는 경우 다른 어노테이션과 조합하여 다음과 같이 활용할 수 있습니다.
@CachePut(cacheNames = "itemCache", key = "args[0]")
@CacheEvict(cacheNames = "itemAllCache", allEntries = true)
public ItemDto update(Long id, ItemDto dto) {
Item item = itemRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
item.setName(dto.getName());
item.setDescription(dto.getDescription());
item.setPrice(dto.getPrice());
return ItemDto.fromEntity(itemRepository.save(item));
}
itemAllCache는 모든 아이템 리스트에 대한 캐시입니다.
만약 DB에서 한 아이템에 대한 값이 업데이트가 된다면 역시나 캐시에서도 해당 값을 @CachePut으로 업데이트 해주어야 합니다.
그리고 itemAllCache 역시 업데이트 되기 전 값을 포함한 리스트가 캐싱되어 있으므로 새로 갱신이 필요하기에 @CacheEvict를 통해 지워줍니다. 이후에 다시 아이템 리스트에 대한 요청이 들어오면 새로운 리스트가 캐시에 갱신 될 것입니다.
페이지네이션이 적용되어있을 경우, page와 size 마다 캐싱이 이루어져야 합니다. 또한 검색 기능의 경우 검색어에 따라서도 캐싱이 개별로 이루어져야 합니다. 이럴 경우 다음과 같이 여러 값을 key로 지정해 구현할 수 있습니다.
@Cacheable(
cacheNames = "itemSearchCache",
key = "{ args[0], args[1].pageNumber, args[1].pageSize }"
)
public Page<ItemDto> searchByName(String query, Pageable pageable) {
return itemRepository.findAllByNameContains(query, pageable).map(ItemDto::fromEntity);
}
검색어로 받은 query를 args[0]을 통해, 페이지네이션을 위해 받은 Pageable 객체를 args[1]을 통해 Pageable 객체의 pageNumber와 pageSize를 Key로 활용해주었습니다.
만약 query가 0이고 pageNumber이 0이고 pageSize이 20이면 다음과 같이 캐싱이 됩니다.
