본문 바로가기

개발/루퍼스(Loopers)

Redis ZSET로 랭킹 시스템 구현하기

반응형

무신사나 29cm 같은 e커머스에 필수 기능인 랭킹 시스템은 과연 어떤식으로 구현되어있는 것일까?

나는 이전에 kafka 기반의 이벤트 파이프라인을 구축하였다. 이번에는 그 위에 Redis ZSET을 얹어 “조회/좋아요/주문” 신호를 가중치로 녹여 내는 랭킹 시스템을 만들었다. 

 왜 API는 Redis에서 조회하고 Streamer에만 적재하여 책임을 분리한 것일까? 

commerce-api 는 조회, 좋아요, 주문 등이 발생할 때마다 이를 이벤트 형태로 Kafka에 produce 하고, Kafka에 적재된 이벤트는 commerce-streamer 에서 consume 된다 (여기까진 이전 글에 더 자세히 적혀있다)
streamer는 배치 리스너를 통해 이벤트를 묶어서 수신한 뒤, 이벤트 유형에 따라 가중치를 적용하여 Redis ZSET에 랭킹 점수를 누적 적재한다. 이를 통해 랭킹 계산 로직을 API 서버와 분리하였다. 

랭킹 조회 요청은 다시 commerce-api를 통해 처리되는데, API 서버는 Kafka를 거치지 않고 Redis에 직접 접근하여 랭킹 데이터를 조회함으로써, 빠른 응답 속도를 제공하게 하였다.

 

이처럼 랭킹 조회는 빠른 응답을 우선하는 Read Path 로,랭킹 적재는 안정적인 처리를 우선하는 Write Path 로 분리함으로써, 조회 성능과 집계 안정성을 모두 만족하는 구조로 만들었다.

 

메세지 단건 처리를 Kafka 배치 리스너로 변경하다.

랭킹 시스템은 실시간성이 중요하지만, 모든 이벤트를 즉시 반영할 필요는 없다. 오히려 이벤트를 단건으로 처리할 경우 Redis ZSET 연산과 DB 멱등 체크가 과도하게 발생해 시스템 전체 처리량이 급격히 저하될 수 있다.

 

나는 이를 해결하기 위해 Kafka 배치 리스너(batch listener) 를 도입했다. 이를 통해 컨슈머는 일정 개수 또는 시간 단위로 메시지를 묶어 수신하고, 애플리케이션 레벨에서 이를 정제한 뒤 한 번에 처리함으로써 Redis 연산 횟수를 줄이고 전체 처리량을 크게 향상시킬 수 있었다.

 

이벤트 가중치를 통한 랭킹 점수 설계

각 이벤트는 일간 단위의 Redis ZSET 키(e.g. ranking:all:{yyyyMMdd})에 점수 누적 방식으로 반영된다.

나는 모든 이벤트를 동일하게 취급하지 않고, 이벤트의 성격에 따라 서로 다른 Weight 를 부여하였는데, 

조회 : Weight = 0.1 , Score = 1
좋아요 : Weight = 0.2 , Score = 1
주문 : Weight = 0.6 , Score = price * amount

 

다음과 같은 가중치를 부여함으로서 구매 전환에 가까운 행동일수록 랭킹에 더 큰 영향을 주도록하였다.


 

- 추후 보강할 것

현재 랭킹 시스템은 조회·좋아요·주문과 같은 이벤트를 기반으로 점수를 누적하는 구조이기 때문에, 이벤트 이력이 전혀 없는 신규 상품이나 신규 브랜드의 경우 랭킹에 노출되기 어려운 콜드 스타트 문제가 존재한다.

 

이 문제를 해결하기 위해 나는 Score Carry-Over를 도입할 예정인데, 이는 이전 날의 있는 데이터들의 가중치를 좀 끌고와서 합산해서 노출하는 방법을 말한다.

 

예시를 통해 쉽게 설명하자면 

오늘 점수 = (어제 점수 × 가중치) + 오늘 새로 계산된 점수

어제 점수: 100
carry-over 가중치: 0.8
오늘 새 점수: 30
오늘 최종 점수 = 100 × 0.8 + 30 = 110

 

다음과 같은 과정을 통해 랭킹의 신뢰도를 높일 수 있도록 할 예정이다. 

반응형