
쇼핑몰의 '인기 상품 TOP 10'이나 게임의 '실시간 랭킹'처럼 순위를 매겨 보여주는 기능은 사용자 경험에 매우 중요합니다. 하지만 이를 RDB(관계형 데이터베이스)만으로 구현하려면 성능 고민이 깊어질 수밖에 없습니다. 왜 리더보드에서 Redis의 Sorted Set이 좋은지 알아봅니다.
이커머스 서비스에서 '가장 많이 팔린 물품'을 조회한다고 가정해 봅시다. 보통은 아래와 같은 과정을 거칩니다.
Item 테이블과 Order 테이블을 JOIN하고, SUM이나 COUNT 같은 집계 함수를 사용해야 합니다.ORDER BY)하는 과정에서 DB 부하가 급증합니다.sales_count 컬럼을 추가해도, 주문이 들어올 때마다 빈번하게 UPDATE가 발생하므로 쓰기 성능과 데이터 정합성 문제가 생길 수 있습니다.Redis의 Sorted Set은 값(Member)과 점수(Score)를 쌍으로 저장하며, 저장과 동시에 점수 순으로 데이터를 정렬합니다.
객체 데이터(ItemDto)를 JSON 형태로 저장하기 위해 Serializer를 설정한 Bean을 등록합니다.
@Bean
public RedisTemplate<String, ItemDto> rankTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, ItemDto> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.json()); // JSON 직렬화
return template;
}
RedisTemplate에서 Sorted Set을 조작하기 위해서는 ZSetOperations 인터페이스가 필요합니다. 생성자를 통해 이를 주입받아 사용합니다.
@Slf4j
@Service
public class ItemService {
private final ItemRepository itemRepository;
private final OrderRepository orderRepository;
private final ZSetOperations<String, ItemDto> rankOps;
public ItemService(
ItemRepository itemRepository,
OrderRepository orderRepository,
RedisTemplate<String, ItemDto> rankTemplate
) {
this.itemRepository = itemRepository;
this.orderRepository = orderRepository;
this.rankOps = rankTemplate.opsForZSet(); // ZSetOperations 초기화
}
public void purchase(Long id) {
Item item = itemRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
orderRepository.save(ItemOrder.builder()
.item(item)
.count(1)
.build());
// ZINCRBY 명령과 동일: 해당 아이템의 점수를 1 증가시킴
rankOps.incrementScore("soldRanks", ItemDto.fromEntity(item), 1);
}
public List<ItemDto> getMostSold() {
// 점수가 높은 순서대로 10개 조회 (ZREVRANGE)
Set<ItemDto> ranks = rankOps.reverseRange("soldRanks", 0, 9);
if (ranks == null) return Collections.emptyList();
return ranks.stream().toList();
}
}
사용자가 상품을 구매할 때마다 incrementScore(ZINCRBY)를 호출하여 해당 아이템의 점수를 1점씩 올립니다.
public void purchase(Long id) {
Item item = itemRepository.findById(id).orElseThrow();
// 주문 내역 저장 (RDB)
orderRepository.save(ItemOrder.builder().item(item).count(1).build());
// 리더보드 점수 업데이트 (Redis)
// 데이터가 없으면 새로 추가하고, 있으면 기존 점수에 누적합니다.
rankOps.incrementScore("soldRanks", ItemDto.fromEntity(item), 1);
}
Sorted Set은 기본적으로 오름차순 정렬이므로, 내림차순으로 가져오기 위해 reverseRange를 사용합니다.
public List<ItemDto> getMostSold() {
// 0위부터 9위까지 내림차순 조회
Set<ItemDto> ranks = rankOps.reverseRange("soldRanks", 0, 9);
if (ranks == null) return Collections.emptyList();
return ranks.stream().toList();
}
Redis의 Sorted Set을 활용하면 성능 최적화는 물론, 구현 난이도까지 획기적으로 낮출 수 있습니다. 단순히 랭킹 기능뿐만 아니라 실시간 검색어, 대기열 시스템 등 다양한 분야에서 활용할 수 있습니다!