본문 바로가기
IT 서비스/주문 관리 플랫폼

[ 리뷰 기능 ] Redis+Scheduler를 활용한 매장 평균 별점 기능 고도화하기 - 2

by agong이 2025. 2. 21.

저번 글이 궁금하시다면👆https://agongstory.tistory.com/39

 

[ 리뷰 기능 ] 매장 평균 별점 기능 고도화하기 - 1

📜서론단순히 매장 목록을 조회할때 해당 매장의 평균 별점까지 함께 제공하려면 성능 이슈가 발생할 수 있습니다. 이번 글에서는 어떤 방법들이 있고 제가 선택한 해결 방법을 작성하려고 합

agongstory.tistory.com

 

 

 

기본설정

1. build.gradle에 Redis의존성 추가

    implementation 'org.springframework.boot:spring-boot-starter-data-redis' //Redis

 

2. application.yml에 redis 정보 추가

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: {password}

 

3. RedisConfig 설정

@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Value("${spring.data.redis.password}")
    private String password;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
        config.setPassword(password);
        return new LettuceConnectionFactory(config);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
    
    @Bean
    public HashOperations<String, String, String> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }
}

 

 

💡HashOpretion : Redis 해시(Hash) 자료구조 활용
Redis의 해시 자료구조는 하나의 키에 여러 필드-값 쌍을 저장할 수 있습니다. 이를 통해 storeId를 키로 사용하고, 그 아래 rating과 reviewCount를 필드로 저장하기 위해 사용하였습니다.

 

 

4. 스케쥴러 설정 등록

스케쥴러를 통해 redis에 일정주기로 매장들의 평균 별점을 계산해줄것이기 때문에 등록합니다.

@Configuration
@EnableScheduling
public class SchedulerConfig {
    // 스케줄러 설정이 활성화
}

 

이렇게 하면 일단 기본 설정은 완료되었습니다.

 

5. 서비스 로직 작성

전체 로직은 다음과 같습니다. 부분적으로 살펴보자면 먼저 모든 매장을 조회한 후 해당 매장정보를 바탕으로 평균 별점과 리뷰 개수를 @Query를 통해 DB에서 조회합니다. 

// 매장들의 평균 별점과 리뷰 개수를 조회
@Query("SELECT new com.sparta.bedelivery.dto.StoreRatingInfo(r.store.id, COALESCE(AVG(r.rating), 0), COUNT(r)) " +
        "FROM Review r " +
        "WHERE r.store.id IN :storeIds " +
        "GROUP BY r.store.id")
List<StoreRatingInfo> findAverageRatingAndReviewCountByStoreIds(@Param("storeIds") List<UUID> storeIds);

 

 

그 후, Redis에 key값과 저장하고자 하는 데이터(라뷰 개수, 평균 별점)을 다음과 같이 넣어줍니다.

// Redis에 저장
for (StoreRatingInfo info : results) {
    UUID storeId = info.getStoreId();
    String avgRating = info.getAverageRating().toString(); // 소수점 첫째 자리까지만 표시
    Long reviewCount = info.getReviewCount();

    // Redis 해시에 저장할 데이터 맵 생성
    Map<String, String> ratingData = new HashMap<>();
    ratingData.put("rating", avgRating);
    ratingData.put("reviewCount", reviewCount.toString());

    // Redis 해시에 데이터 저장 (키: store:{storeId}:reviewInfo)
    String redisKey = "store:" + storeId + ":reviewInfo";
    hashOperations.putAll(redisKey, ratingData);
}
더보기

전체 코드는 다음과 같습니다.

@Transactional(readOnly = true)
public void updateStoreRatingsInRedis() {
    // 삭제되지 않은 매장 목록 조회
    List<Store> stores = storeRepository.findAllByDeleteAtIsNull();

    // Store 리스트에서 UUID 리스트로 변환
    List<UUID> storeIds = stores.stream()
            .map(Store::getId)
            .collect(Collectors.toList());

    // 평균 별점과 리뷰 개수 조회
    List<StoreRatingInfo> results = reviewRepository.findAverageRatingAndReviewCountByStoreIds(storeIds);

    // Redis에 저장
    for (StoreRatingInfo info : results) {
        UUID storeId = info.getStoreId();
        String avgRating = info.getAverageRating().toString();
        Long reviewCount = info.getReviewCount();

        // Redis 해시에 저장할 데이터 맵 생성
        Map<String, String> ratingData = new HashMap<>();
        ratingData.put("rating", avgRating);
        ratingData.put("reviewCount", reviewCount.toString());

        // Redis 해시에 데이터 저장 (키: store:{storeId}:reviewInfo)
        String redisKey = "store:" + storeId + ":reviewInfo";
        hashOperations.putAll(redisKey, ratingData);
    }

}

6. Redis에서 데이터를 조회하는 메서드 구현

// 매장의 리뷰 개수와 평균 별점을 조회하는 메서드
public Map<String, String> getStoreReviewInfo(UUID storeId) {
    String redisKey = "store:" + storeId + ":reviewInfo";
    return hashOperations.entries(redisKey);
}

바로 테스트 해보기 위해 Redis에서 key값을 통해 데이터를 조회해보는 메서드를 구현하였습니다.

 

7. 스케쥴러 등록 

해당 메서드를 스케쥴러로 등록합니다.

@Service
@AllArgsConstructor
public class StoreRatingScheduler {
    private final ReviewService reviewService;

    // todo - 1시간(3600000ms)/60 = 1분마다 실행 - 테스트 목적
    @Scheduled(fixedRate = 3600000/60)
    public void updateStoreRatingsJob() {
        System.out.println("[스케줄러 실행] 매장 평균 별점 및 리뷰 개수를 업데이트합니다.");
        reviewService.updateStoreRatingsInRedis();
        System.out.println("[완료] Redis에 평균 별점과 리뷰 개수 업데이트 완료!");
        System.out.println(reviewService.getStoreReviewInfo(UUID.fromString("422bbca4-b4ef-4350-acfe-bf2789dd95e2")));
    }
}

 

❗이렇게 설정한뒤 스프링을 실행시키면 되야되는데?

Reason: Validation failed for query for method public abstract java.util.List com.sparta.bedelivery.repository.ReviewRepository.findAverageRatingAndReviewCountByStoreIds(java.util.List)

 

@Getter
@AllArgsConstructor
public class StoreRatingInfo {
    private UUID storeId;
    private Double averageRating;
    private Long reviewCount;

}
// 매장들의 평균 별점과 리뷰 개수를 조회
@Query("SELECT new com.sparta.bedelivery.dto.StoreRatingInfo(r.store.id, COALESCE(AVG(r.rating), 0.0), COUNT(r)) " +
        "FROM Review r " +
        "WHERE r.store.id IN :storeIds " +
        "GROUP BY r.store.id")
List<StoreRatingInfo> findAverageRatingAndReviewCountByStoreIds(@Param("storeIds") List<UUID> storeIds);

 

찾아보니 기본적으로 데이터베이스에서 제공하는 AVG는 Double을 return하도록 되어있습니다.

하지만 저의 경우 rating은 정수값으로 한정했기 때문에 스프링에서는 Integer로 인식하였습니다.

그렇다보니 실제로 계산된 평균값은 double이지만 StoreRatingInfo에서는 Integer값에 저장하다 보니 타입 불일치로 오류가 발생했습니다.

이를 해결하기 위해 StoreRatingInfo의 rating 타입을 Dobule로 변경하였습니다.

실제 intellij에서는 raiting타입이 Integer기 때문에 AVG(r.rating)이 Integer값을 return한다고 생각하여 오류라고 인식하여 다음과 같이 표시됩니다. 하지만 실제 실행시에는 문제가 없이 정상 실행됩니다ㅠㅠ(이래서 쿼리 DSL을 사용하는건가?)

 

 

😊정리

Scheduler + Redis를 활용하여 평균 별점과 리뷰 개수를 Redis에 저장하고 이를 조회하는 기능을 구현하였습니다.

스케줄러를 처음 사용해보면서 실제 활용 방법을 익혔고, 기존에는 캐싱 용도로만 사용했던 Redis를 인메모리 DB로 활용하며 Redis에 대한 이해도를 더욱 높일 수 있었습니다.

댓글