1 사용자 행동 수와 랭킹 점수
1.1 랭킹은 사용자 행동을 점수로 환산한 결과
사용자가 남긴 행동은 좋아요, 조회, 구매처럼 다양하며 먼저 이러한 행동이 몇 번 일어났는지를 저장한다.
하지만 랭킹에 반영할 때는 어떤 행동이 순위에 더 중요한지를 표현하기 위해 가중치를 적용해 점수로 계산한다.
따라서 랭킹을 만들려면 두 가지가 필요하다.
하나는 사용자 행동 횟수를 저장하는 것, 다른 하나는 그 데이터를 바탕으로 점수를 계산해 저장하는 것이다.
행동 집계 저장소: product_metrics 테이블
- 날짜별, 상품별로 좋아요 수, 조회 수, 주문 수 같은 count 데이터를 저장한다.
- 이곳은 원천 데이터이자 점수를 계산하기 위한 재료 역할을 한다.
랭킹 계산 저장소: Redis Sorted Set
- product_metrics에 쌓인 데이터를 기반으로 계산된 점수(score)를 저장한다.
- 자동 정렬된 상태로 유지되기 때문에 Top-N 상품이나 특정 상품의 순위를 빠르게 조회할 수 있다.
1.2 일 단위 랭킹
그래서 행동 집계 저장소인 product_metrics에는 날짜별 (productId, yyyymmdd) 단위로 카운트가 저장된다.
그 데이터를 집계하여 Redis의 rank:all:product:{yyyyMMdd} 같은 키로 반영하면, 하루 단위의 인기 순위를 손쉽게 조회할 수 있다.
만약 서비스에서 시간 단위 랭킹을 보여주고 싶다면 원리는 동일하다.
일 단위가 아닌 시간 단위별 카운트를 별도의 저장소(DB, Redis Hash, Redis Sorted Set 등)에 쌓고,
이에 대한 점수를 계산·집계하여 Redis에 반영하면 된다.
즉, 윈도우 크기(일, 시간, 분)를 어떻게 정의하느냐에 따라 랭킹의 시간 양자화가 달라진다.
다만 윈도우 크기를 너무 잘게 나누면 카운트 저장 자체가 부담이 될 수 있다.
이 부분은 어떻게 최적화·운영 할 수 있는지 아직도 고민되는 지점이다.
여기까지는 랭킹을 어떻게 설계할 수 있는지에 대한 이야기다.
이제부터는 실제 구현 과정에서 마주쳤던 문제들을 정리해본다.
2. 대량 데이터 처리: Count 저장과 랭킹 계산을 어떻게 나눌 것인가?
이번에 특히 고민했던 부분은 “재료가 되는 count를 저장하는 곳”과 “실제로 랭킹을 계산하는 곳”을 어떻게 구분할 것인가였다.
2.1 기존 count 저장 로직: 분리된 Handler
이전 주에 개발했던 사용자 행동 이벤트는 컨슈머 그룹과 Kafka Listener는 동일하게 두고, KafkaHandler를 이용해 분리해서 처리했다.
- 좋아요 이벤트 → LikeHandler
- 주문 이벤트 → OrderHandler
이 구조에서는 product_metrics 테이블에 데이터를 저장하는 것까지는 문제 없었다.
하지만 저장 로직에서 랭킹 점수 저장을 위해 Redis까지 갱신하려고 하니 각 핸들러마다 코드를 추가해야 했고 기존 로직을 수정해야 하는 부담이 컸다.
결국 핸들러마다 Redis 호출 로직이 흩어지면서 관리가 복잡해질 수밖에 없었다.
2.2 첫번째 시도 : 별도 컨슈머 그룹에서 Redis 반영
그래서 처음에는 “같은 토픽을 다른 Consumer 그룹에서 동시에 수신해서
한쪽은 product_metrics에 사용자 행동 count를 저장하고 다른 한쪽은 Redis에 랭킹용 score를 저장하는 방식을 고려했다.
하지만 이 방식은 DB와 Redis가 서로 다른 경로로 데이터를 반영하기 때문에 이중 집계/누락 문제가 생길 수 있었다.
왜냐하면, DB 조회 + 해당 이벤트 카운트를 더해서 Redis에 반영하므로 DB 저장이 이미 완료된 상황에서는 이중 집계가 되기 때문이다.
그렇다고 DB조회분만 점수를 계산해 반영하면 아직 처리하지 못한 사용자 행동 카운트가 반영되지 않는다.
2.3 최종 구조: product_metrics 테이블 변경 이벤트 발행 → Redis 반영
이 문제를 해결하기 위해 구조를 아래와 같이 하였다.
- 모든 사용자 행동은 product_metrics 테이블에만 반영
- 그 후, 테이블이 변경될 때 새로운 이벤트를 발행
- 이 이벤트를 수신해서 Redis에 랭킹 점수를 반영
이렇게 하면 Redis는 항상 DB 반영이 끝난 후에만 업데이트되므로 DB와 Redis 간 불일치나 이중 카운팅 문제를 막을 수 있었다.
그렇다면 어떻게 최적화 해서 랭킹 반영을 할 수 있을까
랭킹 처리를 위한 이벤트를 분리한 뒤 새로운 이벤트를 수신할 때 Redis 부하를 최소화하기 위해 아래와 같은 방법을 적용하였다.
3. 사용자 행동의 랭킹 반영 최적화
3.1 Batch Listener 활용
- 단건 Listener 일 때, 이벤트 1개마다 Redis에 ZINCRBY 호출 → Redis 부하 ↑
- Batch Listener 일 때, 일정 개수의 메시지를 모아 한 번에 처리
특히 같은 (productId, date) 조합은 동일 파티션으로 들어오게 하여, 중복으로 들어왔을 경우 한 번만 집계하도록 했다.
3.2 ZADD를 통한 일괄 반영
또한 Redis에서는 각 멤버를 ZINCRBY로 건건히 증가시키는 대신, ZADD의 덮어쓰기 기능을 활용해 여러 멤버를 한 번의 명령어로 처리했다. 이렇게 하면 Redis와의 네트워크 통신 횟수를 크게 줄일 수 있다.
👉 해당 내용을 Redis 명령어로 비교해 보면 아래와 같다.
#### ZINCRBY (건건히 증가)
# 상품 101번 좋아요 이벤트 발생 (점수 +3)
ZINCRBY rank:product:day:20250912 3 product:101
# 상품 202번 주문 이벤트 발생 (점수 +6)
ZINCRBY rank:product:day:20250912 6 product:202
# 상품 101번 다시 조회 이벤트 발생 (점수 +1)
ZINCRBY rank:product:day:20250912 1 product:101
#### ZADD (덮어쓰기, 일괄 처리)
ZADD rank:product:day:20250912
4 product:101
6 product:202
14 product:303
9 product:404
5 product:505
🚀 루퍼스 Loop:Pak L2 Backend
백엔드 경력 3년 이상 대상, 10주 실무 중심 부트캠프.
Java/Kotlin 중 하나를 선택해 실전 문제 해결·코드 리뷰·운영 관점 설계 등 실무 기술을 집중적으로 다룹니다.
🎁 20만원 할인 코드: N17NC