캐시 (Cache)
Cache : CPU 내부 작은 영역, 빈번히 접근하는 데이터를 저장해두는 임시 기억 장치
영속성- 파일시스템
빠른 활용-메모리
많이 사용(휘발성)-캐시
캐싱
✅ 기본적으로 본래저장된곳 아니라 다른 곳에 저장하는 행위
접근 빈도가 높은 데이터 - 인메모리에 저장 →조회 속도 감소
ex) 웹브라우저에서 자주 바뀌지않는 이미지 등
💡 캐시를 확인하는 것도 결국 약간의 지연
캐시 미스일 때 결국 다시 원본 저장 공간을 확인해야 하기 때문에 시간이 더 늘어날 수 있음
캐싱 전략 (Caching Strategy)
데이터가 언제든 사라질 수 있고, 너무 크지 않게 관리 되어야함
캐시를 구현하고 사용할때는 해당 데이터가 얼마나 자주 사용될 것인가를 고려해야 함
또한 언제 캐시에 데이터를 저장하고, 언제 확인하는지에 대한 전략을 세워야 함
캐싱 전략 목표: 캐시 적중률↑ 누락율↓
1. Cache-aside (Lazy Loading)
1) 데이터 조회시 캐시를 먼저 확인함
2-1) 캐시에 데이터 있으면 캐시 데이터 (cache hit)
2-2) 없으면 원본에서 가져와 캐시에 저장 (cache miss)
- 최초 조회는 캐시에 데이터가 없기 때문에 상대적으로 오래 걸림
- 첫 번째 요청을 제외하고는 좋은 성능
- 데이터가 최신이라는 보장이 없음
2. Write-Through
1) 데이터 추가 시 캐시에 먼저 저장
2) 그 다음 원본에 저장
3) 조회 시 캐시만 읽으면 됨
- 최신 데이터 보장
- 자주 사용하지 않는 데이터도 캐시에 저장 됨
- 캐시에 항상 걸치기 때문에 조금 오래 걸림
3. Write-Behind
1) 데이터를 캐시에만 저장
2) 일정 주기로 원본 갱신
- 쓰기가 잦을 시 디비 부하 줄일 수 있음
- 원본 적용 전 문제 생기면 데이터 유실 문제 생김
@EnableCaching
@EnableCacing을 사용하려면 캐시를 관리하는 CacheManager의 구현체가 Bean으로 등록되어야 함
@Configuration
@EnableCaching
public class CacheConfig {
// Redis용 캐싱 매니저
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 설정 구성
// Redis를 이용해 Spring Cache를 사용할 때 Redis 관련 설정을 모아두는 클래스
RedisCacheConfiguration configuration = RedisCacheConfiguration
.defaultCacheConfig()
.disableCachingNullValues() // null을 캐싱하는지
.entryTtl(Duration.ofSeconds(100)) // 기본 캐시 유지 시간 (Time To Live)
.computePrefixWith(CacheKeyPrefix.simple()) // 캐시를 구분하는 접두사 설정
.serializeValuesWith( // 캐시에 저장할 값을 어떻게 직렬화/역직렬화 할것인지
RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java())
);
return RedisCacheManager
.builder(connectionFactory)
.cacheDefaults(configuration)
.build();
}
}
조회 (Cache-Aside)
@Cacheable : 이 메서드의 결과는 캐싱이 가능함, 데이터를 발견할 경우(Hit) 메서드 자체를 실행하지 않음
- cacheNames : 만들어질 캐시를 지칭하는 이름, 적용할 캐시 규칙을 지정하기 위한 이름
- key : 캐시 데이터를 구분하기 위해 활용하는 값
@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));
}
@Cacheable(cacheNames = "itemAllCache", key = "methodName")
public List<ItemDto> readAll() {
return itemRepository.findAll()
.stream()
.map(ItemDto::fromEntity)
.toList();
}
생성/수정 (Write Through)
@CachePut: 항상 메서드를 실행하고 결과를 캐싱함
- key를 "#result.id" (ItemDto.id)로 반환하여 readOne 메서드도 이 캐시를 활용할 수 있음
@CachePut(cacheNames = "itemCache", key = "#result.id")
public ItemDto create(ItemDto itemDto) {
Item item = Item.builder()
.name(itemDto.getName())
.description(itemDto.getDescription())
.price(itemDto.getPrice())
.build();
itemRepository.save(item);
return ItemDto.fromEntity(item);
}
삭제 정책
@CacheEvict: 주어진 정보를 바탕으로 저장된 캐시를 지워줌
@CachePut(cacheNames = "itemCache", key = "args[0]")
// 업데이트 시 readAll에 담겨있던 캐시를 지움 - 삭제 정책
@CacheEvict(cacheNames = "itemAllCache", allEntries = true)
public ItemDto update(Long id, ItemDto itemDto) {
Item item = itemRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
item.setName(itemDto.getName());
item.setDescription(itemDto.getDescription());
item.setPrice(itemDto.getPrice());
itemRepository.save(item);
return ItemDto.fromEntity(item);
}
@CacheEvict(cacheNames = {"itemAllCache", "itemCache"}, allEntries = true)
public void delete(Long id) {
itemRepository.deleteById(id);
}
생성/수정 (Write-Behind)
Write-Behind는 Annotation 기반으로 구현할 수 없음
public void purchase(ItemOrderDto dto) {
Item item = itemRepository.findById(dto.getItemId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
orderOps.rightPush("orderCache::behind", dto);
rankOps.incrementScore(
"soldRanks",
ItemDto.fromEntity(item),
1
);
}
@Transactional
@Scheduled(fixedRate = 20, timeUnit = TimeUnit.SECONDS)
public void insertOrders() {
boolean exists = Optional.ofNullable(orderTemplate.hasKey("orderCache::behind"))
.orElse(false);
if (!exists) {
log.info("no orders in cache");
return;
}
// 적재된 주문을 처리하기 위해 별도로 이름을 변경하기 위해
orderTemplate.rename("orderCache::behind", "orderCache::now");
log.info("saving {} orders to db", orderOps.size("orderCache::now"));
orderRepository.saveAll(orderOps.range("orderCache::now", 0, -1).stream()
.map(dto -> ItemOrder.builder()
.itemId(dto.getItemId())
.count(dto.getCount())
.build())
.toList());
orderTemplate.delete("orderCache::now");
}
'DB > Redis' 카테고리의 다른 글
[Redis] HttpSession / Session Clustering (0) | 2025.03.07 |
---|---|
[Redis] Spring에서 Redis (1) | 2025.03.07 |
[Redis] 타입 (1) | 2025.03.07 |
[Redis] Redis 설치 (0) | 2025.03.06 |
[Redis] Redis (0) | 2025.03.05 |