개발노트

25.03.08 캐싱 전략 과 예시 본문

DataBase

25.03.08 캐싱 전략 과 예시

ddong-kka 2025. 3. 8. 00:12

Write-Through Cache

애플리케이션이 데이터를 변경하면 즉시 캐시와 데이터베이스에 동시에 저장하는 방식

캐시가 항상 최신의 데이터를 유지하도록한다

 

장점

  • 데이터 일관성 유지
    • 캐시와 데이터베이스의 데이터가 항상 동일함 
  • 빠른 읽기 속도 제공
    • 데이터가 항상 캐시에 저장되어 있어 읽기 성능이 향상됨

단점

  • 쓰기 성능 저하
    • 모든 쓰기 연산이 캐시와 데이터베이스에 동시에 반영되므로 속도가 느려질 수 있음
  • 불필요한 캐싱 가능성
    • 자주 조회되지 않는 데이터도 캐시에 저장될 수 있어 메모리 낭비 가능

 

사용 예시

  • 사용자 프로필 정보
    • 사용자의 기본 정보는 자주 읽히지만 자주 변경되지 않음
  • 상품 정보 저장
    • 상품이 업데이트될 때마다 캐시와 DB를 동시에 갱신하여 최신 상태 유지

 

변경이 자주 되지않거나 항상 최신 상태를 유지해야하는 경우에 사용하는 전략

 

 // Write-Through
    @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()));
    }

 

Write-Behind ( Write-Back) Cache

데이터를 변경할 때만 캐시에만 저장하고, 일정 시간이 지나거나 특정 조건이 만족될 때 배치 처리로 데이터베이스에 반영하는 방식이다.

 

장점

  • 쓰기 성능 상향
    • 모든 쓰기 연산이 캐시에만 저장되므로 빠르게 처리할 수 있음 
  • 대량 저장 가능
    • 일정 주기로 배치 저장하면 트랙잭션 횟수가 줄어 성능 최적화 가능

단점

  • 데이터 유실 위험
    • 배치 처리 전에 장애가 발생하면 데이터가 손실될 가능성이 있음
  • 일관성 문제
    • 데이터베이스와 캐시 간의 동기화가 지연될 수 있어 최신 데이터 조회가 어려울 수 있음

사용 예시

  • 주문 처리 시스템
    • 주문 데이터를 바로 DB에 저장하는 대신 캐시에 보관 후 배치로 저장
  • 로그 수집 시스템
    • 로그를 실시간으로 DB에 저장하지 않고 캐시에 모아 일정 주기로 반영

 

// Write-Behind
    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
        );
    }

    // Write-Behind
    @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"));

        // saveAll 하기전에 nullable 검사 추가해야함
        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");
    }

 

Read-Through Cache

데이터를 읽을 떄 캐시에 데이터가 없으면 자동으로 데이터베이스에서 가져와 캐시에 저장하는 방식

 

장점

  • 자동 캐싱
    • 캐시에 없는 데이터만 데이트베이스에서 조회하여 저장하므로 관리가 편함 
  • 일관성 유지 가능
    • 캐시에서 만료되면 다시 데이터베이스에서 가져오므로 일관성을 유지할 수 있

단점

  • 초기 응답 속도 지연
    • 캐시에 데이터가 없을 경우 DB를 조회해야 하므로 첫 번째 요청은 느릴 수 있음
  • 캐시 관리 필요
    • 만료 정책을 적절히 설정하지 않으면 불필요한 데이터가 계속 남아 있을 수 있음

 

사용 예시

  • 상품 상세 조회
    • 사용자가 상품을 조회할 때 캐시에 없으면 DB에서 가져와 저장 후 반환
  • 자주 조회되는 설정값 저장
    • 애플리케이션에서 자주 사용하는 설정 값을 DB에서 조회 후 캐시에 저장

 

 

// Read-Through (or Cache-Aside)
    public List<ItemDto> getMostSold() {
        Set<ItemDto> ranks = rankOps.reverseRange("soldRanks", 0, 9);
        if (ranks == null) return Collections.emptyList();
        return ranks.stream().toList();
    }

Cache-Aside (Lazy Loading) Cache

 

애플리케이션이 데이터를 요청할 때 먼저 캐시를 조회하고, 없으면 데이터베이스에서 가져와 캐시에 저장하는 방식

Read-Through와 유사하지만, 캐시가 데이터를 자동으로 불러오는 것이 아니라 애플리케이션이 직접 로직을 구현해야한다.

 

장점

  • 효율적인 메모리 사용
    • 실제로 필요한 데이터만 캐시에 저장되므로 불필요한데이터 캐싱을 방지할 수 있음 
  • 데이터 일관성 보장 가능
    • 캐시 만료 정책을 적절히 설정하면 오래된 데이터를 방지할 수 있음

단점

  • 초기 조회 성능 저하
    • 캐시에 없는 데이터를 조회할 때마다 DB를 참조해야 하므로 첫 번째 조회가 느릴 수 있음
  • 캐시 유지 비용 증가
    • 캐시 만료 정책을 적절히 설정하지 않으면 불필요한 데이터가 계속 남아 있을 수 있음

사용 예시

  • 게시글 조회 시스템
    • 자주 조회되는 게시글을 캐시에서 먼저 확인하고, 없으면 DB에서 가져와 캐싱
  • 유저 세션 정보 저장
    • 로그인한 유저의 세션 정보를 캐시에 저장하고, 없으면 DB에서 불러와 캐싱

 

// Cache-Aside
    @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));
    }

    // Cache-Aside
    @Cacheable(cacheNames = "itemAllCache", key = "methodName")
    public List<ItemDto> readAll() {
        return itemRepository.findAll()
                .stream()
                .map(ItemDto::fromEntity)
                .toList();
    }

 

Write-Around Cache

데이터를 변경할 때 캐시에 저장하지 않고 직접 데이터베이스에 저장하는 방식

이후 데이터가 요청될 때 캐시에 없는 경우 데이터베이스에서 가져와 캐시에 저장한다.

 

장점

  • 불필요한 캐싱 방지
    • 자주 사용되지 않는 데이터가 캐시에 저장되지 않으므로 메모리 낭비가 적음.
  • 쓰기 성능 최적화
    • 데이터를 변경할 때 캐시에 쓰는 부담이 없으므로 빠름.

단점

  • 초기 조회 성능 저하
    • 캐시에 없는 데이터를 조회할 때마다 DB를 참조해야 하므로 첫 번째 조회가 느릴 수 있음
  • 자주 변경되는 데이터에 적합하지 않음
    • 데이터가 자주 변경되면 캐시가 자주 비워져 다시 DB에서 읽어오는 부담이 발생

사용 예시

  • 로그 저장 시스템
    • 로그 데이터는 거의 조회되지 않으므로 바로 DB에 저장하고 필요할 때만 캐싱