Upstash Redis로 서버리스 게임 리더보드 만들기
Rabbit Hole의 점수는 결국 브라우저가 저한테 보내주는 숫자일 뿐인데, 이걸로 글로벌 리더보드를 게임 서버나 DB 없이 만들고 싶었어요.
Upstash Redis 하나로 충분하더라고요.
무엇을 저장하고, Redis 자료형 두 개가 어떻게 일을 다 하는지 적어봅니다.

랭킹은 sorted set 하나
리더보드는 Redis sorted set이 딱 그거예요.
점수로 정렬된 멤버들.
점수 제출은 명령 하나고, 거기 붙는 플래그 두 개가 핵심입니다.
const changed = await redis.zadd(
key,
{ gt: true, ch: true },
{ score: entry.score, member: entry.playerId },
);
플레이어 한 명이 멤버고, 그 점수가 sorted set의 점수예요. gt: true는 새 점수가 더 높을 때만 갱신하라는 뜻이라, 이전 값을 먼저 읽지 않아도 set이 알아서 플레이어당 최고점 하나만 들고 있습니다. ch: true는 실제로 바뀐 멤버 수를 ZADD가 돌려주게 하는데, 이걸로 다른 걸 건드릴지 말지를 정해요.
이건 바로 아래에서.
상위 N개 읽기는 명령 하나 더, 높은 게 먼저 오도록 뒤집어서:
const raw = await redis.zrange(key, 0, limit - 1, { rev: true, withScores: true });
이름은 짝꿍 해시에
sorted set은 숫자를 들고 있지만, 저는 이름이랑 도달 깊이도 같이 보여주고 싶었어요.
그건 같은 멤버로 묶인 해시에 넣는데, ch가 점수가 진짜 올랐다고 알려줬을 때만 씁니다.
if (changed) {
await redis.hset(hashKey, { [entry.playerId]: JSON.stringify(meta) });
}
이 조건이 중요해요.
제출할 때마다 표시 데이터를 쓰면, 나중의 더 낮은 점수가 누군가의 진짜 최고점에 붙은 이름이랑 깊이를 덮어써 버리거든요.
그리고 행을 다시 읽을 땐 해시에 뭐가 있든 sorted set 점수가 이깁니다.
숫자는 늘 맞고, 해시가 혹시 어긋나도 다음 읽기 때 표시가 알아서 맞춰져요.
보드 둘, 알아서 굴러간다
역대 보드랑 데일리 보드가 있고, 굴이랑 날짜로 키를 나눕니다.
얘네가 무한정 커지지 않게 막는 작은 장치가 둘 있는데, 쓴 만큼 돈 내는 DB에선 이게 싼 장난감이냐 예상 못 한 청구서냐를 가릅니다.
윈도우가 알아서 굴러가는 건 쓸 때마다 만료를 다시 걸기 때문이에요.
데일리는 일주일, 역대는 90일 살고, 노는 키는 그냥 사라집니다.
정리 잡 같은 건 어디에도 없어요.
await redis.expire(key, daily ? SEVEN_DAYS : NINETY_DAYS);
그리고 보드가 들고 있는 멤버 수에 상한을 겁니다.
쓸 때마다 상위 N개 아래는 다 버려요.
await redis.zremrangebyrank(key, 0, -(KEEP + 1));
이게 없으면 인기 있는 보드는 끝없이 커지고, 저장하고 읽는 비용도 같이 커집니다.
상위 몇 개로 잘라두면 열려 있던 비용이 고정 비용이 돼요.
레이트리밋은 공유돼야 한다
서버리스에서 짚고 갈 함정 하나.
인메모리 레이트리밋은 요청이 매번 다른 인스턴스에 떨어질 수 있는 환경에선 아무 소용이 없어요.
인스턴스마다 자기만의 빈 카운터를 갖고 있으니까요.
카운터는 공유되는 곳, 그러니까 여기서도 Redis에 있어야 합니다.
고정 창으로요.
const bucket = Math.floor(now / (WINDOW_SEC * 1000));
const n = await redis.incr(`rl:${key}:${bucket}`);
if (n === 1) await redis.expire(`rl:${key}:${bucket}`, WINDOW_SEC);
return n <= MAX;
INCR이 그 창의 첫 요청에서 키를 1로 만들고, 만료는 한 번만 걸고, 그 창에서 한도를 넘는 건 전부 돌려보냅니다.
정리하면
게임 서버도, 관계형 DB도, 예약해서 돌리는 정리 작업도 필요 없어요.
랭킹은 sorted set에, 이름 같은 표시 데이터는 해시에 담고, 윈도우는 만료로 굴리고, 크기는 랭크 트림으로 잡습니다.
리더보드 하나 정도만 있으면 되는 작은 게임이라면, 이 정도 구성으로 거의 공짜로 돌아가고 손볼 것도 별로 없어요.