실시간 게임 순위표
- 10.1 문제 이해 및 설계 범위 확정
- 10.2 개략적 설계안 제시
- 10.3 상세 설계
- 10.4 마무리
**실시간 게임 순위표(Leaderboard)**는 모바일 게임에서 "내가 지금 몇 등이지?"를 보여주는 기능이야. 단순해 보이지만 사용자가 수백만 명이면 순위 계산이 만만치 않지. 핵심 도구는 Redis의 sorted set이야.
기능 요구사항을 보면:
- 게임 결과에 따라 사용자의 점수가 갱신된다
- 실시간 Top 10 순위를 표시한다
- 특정 사용자의 현재 순위를 표시한다
- 특정 사용자 주변 순위(예: 내 위아래 4명씩)를 표시한다
비기능 요구사항은:
- 실시간 갱신 — 점수가 변하면 순위가 즉시 반영돼야 해
- 높은 확장성 — DAU 500만 명, 동시 접속 2,500만 명 수준
- 낮은 지연시간 — 순위 조회가 수 밀리초 이내
규모 계산 — 하루에 사용자당 평균 10게임을 한다면, 하루 5,000만 건의 점수 업데이트. 피크 시간대 QPS는 초당 약 2,500건 정도야.
일단 가장 직관적인 방법을 생각해 보자. users 테이블에 score 컬럼이 있고, 순위를 구하려면:
SELECT *, RANK() OVER (ORDER BY score DESC) AS ranking FROM users
이건 전체 테이블을 스캔해야 해. 500만 행이면 한 번의 순위 조회에 몇 초가 걸릴 수 있어. 실시간 순위표에는 부적합하지.
인덱스를 잘 걸어도 "내가 몇 등이냐"를 구하려면 나보다 점수가 높은 행의 수를 세야 하니까, 데이터가 많으면 느려져.
이 문제를 아름답게 푸는 자료구조가 **Redis의 sorted set(ZSET)**이야.
Sorted set은 각 멤버에 **score(점수)**를 부여하고, 점수 기준으로 정렬된 상태를 항상 유지해. 핵심 연산들의 시간 복잡도가 이래.
ZADD key score member— 멤버 추가 또는 점수 갱신: O(log n)ZREVRANK key member— 점수 내림차순에서 멤버의 순위: O(log n)ZREVRANGE key start stop— 순위 범위의 멤버 목록: O(log n + k) (k = 반환 개수)ZREVRANGEBYSCORE key max min— 점수 범위의 멤버 목록
500만 멤버여도 log(5,000,000) ≈ 22이니까, 한 번의 순위 조회에 22번 정도의 비교 연산이면 돼. 밀리초 이하지.
내부적으로 sorted set은 **스킵 리스트(skip list)**와 해시 테이블을 조합한 자료구조야. 스킵 리스트 덕분에 정렬 순서를 유지하면서 O(log n) 검색이 가능하고, 해시 테이블로 특정 멤버의 점수를 O(1)에 조회하지.
점수 갱신 흐름을 보면:
- 게임 서버가 게임 결과를 API 서버에 전송
- API 서버가 Redis에
ZADD leaderboard <new_score> <user_id>실행 - Redis가 sorted set 내에서 해당 사용자의 점수를 갱신하고 순서를 재배치
이게 끝이야. 별도의 "순위 재계산" 과정이 없어. sorted set이 항상 정렬된 상태를 유지하니까.
순위 조회 흐름을 보면:
Top 10 조회: ZREVRANGE leaderboard 0 9 WITHSCORES → 상위 10명의 사용자 ID와 점수 반환
내 순위 조회: ZREVRANK leaderboard <user_id> → 0-based 순위 반환 (1 더하면 실제 순위)
내 주변 순위: 먼저 내 순위를 구하고, ZREVRANGE leaderboard (rank-4) (rank+4) 로 주변 9명을 가져와
Redis에는 user_id와 score만 저장하고, 사용자 이름이나 프로필 이미지 같은 상세 정보는 **별도의 DB(MySQL, Redis Hash 등)**에 저장해. 순위표를 보여줄 때 user_id 목록으로 상세 정보를 조회해서 합치지.
점수가 같으면 누가 더 높은 순위인가? 여러 전략이 가능해.
- 먼저 달성한 사람이 높은 순위 — score에 타임스탬프를 미세하게 반영. 예:
score = actual_score * 1000000 + (MAX_TIMESTAMP - timestamp). 같은 점수라도 먼저 달성한 사람의 보정 점수가 더 높지. - 단순히 Redis의 기본 사전순 정렬에 맡기기 — 동점이면 멤버 이름의 사전순으로 정렬돼. 보통은 별로 유용하지 않아.
사용자 500만 명이면 각 엔트리에 약 100바이트(사용자 ID + 점수 + 내부 오버헤드)가 필요하다 하면 약 500MB. Redis 인스턴스 하나로 충분하지.
사용자가 5억 명이 되면? 50GB로 단일 Redis 인스턴스로는 어려워. 이때는 샤딩이 필요해.
고정 파티션 — 점수 범위로 나눠. 0-100점은 샤드 1, 101-200점은 샤드 2... 이런 식. 문제는 점수 분포가 균등하지 않으면 특정 샤드에 데이터가 몰리지.
해시 파티션 — user_id를 해싱해서 샤드를 결정. 데이터 분포는 균등하지만, 글로벌 순위를 구하려면 모든 샤드에 질의해서 결과를 합쳐야 해. Top 10을 구하려면 각 샤드의 Top 10을 가져와서 합친 다음 정렬하지.
실전에서는 점수 범위 파티션이 순위 계산에 더 유리해. "내 순위"를 구할 때 나보다 점수가 높은 범위의 샤드들의 크기를 합산하면 되니까.
Redis는 메모리 기반이니까 서버가 죽으면 데이터가 날아갈 수 있어. **AOF(Append Only File)**나 RDB 스냅샷으로 영속성을 확보하고, **복제(replica)**를 두어서 장애 시 빠르게 복구하지.
추가로 점수 업데이트를 MySQL 같은 영속 DB에도 기록해두면, Redis가 완전히 날아가도 DB에서 다시 구축할 수 있어.
순위표는 적절한 자료구조를 선택하면 난이도가 확 내려가는 전형적인 문제야. RDBMS로 접근하면 막히지만, Redis sorted set을 쓰면 깔끔하게 풀리지. 면접에서는 "왜 RDBMS가 안 되는지"를 먼저 설명하고, sorted set으로 자연스럽게 넘어가는 흐름이 좋아.
정리
10장 읽고 기억할 거 세 가지:
- Redis sorted set은 O(log n)에 순위 조회와 점수 갱신이 가능한 완벽한 자료구조야. 내부적으로 스킵 리스트를 사용해서 항상 정렬 상태를 유지하지.
- RDBMS로 순위를 구하려면 전체 스캔이나 COUNT가 필요해서 대규모에서 비현실적이야. 이게 Redis를 선택하는 이유지.
- 대규모에서는 Redis를 점수 범위로 샤딩하면 글로벌 순위 계산이 효율적이야. 영속성은 AOF/RDB + 별도 DB 백업으로 확보하지.