WIL (랭킹 시스템 설계: Redis ZSET부터 Spring Batch + MV까지)

KwonMoYang·2026년 4월 18일

이번 주 주제

상품 랭킹 시스템을 일간 / 주간 / 월간 세 가지 주기로 나눠 구현했다.
핵심은 "조회 주기에 따라 적절한 저장소를 고르는 것" 이었다.

주기저장소갱신 방식
일간(실시간)Redis ZSETKafka 이벤트 기반 ZINCRBY
주간/월간RDB Materialized ViewSpring Batch 집계

1. Redis ZSET으로 실시간 랭킹 만들기

왜 ZSET인가?

랭킹은 결국 "점수 기반 정렬 + 순위 조회" 문제다.
ZSET은 내부적으로 skip list + hash를 써서 score 정렬 삽입/조회가 O(log N)으로 보장되고,
ZINCRBY는 원자적이라 동시성 고민이 크게 줄어든다.

가중치 설계

이벤트마다 중요도가 다르기 때문에 단순 카운트가 아니라 가중치를 뒀다.

  • 조회 : 0.1
  • 좋아요 : 0.2
  • 주문 : 0.7 × log10(quantity + 1)

주문에 log10을 씌운 건, 대량 주문 1건이 모든 지표를 압도하는 것을 막기 위함이다.
이걸 안 넣으면 특정 상품이 랭킹을 독점해서 "랭킹"의 의미가 사라진다.

키 전략

ranking:20260418  ← 날짜별 ZSET

일별로 키를 쪼개면 "오늘의 랭킹"만 계산/조회하면 되고,
TTL만 걸어두면 과거 데이터 정리도 자동화된다.

이벤트 드리븐 업데이트

OrderPlacedEvent → Kafka → OrderEventConsumer → ZINCRBY
CatalogViewEvent → Kafka → CatalogEventConsumer → ZINCRBY

주문/조회 API 응답 속도가 랭킹 계산에 영향받지 않도록 Kafka로 분리했다.
API는 이벤트만 던지고 끝. 랭킹은 streamer 모듈이 따로 받아서 반영한다.


2. 주간/월간은 왜 Redis가 아닐까?

처음엔 "ZSET 키를 ranking:week:2026W16처럼 주 단위로 만들면 되지 않나?" 생각했다.
근데 몇 가지 문제가 있다.

  1. Redis 메모리 비용 - 주간 누적 데이터는 훨씬 크다. 상품 수 × 주 수.
  2. 이벤트 재계산 불가 - 과거 데이터 보정이 필요하면 ZSET은 고칠 방법이 없다.
  3. 복잡한 집계 불가 - "카테고리별 주간 Top 10" 같은 쿼리는 RDB가 훨씬 자연스럽다.

그래서 일간은 Redis, 주간/월간은 MV(Materialized View) + Spring Batch 조합으로 갔다.


3. Spring Batch로 MV 갱신하기

Reader → Processor → Writer 패턴

ProductMetricsDaily (원천)
   ↓ Reader (chunk 단위로 읽기)
   ↓ Processor (점수 계산)
   ↓ Writer (MvProductRankWeekly에 bulk insert)

Row-by-row가 아니라 청크 단위로 처리해서 처리량 확보.

버전 기반 무중단 갱신

이번에 가장 크게 배운 패턴.

1. 새 버전으로 MV 테이블에 write  (version = N+1)
2. ActivateVersionTasklet 실행     (active_version = N+1)
3. ClearOldVersionTasklet 실행     (version = N 삭제)

배치 도는 중에도 API는 이전 버전(N)을 읽기 때문에 조회 중단이 없다.
활성 버전을 원자적으로 스왑하는 순간에만 새 데이터가 노출된다.
"blue-green 배포를 데이터에 적용한 느낌" 이 딱 맞는 비유였다.


4. 한 줄 회고

"저장소는 접근 패턴이 결정한다."

실시간 증분 + 빠른 순위 조회 → Redis ZSET.
집계/히스토리/복잡 쿼리 → RDB + 배치.
둘을 하나로 통일하려고 하지 말고, 역할에 맞는 도구를 각자 쓰게 두는 것이 이번 주의 가장 큰 교훈이었다.


profile
Dot Your moment.

0개의 댓글