실시간 상품 랭킹? Redis ZSET만 쓰면 끝인 줄 알았는데요

2025. 9. 12. 16:20·Study/Architecture

이커머스 프로젝트를 진행하면서 실시간 상품 랭킹을 설계하게 되었다.

사실 어지간한 이커머스에는 다 있는 기능이라 쉽게 끝날 거라 생각했지만… 역시나 고민할 부분이 정말 많았다.

 

 

 

왜 실시간 랭킹인가?

사용자가 상품을 조회하고, 좋아요를 누르고, 구매한다.

이때마다 바로바로 갱신되는 랭킹을 보여주고 싶었다.

기존에는 단순히 DB 테이블에 데이터를 쌓고 조회하는 방식이었다.

하지만 트래픽이 늘어난다고 생각하면 단순 집계 쿼리는 곧 심각한 병목으로 이어질 것 같았다.

따라서 실시간 반영 + 빠른 조회를 만족시킬 수 있는 Redis를 선택했다.

 

 

 

Redis ZSET

Redis의 Sorted Set (ZSET) 은 score 기반으로 자동 정렬되는 자료구조다.

랭킹도 점수를 기반으로 정렬하기 때문에 아주 적합하다고 생각했다.

 

ZSET 구조 특징

  • (member, score) 쌍을 score 기준으로 정렬된 상태로 유지
  • 삽입/수정: O(logN)
  • Top-N 조회: O(N)

 

주요 연산

  • ZADD key score member : score와 함께 member 저장 (중복 시 갱신)
  • ZREVRANGE key 0 N WITHSCORES : Top-N 조회
  • ZREVRANK key member : 특정 멤버의 순위 조회
  • ZSCORE key member : 특정 멤버의 점수 조회

 

ZSET 사용 시 이벤트 발생 시 점수 누적은 이렇게 할 수 있었다.

# 조회, 좋아요, 구매 이벤트
ZINCRBY ranking:20250912 1 product:1

조회는 다음처럼 할 수 있었다.

ZRANGE ranking:20250912 0 9 REV WITHSCORES

이렇게 작성할 경우 TOP 10 랭킹 목록을 조회할 수 있었다.

 

여기까지만 보면 끝난 것 같았지만..?

 

 

 

시간의 양자화: 며칠 전에 본 상품이 왜 오늘도 1등이지?

실시간이라고 해서 무작정 데이터를 누적하면 오래된 상품만 상위에 고정되는 롱테일 현상이 발생한다.

 

이를 막기 위해 날짜별로 key를 분리했다.

ranking:product:all:{yyyyMMdd}
  • ranking:20250911 → 2025년 9월 11일 랭킹
  • ranking:20250912 → 2025년 9월 12일 랭킹

 

또한 TTL은 2일로 설정했다.

너무 길게 가져가면 메모리 낭비가 심해지고, 너무 짧으면 비교/추세 분석이 어려워지기 때문에 2일 정도가 균형점이라고 생각해 설정했다.

📌 롱테일 현상

특정 상품이 한번 상위에 오르면 데이터가 무한히 누적되면서 계속 상위에 남는 문제.

 

 

 

가중치 설계: 조회만 많은데 1등이라고요?

처음엔 단순히 좋아요=1, 조회=1, 주문=1 로 점수를 올렸다.

그랬더니 조회수만 많은 상품들이 상위 랭킹을 차지했다.

좋아요나 조회도 중요하지만, 진짜 인기 상품은 결국 실제로 주문된 상품이라고 생각했다.

 

그래서 이벤트별로 가중치를 조정했다.

조회 = 1
좋아요 = 3
구매 = 6

 

하지만 점수가 지나치게 커지면 문제가 또 발생했다.

Redis ZSET의 score는 double 타입이기 때문에 값이 과도하게 커지면 정확성 손실 문제가 생길 수 있고 작은 변화가 무의미해질 수 있었다.

 

최종적으로는 좋아요:조회:주문 = 0.3 : 0.1 : 0.6 비율을 설정해 단순 노출이 아니라 실제 반응이 좋은 상품이 상위 랭킹에 오르도록 조정했다.

 

 

 

대량 데이터 처리: 초당 수만 건 이걸 Redis가 다요?

초기에는 이벤트가 들어올 때마다 곧바로 ZINCRBY를 호출했다.

하지만 운영을 가정하면 초당 수천~수만 건이 몰릴 수 있고, 그럴 경우 Redis CPU와 네트워크 모두에 심각한 부하가 생긴다.

그래서 Consumer 로직을 개편했다.

@KafkaListener(...)
public void consume(List<ConsumerRecord<String, KafkaMessage<?>>> records, Acknowledgment ack) {
    Map<Long, ProductMetricsCount> aggregate = new HashMap<>();

    for (ConsumerRecord<String, KafkaMessage<?>> record : records) {
        KafkaMessage<?> message = record.value();
        Map<String, Object> payload = (Map<String, Object>) message.payload();

        Long productId = getValue(payload, "productId", Long.class);
        Integer quantity = getValue(payload, "quantity", Integer.class);

        aggregate.computeIfAbsent(productId, id -> new ProductMetricsCount())
                .apply(message.type(), quantity != null ? quantity : 0);
    }

    if (!aggregate.isEmpty()) {
        productMetricsService.bulkUpdate(aggregate);
        productRankingCacheService.updateRanking(aggregate);
    }

    ack.acknowledge();
}

 

개선 포인트

  1. Consumer가 batch 메시지를 읽고
  2. productId 단위로 점수 집계
  3. 최종 집계 결과만 Redis에 반영

결과적으로 Redis 호출 횟수가 크게 줄어 부하도 완화될 수 있었다.

DB upsert 처리도 같은 전략을 적용했다.

 

 

 

콜드스타트: 랭킹 화면이 비어있다구요?

코드를 다 짜고 테스트를 돌렸는데 조회 API가 아무것도 반환하지 않았다.

당연함. 초기 데이터가 없었기 때문.

 

더미 데이터를 넣고 끝내려 했지만..?

문득 운영 환경에서는 날짜가 바뀌는 순간에도 같은 문제가 발생할 수 있다는 걸 깨달았다.

 

자정 이후 첫 사용자가 빈 화면을 마주한다면, 단순히 기능이 안 돌아가는 게 아니라 랭킹 자체에 대한 신뢰를 잃을 수 있다고 생각했다.

일단 대체해서 보여줄 데이터가 있으면 좋겠지? 그리고 아예 미리 데이터를 채워둘 수 있다면 더 좋을 것 같다.

이런 고민을 통해 두 가지 방법을 생각했다.

 

Fallback

전날 데이터도 유저의 실제 행동이므로 기본 신뢰도는 확보할 수 있다.

오늘 데이터가 비어 있으면 전날 데이터를 대신 보여준다.

 

Warm-up

전날 23:30에 전일 TOP100을 뽑아 다음날 key에 미리 적재한다.

단, 그대로 적재하면 또 롱테일 문제가 생기므로 일정 비율로 희석해 반영했다.

 

덕분에 날짜가 바뀌어도 유저는 빈 화면이 아니라 어느 정도 신뢰성 있는 순위를 보게 되었다.

 

 

 

마무리

단순히 상품을 정렬해서 보여주기만 하면 끝날 줄 알았던 랭킹 기능이 이렇게 많은 고민을 안겨줄 줄은 몰랐다.

Redis ZSET이라는 딱 맞는 도구가 있었어도 누적 데이터 관리, 대용량 처리, 초기 데이터 문제까지 신경 써야 할 게 정말 많았다.

기술은 쓰면 쓸수록 고려해야 할 변수가 늘어난다는 말을 매주 체감하고 있는 것 같다.

 

이번 작업에서 특히 많이 고민한 건 진짜 실시간이란 무엇인가? 였다.

어디까지를 실시간이라고 할 수 있을까?

몇 초, 몇 분 단위의 지연은 허용해도 될까?

생각보다 그 경계는 명확하지 않았고 사실 아직 나만의 답도 찾진 못한 것 같다.

 

결국 사용자가 사용할 서비스니까 사용자가 느끼기에 충분히 빠르고 자연스러운 수준에서 답을 찾아야 하지 않을까..

좋은 기능이란 단순히 더 최신 기술, 더 좋은 기술을 쓰는 것보단 사용자 입장에서 자연스럽고 신뢰할 수 있는 기능이지 않을까 생각해 보게 되었다.

 

어쩌면 기능과 기술을 설계하는 건 사용자 경험을 설계한다고 봐도 되지 않을까?

아직 답은 다 못 찾았지만 이런 시행착오들이 쌓여서 내가 만드는 서비스를 조금은 더 좋아지게 만들 수 있을 것 같다.

반응형

'Study > Architecture' 카테고리의 다른 글

Spring Batch로 랭킹 집계 확장하기  (0) 2025.09.19
내부 이벤트를 넘어 Kafka 기반 이벤트 파이프라인으로  (0) 2025.09.05
주문과 결제, 어디까지 한 트랜잭션으로 묶어야 할까?  (2) 2025.08.29
장애 대응 시스템 구축하기  (0) 2025.08.22
캐시 구조 개선  (0) 2025.08.22
'Study/Architecture' 카테고리의 다른 글
  • Spring Batch로 랭킹 집계 확장하기
  • 내부 이벤트를 넘어 Kafka 기반 이벤트 파이프라인으로
  • 주문과 결제, 어디까지 한 트랜잭션으로 묶어야 할까?
  • 장애 대응 시스템 구축하기
haylee
haylee
개발하면서 보고 듣고 느낀 것들을 정리하는중
  • haylee
    haylee
    haylee
    • 홈
    • GitHub
    • LinkedId
    • 분류 전체보기 (19)
      • Project (6)
        • 회고 (6)
      • Study (13)
        • Database (3)
        • Test (1)
        • Architecture (9)
  • hELLO· Designed By정상우.v4.10.1
haylee
실시간 상품 랭킹? Redis ZSET만 쓰면 끝인 줄 알았는데요
상단으로

티스토리툴바