Database/Redis

Spring Boot Redis 캐싱 전략

최-코드 2024. 8. 7. 11:27

상황 : 게시글 좋아요를 구현할 때 많은 반복이 발생하는 좋아요에 대해 배치 insert를 하는 방법이 없을까 고민이 들었다.

 

해결방안 : redis의 캐싱 전략을 이용하자.

 

배경지식 : cache hit, cache miss 모두 클라이언트가 조회를 요청할 때의 경우이다.

 

캐싱 전략 

  • 웹 서비스 환경에서 서버의 성능 향상을 기대할 수 있는 기술이다.
  • 메모리를 사용하기 때문에 요청이 오면 DB보다 훨씬 빠르게 응답할 수 있다.
  • 메모리는 작은 용량이기 때문에 데이터의 종류와 양, 제거 시기에 대해 전략을 세워야 할 필요가 있다.
  • 데이터 불일치 문제가 생길 수 있다. 따라서 적절한 캐시 읽기 전략과 캐시 쓰기 전략을 통해, 캐시와 DB간의 데이터 불일치 문제를 극복하면서도 빠른 성능을 잃지 않아야 한다.

캐시 읽기 전략 

  • Look Aside(Cache Aside)
    • 데이터를 찾을 때 캐시에 저장된 데이터가 있는지 우선적으로 확인하는 전략이다. 만약 cache miss이면 DB에서 조회한다.
    • 캐시와 DB가 분리되어 가용되기 때문에 만일 redis가 다운되더라도 DB에서 데이터를 가져올 수 있어 서비스 자체에는 문제 가없다.
    • redis에 데이터가 없으면 DB에서 가져와야 하므로 한번에 한 건만 가져오는 서비스보다 여러 건을 한 번에 가져오는 서비스에 적합하다. 
    • read through와 달리 필요한 데이터에 대해서만 캐시에 저장하므로 메모리 효율이 좋다.
    • cache warming을 통해 초기에 cache miss를 피할 수 있다. cache warming이란 미리 cache로 db의 데이터를 삽입하는 작업을 의미한다. 메모리 용량에 의해 결국 오래된 데이터는 expire되는데, 그러면 또 다시 db에 부하가 올 수 있으므로 TTL을 잘 조정할 필요가 있다.
  • Read Through
    • 캐시에서만 데이터를 조회하는 방법이다.
    • look aside의 경우 캐시에 저장할 때 비동기로 처리한 후 db에서 가져온 값을 반환해주면 되지만, 이 읽기 전략의 경우 무조건 캐시에 저장한 후 캐시에서 반환해야 하므로 상대적으로 조회 속도가 느리다.
    • 캐시에만 전적으로 의존하므로 redis가 다운될 경우 서비스 이용에 차질이 생긴다.
    • db에서 캐시로 데이터 불러오는 것을 라이브러리 또는 캐시 제공자에 의해 자동으로 수행되므로 애플리케이션에서 코드를 줄일 수 있으며, 직접적인 db에 접근을 최소화할 수 있다.
두 읽기 전략의 큰 차이점은 직접적으로 db를 읽는 주체가 누군지이다.

 

캐시 쓰기 전략

  • Write Back(Write Behind)
    • 애플리케이션이 데이터를 우선적으로 캐시에 저장한 후, 곧바로 클라이언트의 반환하며 나중에 쌓인 데이터를 한 번에 DB에 저장하는 방식이다.
    • 캐시에 모은 데이터를 배치 작업을 통해 db에 저장한다. 따라서 쓰기 작업에 대해 비용을 줄일 수 있다.
    • write가 빈번하면서 read 하는데에 많은 양의 resource가 소모되는 서비스에 적합하다.
    • 캐시에서 오류가 발생하면 데이터가 영구 소실된다.
    • 반대로 db에서 장애가 발생하여도 캐시를 통해 서비스를 제공할 수 있다.
  • Write Through
    • db와 캐시에 동시에 데이터를 저장한다.
    • write back과 달리 데이터를 저장할 때 먼저 캐시에 저장한 다음 바로 db에 저장한다.
    • read through과 같이 저장이 자동으로 수행되는 방식이다.
    • db와 캐시가 항상 동기화되어 있으므로 캐시는 항상 최신 상태로 유지된다.
    • 데이터 소실이 발생하면 안 되는 상황에서 적합하다.
    • 매 요청마다 두 번의 write가 발생하므로 빈번한 생성, 수정이 발생하면 성능적으로 좋지 않다.
write back과 write through 모두 자주 사용하지 않는 데이터면 메모리 낭비가 되므로 꼭 TTL을 설정해야 한다.
  • Write Around
    • write through보다 훨씬 빠르다.
    • 모든 데이터는 db에 저장한다. (캐시 갱신 X) -> 데이터 불일치 발생
    • miss가 발생할 때 db에서 가져와 캐시에 데이터를 저장한 후 반환한다.
    • 데이터가 수정, 삭제될 때마다 캐시 또한 삭제하거나 TTL을 짧게 설정하는 식으로 불일치를 해소해야 한다.
    • 조회를 잘 안 하는 경우에 적합하다.

 

캐시 읽기 + 쓰기 추천 조합

  • Look Aside + Write Around : 가장 일반적으로 자주 쓰이는 조합이다.
  • Read Through + Write Around : 항상 db에 쓰고 캐시에서 읽을 때 항상 db 읽어오므로 데이터 불일치 이슈에 대해 안전하다.
  • Read Through + Write Through : 데이터를 쓸 때 항상 캐시에 먼저 쓰므로, 읽어올 때도 최신 캐시 데이터 보장하고 데이터를 쓸 때 항상 db에도 저장하므로 데이터 불일치 이슈 해소할 수 있다.

사용한 캐싱 조합 : 모든 데이터에 대해 캐시를 쓸 필요는 없을 거 같아서 look aside를 골랐고, 데이터 생성이 빈번히 발생하므로 write back을 골랐다. 즉, look aside + write back 조합이다.

 

댓글 신고 사용 예시

@Transactional
    public void report(Long commentId) {
        redisService.addReportCount(commentId);
}

@Scheduled(fixedDelay = 3000000, initialDelay = 3000000)
public void reportCountToDB() {
    Set<String> allKeys = redisService.getAllKeys();
    List<Long> onlyKeys = allKeys.stream()
            .map(key -> Long.parseLong(key.split(":")[1])).toList();
    Map<Long, Integer> commentIdAndReportCount = redisService.getReportCount(onlyKeys);
    int keySize = onlyKeys.size();
    for (int i = 0; i < keySize; ) {
        List<CommentEntity> commentEntities = commentRepository.findAllById(
                onlyKeys.subList(i, i += i + 100 < keySize ? 100 : keySize % 100));
        commentRepository.batchUpdate(commentEntities, commentIdAndReportCount);
    }
}

좋아요를 캐싱하면서 발생한 문제점

  • 사용자가 좋아요한 목록을 불러올 때 페이지네이션을 해야하는데, 데이터 정합성이 안 맞는 문제가 발생했다. 그래서 redis에서 10개의 데이터를 가져오자니 딱 10개가 아니면 db에도 접근해야하며, 스케쥴링에 의해 db에 저장되면 나중에 데이터를 불러올 때 중복되는 데이터가 발생할 가능성이 있었다.
  • 좋아요를 한 번 누르면 더이상 누르지 못하도록 구현했다. 이 때 사용자가 좋아요를 눌렀는지 확인을 하려면 먼저 redis에 접근해야 하고, redis에 없으면 db에 접근을 해야한다. 또한 좋아요 수를 불러올 때 db와 redis 모두 접근해서 통합된 좋아요 수를 반환해야한다. 어처피 좋아요는 취소 없이 한 번만 되고, 벌크 연산을 위해 조회 때 두 데이터베이스에 2번씩 접근해야 하는 점에서 개운하지 않아 캐싱을 빼기로 했다. + 역정규화를 통해 likeCount를 따로 필드에 추가하였다.

깨달은점 : 좋아요에 대해 캐싱 전략을 이용하면 조회의 경우 많은 에러가 있는 것을 발견했다. 내 생각에는 조회를 할 필요 없는, 것보다는 실시간성이 필요없는 부분에 대해서만 캐싱을 사용하는 점이 좋을 거 같다.