[Redis] Springboot + Redis 캐싱 실습

남순식·2026년 3월 19일

Spring Cache와 Redis를 결합하여 캐싱을 적용해본 실습을 정리

캐싱은 반복되는 데이터 조회에서 DB 부하를 줄이고 응답 속도를 높이는 핵심 기술임.

Cache 전략 요약

전략설명비고
Cache-Aside데이터를 찾을 때 캐시를 먼저 확인하고 없으면 DB 조회가장 일반적임
최초 요청자는 캐싱 되어 있지 않은 정보를 받기 때문에 오래걸림
Write-ThroughDB에 데이터를 쓸 때 캐시에도 동시에 업데이트캐시의 데이터 상태는 항상 최신 데이터임이 보장
Write-BehindDB 쓰기 전 캐시에 먼저 저장 후, 나중에 배치로 DB 반영쓰기 성능 극대화 (실습 미진행),
데이터 유실 가능성 있음

Write-Behind(Write-Back)
이번에 직접 구현하진 않았으나, 쓰기 작업이 매우 빈번할 때 사용하는 방식. 모든 데이터를 캐시에 먼저 저장한 뒤 일정 주기에 따라 DB에 한꺼번에 반영하여 DB 부하를 줄이는 전략.

용어정리

  • 캐시 적중(Cache Hit): 캐시에 접근했을 때 찾고 있는 데이터가 있는 경우
  • 캐시 누락(Cache Miss): 캐시에 접근했을 때 찾고 있는 데이터가 없는 경우를
  • 삭제 정책(Eviction Policy): 캐시에 공간이 부족할때 어떻게 공간을 확보하는지에 대한 정책

1. Spring Cache 설정: @EnableCaching

가장 먼저 캐싱 기능을 활성화하기 위해 설정 클래스를 작성함. Redis를 저장소로 사용하기 위해 RedisCacheManager를 빈으로 등록함.

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues() // null 값은 캐싱하지 않음
                .entryTtl(Duration.ofSeconds(10)) // TTL(Time To Live) 10초 설정
                .computePrefixWith(CacheKeyPrefix.simple())
                .serializeValuesWith(SerializationPair.fromSerializer(RedisSerializer.java()));

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

2. 주요 캐싱 어노테이션 및 전략

실습을 통해 Cache-Aside(Look Aside)Write-Through 전략을 직접 적용해 봄.

2-1. @Cacheable: 조회 성능 최적화 (Cache-Aside)

데이터가 캐시에 있으면 바로 반환하고, 없으면 DB에서 조회 후 캐시에 저장하는 방식임.

  • 적용 대상: 상세 조회(readOne), 전체 목록 조회(readAll)
  • 특징: 동일한 인자로 호출 시 메서드 내부의 로그가 찍히지 않음(캐시 히트).
// cacheNames: 메서드로 인해서 만들어질 캐시를 지칭하는 이름
// key: 캐시에서 데이터를 구분하기 위해 활용할 값
@Cacheable(cacheNames = "itemCache", key = "args[0]")
public ItemDto readOne(Long id) {
    log.info("Read One: {}", id);
    return repository.findById(id)
            .map(ItemDto::fromEntity)
            .orElseThrow(() 
                    -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}

2-2. @CachePut: 데이터 동기화 (Write-Through)

메서드를 항상 실행하여 DB에 데이터를 저장하고, 그 결과를 캐시에도 즉시 업데이트함.

  • 적용 대상: 생성(create), 수정(update)
  • 장점: 다음에 해당 데이터를 조회할 때 항상 최신 상태의 캐시를 보장받을 수 있음.
@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())
            .stock(dto.getStock())
            .build()
    ));
}

2-3. @CacheEvict: 캐시 데이터 삭제

데이터가 변경되거나 삭제될 때 기존의 낡은 캐시를 제거함.
내용이 수정되었지만 readAll은 수정되 전 내용이 캐싱되어 있어서 다음 요청자는 수정된 정보를 알 수 없음.
이템의 정보가 바뀌었으니, 데이터를 전부 돌려준 결과가 더이상 유효하지 않기 때문에 캐싱을 삭제 해야함.

  • 적용 대상: 수정(update), 삭제(delete)
  • 활용: allEntries = true 옵션을 사용하면 특정 캐시 네임(예: itemAllCache)의 모든 데이터를 한 번에 비울 수 있음.
@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());
    item.setStock(dto.getStock());
    return ItemDto.fromEntity(itemRepository.save(item));
}

3. 검색 결과 캐싱

단순 ID 조회가 아닌, 검색어(query)와 페이징 정보(pageNumber, pageSize)를 조합하여 복합적인 키로 캐싱을 구현함.

3-1. 준비

@SpringBootApplication
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
public class RedisApplication {
    // ...
}
  • 참고: pageSerializationMode = VIA_DTO 설정은 Page 인터페이스의 구현체를 JSON 등으로 변환할 때 발생하는 복잡한 구조 문제를 해결해 줌.

3-2. 검색 캐싱

@Cacheable(
    cacheNames = "itemSearchCache", 
    key = "{ args[0], args[1].pageNumber, args[1].pageSize }"
)
public Page<ItemDto> searchByName(String query, Pageable pageable) { ... }
  • Redis 확인 시 itemSearchCache::[검색어, 페이지번호, 사이즈] 형태의 키가 생성되는 것을 확인.

마무리

Spring Boot에서 제공하는 어노테이션만으로도 복잡한 캐싱 로직을 비즈니스 로직과 분리하여 깔끔하게 처리할 수 있었음.
다만 캐싱성격이 데이터가 있을 경우 빠르지만, 캐싱된 데이터가 없을경우(캐싱미스) 조금의 지연 발생하는 것을 알았다.
서비스의 성격에 따라 적절한 TTL과 Evict 정책을 세우고 캐싱 히트, 캐싱 미스를 잘 확인해서 캐싱 전략을 세워야 겠다.

profile
응집력있는 시간을 보내기 위한 블로그

0개의 댓글