Redis의 ZSET 자료 구조를 활용해 실시간 상품 일간 랭킹 시스템을 구현하며 고민했던 설계 포인트를 정리합니다.
랭킹은 단순한 정렬 문제가 아니라, 시간 기준·집계 단위·안정성에 대한 선택의 연속이었습니다.
일간 랭킹의 기준은 크게 두 가지 방식으로 나눌 수 있습니다.
캘린더 기준
Sliding Window 기준 (최근 24시간)
처음에는 데이터 왜곡이 적고, 최근 24시간 기준이 더 합리적으로 보였습니다.
하지만 콜드 스타트 문제를 실제로 고민하고 일간 랭킹의 범위를 명확히 구분하기 위해,
이번 구현에서는 캘린더 기준(00:00 시작)의 일간 랭킹을 선택했습니다.
랭킹 시스템에서 중요한 질문 중 하나는 다음과 같습니다.
연속적으로 발생하는 이벤트를 어떤 시간 단위로 끊어 집계할 것인가?
1분 단위 집계
너무 큰 단위 집계
이 두 극단 사이에서,
노이즈는 완만하게 흡수하면서도 의미 있는 변화는 반영할 수 있는 단위로
10분 단위(time-bucket) 집계를 선택했습니다.
추후 10분 단위 데이터를 기반으로 시간 감쇠(Time Decay) 모델을 적용해
급상승 상품·브랜드 랭킹으로 확장할 수도 있습니다.
product_views:{productId}::{yyyyMMddHHmm}
product_likes:{productId}::{yyyyMMddHHmm}
product_sales:{productId}::{yyyyMMddHHmm}
이벤트는 eventTime 기준으로 처리되기 때문에,
“12:00에 배치가 실행되었다고 해서,
12:00까지 발생한 모든 이벤트가 이미 집계되었다고 보장할 수는 없다.”
이 문제를 해결하기 위해 변화량(delta) 기반 영속화 방식을 선택했습니다.
이벤트 발생 시
배치 실행 시
"product_likes:*" 와 같은 패턴으로 Redis SCAN 조회
redisTemplate.getAndDelete(key) 사용
DB에는 delta update 방식으로 누적 저장
이 방식으로,
집계된 지표를 기반으로, 상품별 인기도 점수를 계산해
일자 단위 ZSET에 저장합니다.
ranking:all:{yyyyMMdd}
VIEW_WEIGHT = 0.1
LIKE_WEIGHT = 0.2
ORDER_WEIGHT = 0.6
TTL_DAYS = 2
점수는 가중치 기반으로 계산되며,
ZSET의 score를 증가시키는 방식으로 반영합니다.
redisTemplate.opsForZSet()
.incrementScore(rankingKey, productId.toString(), score);
reverseRange(rankingKey, start, end)
reverseRank(rankingKey, productId)
score(rankingKey, productId)
zCard(rankingKey)
캘린더 기준 일간 랭킹의 가장 큰 문제는
00시 직후 랭킹 데이터가 거의 없다는 점입니다.
이를 완화하기 위해,
매일 23:50, 오늘의 랭킹 점수를 가중치 0.2로 감쇠하여
다음 날 랭킹 키에 미리 반영합니다.
랭킹 시스템을 구현하다 보면,
로그처럼 모든 데이터를 정확히 기록해야 할 것 같은 유혹을 받게 됩니다.
하지만 랭킹은 로그 시스템이 아니라,
“인기 측정기”가 아닌
“노출을 제어하는 시스템”
입니다.
추가로,
10분 단위 집계 데이터를 시간 감쇠(Time Decay) 모델에 적용해
‘지금 뜨는 상품(Trending)’ 랭킹을 계산하는 방식으로 확장하여 롱테일 문제를 해결해 볼 수 있을 것입니다.
이번 설계는
“완벽한 집계”보다는
사용자가 신뢰할 수 있는 랭킹 경험을 목표로 한 선택들의 결과였습니다.