1. 캐시를 사용하는 상황
- 쓰기보다 조회가 많이 일어나는 상황
- 평균 평점, 총 리뷰 수에 캐시를 사용하기 때문에 쓰기 연산보다 읽기 연산이 많이 발생
2. 쓰기 정책 : Write Through 전략은?

- 캐시와 백업 저장소에 업데이트를 같이 하여 데이터 일관성을 유지할 수 있어서 안정적
- 쓰기 작업이 많은 시스템이라면 눈에 띄는 지연을 유발
3. 쓰기 정책 : Write Back 전략은?
Write-Back은 우선 캐시 메모리에만 데이터를 Write 하여 사용하다가 캐시 메모리가 새로운 데이터 블록으로 교체되는 때에 (다른 Tag를 가진 데이터가 캐시 블록에 할당될 때) 데이터를 주기억장치에도 저장하는 정책
캐시 메모리에 있는 데이터를 여러 번 Overwrite하여 사용한 다음, 캐시 메모리가 해제되는 때에 메인 메모리에 데이터를 업데이트하는 정책
- 장점 : Write-Through 보다 훨씬 처리 속도가 빠름
- 단점 : 속도는 빠르지만 구현하기 어렵고 캐시 메모리에만 업데이트하고 메모리에는 바로 업데이트하지 않기 때문에 캐시 일관성이 유지되기가 힘들어 종종 캐시와 메모리가 서로 값이 다른 경우가 발생할 때가 있음.
빠른 서비스를 요구하는 상황에는 Write Back을 사용하는 것이 좋음.
참고 : https://shuu.tistory.com/49
4. 캐시에 저장한 후 스케줄링을 통하여 평균 평점 계산 하기
영화 조회 및 리뷰 서비스(https://github.com/chanmin-00/SpringProject_WatchWithMe)에서 리뷰를 작성하고 각 영화 리뷰 평점을 Redis 캐시에 저장한 후 스케줄링을 통하여 5분마다 Redis 캐시를 조회한 후 영화의 평균 평점 계산하여 데이터베이스에 저장하기
Write Back 전략을 사용하여 구현
4.1 Redis Repository 및 Service 생성

- Redis 디렉토리에 Repository 및 Service 생성
@Repository
@RequiredArgsConstructor
public class ReviewCacheRepositoryImpl implements ReviewCacheRepository{
private final RedisTemplate<String, String> redisTemplate;
private static final String REVIEW_KEY_PREFIX = "reviews : ";
private static final String CHANGED_MOVIES_KEY = "changed_movies";
@Override
public void saveRating(String movieId, String value) {
redisTemplate.opsForList().rightPush(REVIEW_KEY_PREFIX + movieId, value);
}
@Override
public void saveChangedMovie(String movieId) {
redisTemplate.opsForSet().add(CHANGED_MOVIES_KEY, movieId);
}
@Override
public List<String> getRatingList(String movieId) {
return redisTemplate.opsForList().range(REVIEW_KEY_PREFIX + movieId, 0, -1);
}
@Override
public Set<String> getChangedMovieList() {
return redisTemplate.opsForSet().members(CHANGED_MOVIES_KEY);
}
@Override
public void deleteRatingList(String movieId) {
redisTemplate.delete(REVIEW_KEY_PREFIX + movieId);
}
@Override
public void deleteChangedMovie() {
redisTemplate.delete(CHANGED_MOVIES_KEY);
}
}
- Redis에 영화별 리뷰에 대한 평점을 List 형태로 저장
- Redis에 리뷰가 작성된 영화 목록을 Set 형태로 저장
@Service
@RequiredArgsConstructor
@Transactional
public class ReviewCacheServiceImpl implements ReviewCacheService{
private final ReviewCacheRepository reviewCacheRepository;
@Override
@Transactional
public void saveRating(String movieId, String value) {
reviewCacheRepository.saveRating(movieId, value);
reviewCacheRepository.saveChangedMovie(movieId);
}
@Override
public List<String> getRatingList(String movieId) {
return reviewCacheRepository.getRatingList(movieId);
}
@Override
public Set<String> getChangedMovieList() {
return reviewCacheRepository.getChangedMovieList();
}
@Override
@Transactional
public void deleteRatingList(String movieId) {
reviewCacheRepository.deleteRatingList(movieId);
}
@Override
@Transactional
public void deleteChangedMovie() {
reviewCacheRepository.deleteChangedMovie();
}
}
4.2 리뷰 작성이 진행 될 때 Redis에 리뷰 평점 저장하기
public Long write(WriteReviewRequestDto writeReviewRequestDto){
String email = writeReviewRequestDto.email();
String reviewText = writeReviewRequestDto.reviewText();
Double memberRating = writeReviewRequestDto.memberRating();
String memberRatingGenre = writeReviewRequestDto.memberRatingGenre();
Long movieId = writeReviewRequestDto.movieId();
Member member = memberRepository.findByEmail(email).orElse(null);
if (member == null)
throw new GlobalException(GlobalErrorCode._BAD_REQUEST);
Movie movie = movieRepository.findById(movieId).orElse(null);
if (movie == null)
throw new GlobalException(GlobalErrorCode._BAD_REQUEST);
Review review = Review.createReview(reviewText, memberRating, memberRatingGenre);
review.setMember(member);
review.setMovie(movie);
reviewRepository.save(review);
reviewCacheService.saveRating(movieId.toString(), memberRating.toString());
return review.getReviewId();
}
reviewCacheService.saveRating(movieId.toString(), memberRating.toString());
- ReviewService를 통해 리뷰가 작성될 때 ReviewCacheService의 saveRating 메소드를 호출하여 Redis에서 평점 저장
4.3 스케줄링을 통하여 리뷰 평균 평점 계산하여 DB에 저장하기
@Scheduled(initialDelay=1000, fixedDelay = 60000 *3) // 3분마다 실행
@Transactional
public void calculateAverageReviewRating() {
Set<String> changedMovieList = reviewCacheService.getChangedMovieList();
if (changedMovieList != null) {
for (String movieIdStr : changedMovieList) {
Long movieId = Long.parseLong(movieIdStr);
List<String> ratingList = reviewCacheService.getRatingList(movieIdStr);
if (ratingList != null && !ratingList.isEmpty()){
Movie movie = movieService.getMovie(movieId);
double currentRating = (movie.getUserRating() != null) ? movie.getUserRating() : 0.0;
int reviewCount = movieService.getReviewList(movieId).size();
double newSum = ratingList.stream()
.mapToDouble(Double::parseDouble)
.sum() + currentRating * (reviewCount - ratingList.size());
movie.setUserRating(newSum / reviewCount);
reviewCacheService.deleteRatingList(movieIdStr);
}
}
reviewCacheService.deleteChangedMovie();
}
log.info("평점 업데이트 완료");
}
- 3분 간격으로 스케줄링
- Redis에서 리뷰가 작성된 영화를 조회한 후, 각 타겟 영화의 리뷰 평점 조회
- 각 영화별 Redis에서 조회된 리뷰 평점과 이미 저장된 평점을 합하여 최종적으로 계산된 최신 평균 평점을 DB에 저장
- 계산 후 Redis 캐시 삭제
'Dev > SpringBoot' 카테고리의 다른 글
카카오 로그인 구현하기(1) - 카카오 로그인 (0) | 2025.02.10 |
---|---|
SpotlessApply 및 Pre-Commit (0) | 2025.02.10 |
SpringBoot에서 Redis 연동하기 (0) | 2025.02.10 |
전역 예외 처리 (0) | 2025.02.10 |
주기적인 작업 자동화 구축 (0) | 2025.02.10 |