Redis와 WebSocket으로 실시간 리더보드 구축하기
Redis Sorted Set으로 순위를 계산하고 WebSocket으로 변경된 순위를 즉시 전달하는 리더보드 설계를 정리합니다. Unity 같은 게임 클라이언트에서 안정적으로 동기화하기 위한 서버 구조와 운영 주의점도 함께 다룹니다.

왜 Redis와 WebSocket인가
게임 리더보드는 점수가 자주 바뀌고 플레이어는 자신의 순위 변화를 바로 확인하고 싶어 합니다. 매번 HTTP 요청으로 전체 순위를 조회하면 접속자가 늘어날수록 같은 데이터를 반복해서 전송하게 됩니다.
Redis의 Sorted Set은 점수와 정렬을 함께 관리하므로 순위 계산에 적합합니다. WebSocket은 서버가 변경 사항을 연결된 클라이언트에 즉시 보낼 수 있어 화면 갱신 비용을 줄여 줍니다.
flowchart LR
C[게임 클라이언트] -->|점수 제출 HTTPS| A[게임 API 서버]
A -->|ZINCRBY 또는 ZADD| R[(Redis Sorted Set)]
A -->|순위 변경 이벤트| B[브로드캐스트 채널]
B --> W[WebSocket 서버]
W -->|변경된 순위 데이터| C
Sorted Set으로 점수와 순위 관리하기
Sorted Set의 멤버는 플레이어 ID이고 점수는 정렬 기준입니다. 시즌과 게임 모드를 키에 포함하면 데이터 범위를 명확히 분리할 수 있습니다.
leaderboard:season:2026-summer:solo
점수 누적에는 ZINCRBY, 절대 점수 반영에는 ZADD를 사용합니다. 게임 서버가 최종 점수를 검증한 뒤 Redis를 갱신해야 합니다. 클라이언트가 보낸 점수를 그대로 신뢰하면 패킷 변조로 리더보드를 조작할 수 있습니다.
ZINCRBY leaderboard:season:2026-summer:solo 120 player:481
ZREVRANK leaderboard:season:2026-summer:solo player:481
ZREVRANGE leaderboard:season:2026-summer:solo 0 9 WITHSCORES
ZREVRANK는 0부터 시작하는 순위를 반환합니다. 사용자에게 보여 줄 때는 1을 더합니다. 점수가 같을 때의 정렬 규칙도 미리 정해야 합니다. Redis Sorted Set은 같은 점수인 멤버를 멤버 문자열 기준으로 정렬하므로 획득 시각 같은 별도 타이브레이커가 필요하다면 점수 표현 방식이나 보조 자료 구조를 설계해야 합니다.

점수 갱신과 이벤트 발행을 한 작업으로 묶기
점수를 갱신한 뒤 순위를 조회하고 이벤트를 발행하는 과정이 분리되면 중간에 다른 갱신이 끼어 클라이언트에 오래된 순위를 보낼 수 있습니다. Lua 스크립트로 관련 Redis 명령을 원자적으로 처리할 수 있습니다.
-- KEYS[1]: 리더보드 키
-- ARGV[1]: 플레이어 ID
-- ARGV[2]: 증가 점수
local score = redis.call('ZINCRBY', KEYS[1], ARGV[2], ARGV[1])
local rank = redis.call('ZREVRANK', KEYS[1], ARGV[1])
return { score, rank }
애플리케이션 서버는 반환된 점수와 순위를 이벤트 메시지로 만듭니다. 점수 변경 이벤트에는 전체 리더보드 대신 변경을 식별하는 최소 정보만 넣는 편이 좋습니다.
{
"type": "leaderboard.updated",
"seasonId": "2026-summer",
"mode": "solo",
"playerId": "481",
"score": 1720,
"rank": 7,
"revision": 1042
}
WebSocket으로 무엇을 보낼지 결정하기
상위 10명 안에서 순위가 바뀌었다고 모든 접속자에게 전체 목록을 보내면 트래픽이 빠르게 커집니다. 일반적으로는 다음 방식을 함께 사용합니다.
- 최초 연결 시 상위 N명과 내 순위를 스냅샷으로 전송합니다.
- 이후에는 변경 이벤트 또는 영향받은 상위 N명만 전송합니다.
- 클라이언트는
revision이 건너뛰었거나 메시지 처리에 실패했을 때 HTTP API로 스냅샷을 다시 요청합니다. - 화면에 보이지 않는 리더보드 채널은 구독을 해제합니다.
WebSocket 서버가 여러 대라면 한 서버에서 발생한 이벤트를 다른 서버에 연결된 사용자에게도 전달해야 합니다. Redis Pub/Sub는 실시간 전달에는 간단하지만 구독 중이 아닌 소비자는 메시지를 받지 못합니다. 재처리가 필요한 점수 확정 이벤트에는 Redis Streams나 별도 메시지 브로커를 검토하고 WebSocket 알림은 재동기화 가능한 힌트로 취급하는 설계가 안전합니다.
Unity 클라이언트 동기화 예시
클라이언트는 수신한 변경 메시지를 바로 목록에 덮어쓰기보다 현재 목록의 해당 플레이어를 찾고 점수와 순위를 갱신한 뒤 정렬합니다. 메시지가 누락될 수 있으므로 일정 조건에서 전체 스냅샷을 다시 받는 경로가 반드시 있어야 합니다.
[System.Serializable]
public class LeaderboardUpdate
{
public string type;
public string playerId;
public int score;
public int rank;
public long revision;
}
public void ApplyUpdate(LeaderboardUpdate update)
{
if (update.revision != lastRevision + 1)
{
RequestLeaderboardSnapshot();
return;
}
lastRevision = update.revision;
UpdateOrInsertRow(update.playerId, update.score, update.rank);
SortVisibleRows();
}
revision은 서버가 리더보드 채널별로 증가시키는 번호입니다. 재연결 후에는 이전 메시지를 억지로 이어 붙이기보다 최신 스냅샷을 받아 기준 상태를 다시 만드는 편이 단순하고 신뢰할 수 있습니다.
운영에서 확인할 항목
리더보드 키에는 시즌 종료 시점에 맞춘 만료 정책을 두거나 시즌 종료 작업에서 아카이브한 뒤 새 키로 전환합니다. 점수 제출 API는 인증과 속도 제한을 적용하고 서버 측 플레이 기록으로 점수의 유효성을 검증합니다.
또한 관측 지표를 준비해야 합니다. Redis 명령 지연 시간, WebSocket 연결 수, 채널별 전송량, 재동기화 비율을 기록하면 병목과 메시지 누락 문제를 빠르게 찾을 수 있습니다.
핵심은 Redis가 순위 계산을 담당하고 WebSocket은 최신 상태를 알리는 통로라는 역할 분리입니다. 정확성은 서버 검증과 스냅샷 재동기화로 보완하면 빠르면서도 복구 가능한 실시간 리더보드를 만들 수 있습니다.



