🏆 게임 리더보드 시스템

60

왜 이걸 만들까? — 1등이 누구인지 즉시 알고 싶다

여러분이 모바일 퍼즐 게임을 만들고 있다고 상상해 보세요.

게임 리더보드 아키텍처 — DynamoDB + ElastiCache Redis

플레이어들은 스테이지를 클리어할 때마다 점수를 얻고, "내가 전체 몇 등이지?"를 궁금해합니다. 100명 정도는 DynamoDB에서 Scan → 정렬로 충분하지만, 플레이어가 10만 명, 100만 명으로 늘어나면 어떻게 될까요?

DynamoDB에서 100만 건을 스캔하고 정렬하면 수 초~수십 초가 걸립니다. 게임 리더보드는 탭을 누르는 순간 바로 보여야 하는데, 매번 이렇게 느리면 유저는 떠나갑니다.

해답은 Redis의 Sorted Set 자료구조입니다.

Redis Sorted Set은 각 멤버에 점수(score)를 연결하여 항상 정렬된 상태를 유지합니다. 100만 명 중에서 특정 플레이어의 순위를 조회하는 데 걸리는 시간은 단 0.1ms — DynamoDB 스캔 대비 10,000배 이상 빠릅니다.

이 실습에서는:

  • DynamoDB: 플레이어 프로필과 점수 이력을 영구 저장 (데이터 안전성)
  • ElastiCache Redis: 실시간 순위 계산 전용 캐시 (초고속 조회)
  • Lambda + API Gateway: 점수 등록/순위 조회 API

이 세 가지를 조합하여 대규모 실시간 리더보드를 만듭니다.

이 프로젝트를 통해 배우게 되는 것:

  • Redis Sorted Set: 자동 정렬 자료구조의 원리와 활용법
  • 인메모리 vs 디스크 DB: 속도와 영속성의 트레이드오프
  • VPC 네트워킹: Lambda에서 ElastiCache에 접근하기 위한 VPC 구성
  • Cache-Aside 패턴: 캐시와 데이터베이스를 함께 사용하는 아키텍처 패턴
  • 성능 비교: DynamoDB Scan vs Redis Sorted Set의 응답 시간 차이 직접 체감

실습을 시작하기 전에 AWS 콘솔에 로그인되어 있는지 확인하세요. 리전은 ap-northeast-2 (서울) 을 사용합니다.

아키텍처 개요

Redis Sorted Set으로 실시간 리더보드 구현

데이터 흐름

다이어그램 로딩 중...
게임 리더보드 데이터 흐름

비용 예측

비용 계산기

2시간
0h24h
50 GB
0 GB100 GB
ElastiCache Serverless (Redis)

ECPU/시간

$0.2000
DynamoDB (온디맨드)

백만 쓰기

$62.5000
Lambda

100만 요청당 $0.20 + 실행시간

$0.0040
API Gateway

백만 요청

$175.0000
예상 총 비용$237.7040

* 실제 비용은 AWS 요금 정책에 따라 달라질 수 있습니다.

Step 1: DynamoDB 플레이어 테이블 생성

DynamoDB는 플레이어의 영구 데이터를 저장합니다. 닉네임, 누적 점수, 게임 횟수, 최고 점수 등을 저장하여, Redis 데이터가 유실되더라도 복구할 수 있는 안전장치 역할을 합니다. 온디맨드 모드를 선택하면 트래픽에 따라 자동으로 용량이 조절되어, 실습에서 비용을 최소화할 수 있습니다.

  1. DynamoDB 콘솔 → 테이블 생성 클릭
  2. 테이블 이름: game-players
  3. 파티션 키: player_id (String) — 각 플레이어의 고유 식별자
  4. 정렬 키는 비워 두기 (단순 키-값 조회용이므로)
  5. 용량 모드: 온디맨드 선택 (사전 용량 계획 불필요)
  6. 테이블 생성 클릭 → 상태가 "활성"으로 바뀔 때까지 대기 (약 30초)
코드
aws dynamodb create-table \
  --table-name game-players \
  --attribute-definitions AttributeName=player_id,AttributeType=S \
  --key-schema AttributeName=player_id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region ap-northeast-2

DynamoDB는 플레이어 프로필과 점수 이력을 영구 저장하는 용도로 사용합니다. 실시간 순위 계산은 Redis가 담당하므로 DynamoDB에 순위를 저장할 필요는 없습니다. 순위는 매 순간 변할 수 있기 때문에, 저장보다는 실시간 계산이 적합합니다.

Step 2: ElastiCache Redis 클러스터 생성

ElastiCache Redis는 인메모리 데이터 저장소로, 리더보드의 핵심 엔진입니다. Sorted Set이라는 자료구조가 내장되어 있어, 점수 등록과 동시에 자동 정렬이 유지됩니다. Lambda에서 Redis에 연결하려면 같은 VPC 안에 있어야 합니다.

  1. ElastiCache 콘솔 → 서버리스 캐시생성

  2. 캐시 이름: game-leaderboard

  3. 엔진: Redis 선택

  4. 기본 VPC 및 서브넷 선택 (Lambda와 동일한 VPC를 사용해야 함)

  5. 생성 클릭 → 생성 완료까지 약 2~3분 대기

  6. 엔드포인트 주소를 복사하여 메모 (예: game-leaderboard-xxxxx.serverless.apne2.cache.amazonaws.com)

  7. ElastiCache 콘솔 → Redis 클러스터생성

  8. 클러스터 모드: 비활성화, 이름: game-leaderboard

  9. 노드 타입: cache.t3.micro (프리티어 대상 — 750시간/월 무료)

  10. 복제본 수: 0 (실습용이므로 비용 절감)

  11. 기본 VPC 및 서브넷 선택

  12. 보안 그룹: Lambda가 접근할 수 있도록 인바운드 포트 6379 허용 필요

  13. 생성 클릭 → 엔드포인트 주소 메모

Serverless 옵션은 설정이 간편하고 사용량 기반 과금입니다. 프리티어 활용이 필요하면 cache.t3.micro 클러스터를 선택하세요.

VPC 보안 그룹 설정 필수: Lambda에서 Redis에 접속하려면 Redis 보안 그룹의 인바운드 규칙에 TCP 포트 6379를 Lambda 보안 그룹에서 허용해야 합니다. 이 설정이 누락되면 Lambda에서 Redis 연결 시 타임아웃 에러가 발생합니다.

중간 점검

여기까지 완료되었으면 아래를 확인하세요:

  • DynamoDB game-players 테이블이 "활성" 상태인지
  • ElastiCache Redis 클러스터/서버리스가 "사용 가능" 상태인지
  • Redis 엔드포인트 주소를 메모했는지
  • Lambda와 Redis가 같은 VPC에 있을 수 있도록 VPC/서브넷 정보를 확인했는지

다음 단계에서는 Lambda 함수를 작성하여 Redis와 DynamoDB를 모두 연결합니다.

Step 3: Lambda 함수 작성 (점수 등록 / 순위 조회)

이 Lambda 함수가 리더보드의 비즈니스 로직 전체를 담당합니다. Redis의 핵심 명령어 4가지(ZADD, ZREVRANK, ZREVRANGE, ZSCORE)를 사용하여 점수 등록과 순위 조회를 구현합니다. Lambda Layer로 redis-py 라이브러리를 추가해야 합니다.

진행률 0/9
  1. 1Lambda 콘솔 → 함수 생성 → 이름: leaderboard-handler
  2. 2런타임: Python 3.12, 아키텍처: arm64 (비용 20% 절감)
  3. 3고급 설정 → VPC 설정: ElastiCache와 동일한 VPC, 프라이빗 서브넷 선택, Redis와 같은 보안 그룹 지정
  4. 4환경 변수에 REDIS_HOST = ElastiCache 엔드포인트 주소 추가
  5. 5실행 역할에 AmazonDynamoDBFullAccess 및 AWSLambdaVPCAccessExecutionRole 정책 추가
  6. 6Lambda Layer 추가: redis-py 라이브러리를 ZIP으로 패키징하여 Layer 생성 후 연결
  7. 7핵심 Redis 명령어 구현: ZADD leaderboard {score} {player_id} — 점수 등록 (이미 존재하면 업데이트) ZREVRANK leaderboard {player_id} — 해당 플레이어의 순위 조회 (0부터 시작) ZREVRANGE leaderboard 0 9 WITHSCORES — Top 10 조회 (점수 내림차순) ZREVRANGE leaderboard {start} {end} WITHSCORES — 주변 순위 조회
  8. 8DynamoDB에도 동시에 PutItem으로 플레이어 데이터 영구 저장
  9. 9Deploy 클릭 → 테스트 이벤트로 동작 확인

Lambda VPC 설정 주의: VPC 내에 Lambda를 배치하면 인터넷 접근이 차단됩니다. DynamoDB와 같은 AWS 서비스에 접근하려면 VPC 엔드포인트(Gateway Endpoint)를 생성하거나, NAT Gateway를 사용해야 합니다. DynamoDB용 VPC 엔드포인트는 무료이므로, VPC → 엔드포인트 → DynamoDB Gateway 엔드포인트를 먼저 생성하세요.

Step 4: API Gateway 엔드포인트 설정

게임 클라이언트가 호출할 REST API를 구성합니다. 점수 등록(POST)과 순위 조회(GET) 두 가지 엔드포인트를 만듭니다. 개인 순위 조회는 URL 경로 변수({player_id})를 사용합니다.

진행률 0/9
  1. 1API Gateway 콘솔 → REST API → 생성
  2. 2API 이름: leaderboard-api
  3. 3리소스 /score 생성 → POST 메서드 추가 (점수 등록) → leaderboard-handler Lambda 연결
  4. 4리소스 /ranking 생성 → GET 메서드 추가 (Top N 조회) → 같은 Lambda 연결
  5. 5/ranking 하위에 {player_id} 리소스 생성 → GET 메서드 추가 (개인 순위 + 주변 순위)
  6. 6각 메서드에서 Lambda 프록시 통합 사용 (이벤트에서 경로/메서드로 분기 처리)
  7. 7CORS 활성화: 각 리소스에서 작업 → CORS 활성화 → 허용 오리진 *
  8. 8API 배포 → 새 스테이지 생성 → 이름: dev
  9. 9배포 URL 복사하여 메모
✏️

본인의 말로 설명해 보세요

프로그래밍을 모르는 게임 기획자에게 'Redis의 Sorted Set이 왜 리더보드에 최적인지'를 사전(dictionary) 비유로 설명해 보세요.

Step 5: 성능 비교 테스트

리더보드의 핵심 가치는 속도입니다. Redis Sorted Set의 빠른 응답 시간을 DynamoDB 직접 조회와 비교하여 체감해 봅니다. 실제 게임에서는 이 차이가 사용자 경험에 큰 영향을 줍니다.

진행률 0/6
  1. 1API로 테스트 플레이어 100명의 점수를 일괄 등록: for 루프 스크립트 작성 또는 Postman Runner 활용
  2. 2Top 10 리더보드 조회 → 응답 시간 측정 (목표: Lambda 포함 50ms 이내, Redis 자체는 1ms 이내)
  3. 3특정 플레이어의 순위 + 주변 5명 조회 테스트 → 정확한 순위 반환 확인
  4. 4동일 플레이어의 점수를 업데이트(ZADD는 덮어쓰기) → 순위가 자동 갱신되는지 확인
  5. 5DynamoDB에서 직접 Scan → 정렬하여 Top 10을 구하는 코드와 Redis ZREVRANGE 응답 시간 비교
  6. 6결과 기록: Redis 조회 시간 vs DynamoDB Scan+정렬 시간 → 배수 차이 계산

Redis Sorted Set의 ZREVRANK 명령은 O(log N) 시간 복잡도로 동작합니다. 100만 명의 플레이어가 있어도 1ms 이내에 순위를 반환할 수 있어, 리더보드에 최적입니다. 반면 DynamoDB Scan은 O(N) — 전체 아이템을 읽어야 해서 데이터가 늘어날수록 급격히 느려집니다.

Lambda 핸들러 코드 핵심 구조

점수 등록과 순위 조회를 처리하는 Lambda 코드의 핵심 패턴입니다:

코드
import json, os, boto3, redis
 
# 전역 스코프 — 웜 스타트 시 재사용
r = redis.Redis(host=os.environ['REDIS_HOST'], port=6379, decode_responses=True)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('game-players')
 
LEADERBOARD_KEY = 'leaderboard'
 
def lambda_handler(event, context):
    path = event['path']
    method = event['httpMethod']
    
    if path == '/score' and method == 'POST':
        body = json.loads(event['body'])
        player_id = body['player_id']
        score = float(body['score'])
        
        # Redis에 점수 등록 (자동 정렬)
        r.zadd(LEADERBOARD_KEY, {player_id: score})
        # DynamoDB에 영구 저장
        table.put_item(Item={'player_id': player_id, 'score': int(score)})
        
        rank = r.zrevrank(LEADERBOARD_KEY, player_id)
        return response(200, {'rank': rank + 1})  # 0-based → 1-based
    
    elif path == '/ranking' and method == 'GET':
        top10 = r.zrevrange(LEADERBOARD_KEY, 0, 9, withscores=True)
        result = [{'player_id': p, 'score': s, 'rank': i+1} for i, (p, s) in enumerate(top10)]
        return response(200, result)

decode_responses=True를 설정하면 Redis 응답이 bytes가 아닌 str로 반환되어 JSON 직렬화가 편해집니다. zadd()는 멤버가 이미 존재하면 점수를 업데이트하므로, 별도의 "업데이트" 로직이 필요 없습니다.

핵심 개념 확인

트러블슈팅 가이드

Lambda에서 Redis 연결 타임아웃:

  • Lambda와 Redis가 같은 VPC/서브넷에 있는지 확인
  • Redis 보안 그룹의 인바운드에 TCP 6379 포트가 Lambda 보안 그룹에서 허용되는지 확인
  • Lambda 환경 변수 REDIS_HOST의 엔드포인트 주소가 정확한지 확인
  • Lambda에 AWSLambdaVPCAccessExecutionRole 정책이 연결되어 있는지 확인

Lambda에서 DynamoDB 접근 실패 (타임아웃 또는 AccessDenied):

  • VPC 내 Lambda는 인터넷에 접근할 수 없으므로 DynamoDB VPC Gateway Endpoint를 생성해야 함
  • VPC 콘솔 → 엔드포인트 → 생성 → 서비스: com.amazonaws.ap-northeast-2.dynamodb → 라우팅 테이블 선택
  • Lambda 실행 역할에 DynamoDB 접근 정책이 포함되어 있는지 확인

redis-py 모듈 Import 에러:

  • Lambda Layer에 redis 패키지가 올바르게 포함되어 있는지 확인
  • Layer ZIP 구조: python/redis/ 디렉토리 아래에 라이브러리 파일이 있어야 함
  • pip install redis -t python/python/ 폴더를 ZIP으로 압축 → Layer로 업로드

완성 후 테스트 가이드

전체 시스템이 올바르게 동작하는지 아래 체크리스트로 검증하세요:

  1. 점수 등록: POST /score{"player_id": "player_001", "score": 1500} → 200 응답 확인
  2. Top 10 조회: GET /ranking → 점수 내림차순 10명 목록이 반환되는지 확인
  3. 개인 순위 조회: GET /ranking/player_001 → 해당 플레이어의 순위 + 주변 5명이 반환되는지 확인
  4. 점수 업데이트: 같은 player_id로 더 높은 점수를 등록 → 순위 자동 갱신 확인
  5. DynamoDB 영구 저장: DynamoDB 콘솔에서 game-players 테이블에 데이터가 저장되었는지 확인
  6. 동시 요청: 여러 플레이어의 점수를 빠르게 연속 등록 → 순위가 즉시 갱신되는지 확인
  7. 엣지 케이스: 동일 점수의 플레이어 2명이 있을 때 순위가 어떻게 표시되는지 확인 (Redis는 사전순으로 처리)
  8. 데이터 정합성: Redis의 Top 10과 DynamoDB의 데이터가 일치하는지 교차 검증

확장 아이디어

  1. 주간/월간 리더보드: Redis에 leaderboard:weekly, leaderboard:monthly 키를 분리하고, EventBridge로 매주/매월 초기화하는 스케줄 구현
  2. 친구 리더보드: 플레이어의 친구 목록을 DynamoDB에 저장하고, ZSCORE로 친구들의 점수만 모아서 별도 순위표 생성
  3. 실시간 순위 변동 알림: 순위가 변경되면 WebSocket API Gateway + Lambda로 클라이언트에 실시간 푸시 알림
  4. 리더보드 히스토리: DynamoDB에 일별 Top 100 스냅샷을 저장하여 "지난주 리더보드" 기능 구현
  5. 치팅 방지: 점수 등록 시 Lambda에서 이전 점수 대비 비정상적 상승(예: 1분에 100만점 증가) 탐지 로직 추가

Redis 핵심 명령어 레퍼런스

리더보드에서 사용하는 Redis Sorted Set 명령어를 정리합니다. Lambda 코드에서 redis-py 라이브러리를 통해 호출합니다.

명령어설명시간 복잡도사용 예
ZADD key score member멤버 추가 또는 점수 갱신O(log N)ZADD leaderboard 1500 player_001
ZREVRANK key member내림차순 기준 순위 반환 (0부터)O(log N)ZREVRANK leaderboard player_001 → 0 (1등)
ZSCORE key member특정 멤버의 점수 반환O(1)ZSCORE leaderboard player_001 → 1500
ZREVRANGE key start stop WITHSCORES내림차순 범위 조회O(log N + M)ZREVRANGE leaderboard 0 9 WITHSCORES → Top 10
ZCARD key전체 멤버 수 반환O(1)ZCARD leaderboard → 100000
ZREM key member멤버 삭제O(log N)ZREM leaderboard player_001
ZINCRBY key increment member점수 증분 업데이트O(log N)ZINCRBY leaderboard 100 player_001

ZREVRANK는 0부터 시작하므로 사용자에게 표시할 때는 +1 해야 합니다. 예: ZREVRANK 결과가 0이면 "1등", 9이면 "10등"입니다. ZINCRBY는 누적 점수 시스템에 유용합니다 — ZADD는 덮어쓰기, ZINCRBY는 더하기입니다.

실습 후 사고 실험

실습에서는 100명의 플레이어를 테스트했지만, 실제 게임 서비스에서는 어떨까요?

플레이어 100만 명 시나리오:

  • Redis Sorted Set은 100만 멤버에서도 ZREVRANK0.1ms — 체감 불가능한 속도
  • 메모리 사용량: 100만 멤버 x (플레이어ID 20바이트 + 점수 8바이트) ≈ 약 30MB — cache.t3.micro(0.5GB)로 충분
  • 초당 1만 건의 점수 업데이트도 단일 Redis 노드로 처리 가능

Redis 장애 복구 시나리오:

  • Redis 서버가 재시작되면 메모리 데이터가 유실됨
  • 복구 방법: DynamoDB에서 전체 플레이어 점수를 읽어 Redis에 ZADD로 재로드 (별도 Lambda 작성)
  • 프로덕션에서는 Redis Multi-AZ 복제본을 설정하여 자동 장애 조치(Failover) 구성

학습 정리

핵심 치트시트

ElastiCache Redis의 Sorted Set과 DynamoDB를 조합하여 초고속 게임 리더보드를 구축했습니다. Redis는 O(log N) 순위 조회로 100만 플레이어도 1ms 이내 응답이 가능하고, DynamoDB는 영구 저장으로 데이터 안전성을 보장합니다. Lambda + API Gateway로 서버리스 API를 구성하여 인프라 관리 없이 확장 가능한 리더보드를 완성했습니다.

핵심 개념

  • Redis Sorted Set각 멤버에 실수형 점수(score)를 연결하여 자동 정렬 상태를 유지하는 Redis 자료구조. ZADD(추가/업데이트), ZREVRANK(순위 조회), ZREVRANGE(범위 조회) 등 O(log N) 명령을 제공합니다.
  • ElastiCacheAWS의 완전관리형 인메모리 캐시 서비스. Redis 또는 Memcached 엔진을 선택할 수 있습니다. 서버리스 모드와 클러스터 모드를 제공하며, VPC 내에서만 접근 가능합니다.
  • 인메모리 데이터베이스모든 데이터를 RAM에 저장하는 데이터베이스. 디스크 I/O가 없어 마이크로초 단위 응답이 가능하지만, 서버 장애 시 데이터 유실 위험이 있습니다. Redis는 AOF/RDB 백업으로 일부 보완합니다.
  • VPC Endpoint (Gateway)VPC 내부에서 DynamoDB, S3 같은 AWS 서비스에 인터넷 없이 프라이빗하게 접근하는 경로. Gateway 타입은 무료이며, 라우팅 테이블에 경로가 추가됩니다.
  • Cache-Aside 패턴애플리케이션이 먼저 캐시(Redis)를 확인하고, 없으면 데이터베이스(DynamoDB)에서 가져와 캐시에 저장하는 패턴. 리더보드에서는 모든 쓰기 시 Redis+DynamoDB 동시 갱신(Write-Through) 방식을 사용합니다.
  • O(log N) 시간 복잡도데이터 수 N이 2배로 늘어도 처리 시간은 1단계만 증가하는 효율. Redis Sorted Set은 Skip List 자료구조를 사용하여 100만 건에서도 약 20번의 비교만으로 순위를 찾습니다.

흔한 실수

  • Lambda와 Redis를 다른 VPC/서브넷에 배치하여 연결 실패
  • Redis 보안 그룹에서 6379 포트를 열지 않아 타임아웃 발생
  • VPC 내 Lambda에서 DynamoDB Gateway Endpoint 없이 접근 시도하여 실패
  • redis-py Layer의 디렉토리 구조가 잘못되어 import 에러 발생
  • ZREVRANK 결과가 0부터 시작하는 것을 간과하여 순위를 1 빼서 표시(실제로는 1을 더해야 함)
  • ElastiCache 클러스터 삭제를 잊어 시간당 과금 지속
DynamoDB Scan+정렬 (리더보드)Redis Sorted Set (리더보드)
전체 테이블 스캔 필요 — O(N)인덱스 기반 즉시 조회 — O(log N)
100만 건: 수 초~수십 초100만 건: < 1ms
RCU 대량 소비 → 비용 증가인메모리 연산 → 추가 비용 미미
정렬을 매번 코드에서 수행항상 정렬된 상태 자동 유지
영구 저장 보장 (99.999%)휘발성 위험 (백업 필요)
✏️

본인의 말로 설명해 보세요

비개발자 팀원에게 'Redis가 100만 명 중에서 순위를 0.1ms 만에 찾을 수 있는 원리'를 전화번호부(사전) 검색 비유로 설명해 보세요.

리소스 정리

실습 완료 후 반드시 아래 순서대로 리소스를 정리하여 불필요한 과금을 방지하세요. 특히 ElastiCache 클러스터/서버리스는 시간당 과금이므로 빠르게 삭제하세요.

  1. API Gateway API 삭제
  2. Lambda 함수 및 Lambda Layer 삭제
  3. ElastiCache 서버리스 캐시 또는 클러스터 삭제 (시간당 과금!)
  4. DynamoDB 테이블 삭제 (game-players)
  5. VPC Gateway Endpoint 삭제 (DynamoDB용 — 무료이지만 정리)
  6. VPC 보안 그룹 (커스텀) 삭제
  7. CloudWatch 로그 그룹 삭제
  8. IAM 역할 및 정책 삭제