게임 서버 스케일아웃 — 1대에서 N대로 늘릴 때 터지는 문제
게임 서버를 1대 기준으로 설계했다가 N대로 스케일아웃하면 보상 중복 지급·세션 꼬임·분산 락 미설계 문제가 터집니다. 18년 서버 개발 경험에서 자주 본 실제 사례와 해결 방향을 정리합니다.
게임 서버 1대로 개발할 때는 문제가 없습니다. 서버를 N대로 늘리는 순간, 안 터지던 것들이 터집니다.
이 글에서는 게임 서버 스케일아웃 시 가장 자주 발생하는 문제 3가지를 다룹니다. 전부 실제로 겪은 사례입니다.
왜 1대에서는 안 터지는가
1대일 때는 모든 요청이 한 프로세스에서 처리됩니다.
메모리에 락을 걸면 동시성 문제가 해결됩니다. 세션은 메모리에 있으니 항상 같은 값입니다. 캐시도 한 곳이니 정합성 걱정이 없습니다.
문제는 동접이 늘어서 서버를 2대, 3대, 10대로 스케일아웃하는 순간입니다. 1대 기준 설계가 무너집니다.
사례 1 — 보상이 2번 지급되는 문제
상황
출석 보상 API가 있습니다. 유저가 하루 1번 받을 수 있습니다.
서버 1대일 때는 문제없습니다. 요청이 들어오면 DB에서 "오늘 받았는지" 확인하고, 안 받았으면 지급합니다.
서버가 3대로 늘어났습니다. 로드밸런서가 요청을 분산합니다.
유저가 보상 버튼을 빠르게 2번 누릅니다. 첫 번째 요청은 서버 A로 갑니다. 두 번째 요청은 서버 B로 갑니다.
서버 A가 DB를 확인합니다. "안 받았음." 지급합니다. 서버 B도 DB를 확인합니다. A가 아직 커밋 전이라 "안 받았음." 또 지급합니다.
보상이 2번 지급됩니다.
1대일 때는 메모리 락으로 막았습니다. N대에서는 메모리 락이 각 서버에 독립적이라 의미가 없습니다.
해결 방향
분산 락이 필요합니다.
Redis의 SETNX(SET if Not eXists)를 쓰는 게 가장 일반적입니다.
// 의사 코드
key = "reward:daily:{userId}:{date}"
acquired = redis.SETNX(key, 1, TTL=24h)
if (!acquired) {
return "이미 수령했습니다"
}
// 보상 지급 로직
db.grantReward(userId)
모든 서버가 같은 Redis를 바라봅니다. 어떤 서버에 요청이 가든, 락은 하나입니다.
핵심은 DB가 아니라 외부 저장소에서 락을 잡는 것입니다. DB 트랜잭션만으로는 타이밍 이슈를 완전히 못 막습니다.
사례 2 — 유저 세션이 서버마다 다른 문제
상황
로그인 후 유저 정보를 서버 메모리에 캐싱합니다. 이름, 레벨, 인벤토리, 접속 시간 등.
서버 1대일 때는 문제없습니다. 모든 API가 같은 메모리를 읽습니다.
서버가 N대로 늘어났습니다.
유저의 첫 요청이 서버 A로 갑니다. 서버 A 메모리에 세션이 생깁니다.
다음 요청이 서버 B로 갑니다. 서버 B에는 세션이 없습니다.
"로그인 안 됨" 에러가 발생합니다.
또는 서버 A에서 아이템을 획득했는데, 서버 B에서는 인벤토리에 안 보입니다.
해결 방향
3가지 선택지가 있습니다.
1. Sticky Session
로드밸런서가 같은 유저를 항상 같은 서버로 보냅니다. 구현이 가장 쉽습니다.
단점: 특정 서버에 유저가 몰릴 수 있습니다. 서버 1대가 죽으면 그 서버 유저의 세션이 전부 사라집니다.
2. 세션 외부 저장소 (Redis)
세션을 서버 메모리가 아니라 Redis에 저장합니다. 어떤 서버에 요청이 가든 같은 세션을 읽습니다.
가장 일반적인 방법입니다. 대부분의 프레임워크가 Redis 세션 어댑터를 지원합니다.
3. 무상태(Stateless) 설계
서버에 세션을 아예 저장하지 않습니다. JWT 토큰에 필요한 정보를 담아 클라이언트가 매 요청마다 보냅니다.
서버 확장이 가장 자유롭습니다. 단, 토큰 크기·갱신·무효화 설계가 필요합니다.
게임 서버에서는 2번(Redis 세션)이 가장 현실적입니다. 게임은 상태가 많아서 JWT에 다 담기 어렵고, Sticky Session은 장애 시 세션 유실 리스크가 큽니다.
사례 3 — 서버마다 캐시가 달라서 게임 데이터가 꼬이는 문제
상황
게임 운영팀이 웹 어드민에서 이벤트 보상을 변경합니다. "골드 1,000" → "골드 2,000"으로.
서버 1대일 때는 캐시를 갱신하면 끝입니다.
서버가 5대입니다. 어드민이 서버 A에 변경 요청을 보냅니다. 서버 A의 캐시가 갱신됩니다.
서버 B, C, D, E는 옛날 값을 들고 있습니다.
서버 A를 탄 유저는 골드 2,000을 받습니다. 서버 B를 탄 유저는 골드 1,000을 받습니다.
같은 이벤트인데 보상이 다릅니다.
유저가 "나는 1,000 받았는데 친구는 2,000 받았다"고 문의합니다. 운영 사고입니다.
해결 방향
캐시 무효화를 전체 서버에 전파해야 합니다.
방법은 크게 2가지입니다.
1. Pub/Sub 방식
Redis Pub/Sub 또는 메시지 큐를 씁니다. 어드민이 데이터를 바꾸면 "캐시 갱신" 이벤트를 발행합니다. 모든 서버가 구독하고 있다가 자기 캐시를 갱신합니다.
// 어드민에서 보상 변경 시
db.updateReward(eventId, newReward)
redis.publish("cache:invalidate", { type: "reward", id: eventId })
// 각 게임 서버에서
redis.subscribe("cache:invalidate", (msg) => {
cache.delete(msg.type, msg.id)
})
2. 캐시를 서버 메모리에 두지 않는 방식
Redis 같은 외부 캐시만 씁니다. 서버 로컬 캐시를 아예 안 씁니다.
가장 단순합니다. 단, 매 요청마다 Redis를 조회해야 해서 지연이 약간 늘어납니다.
게임 서버에서는 1번(Pub/Sub) + 로컬 캐시 TTL 조합이 가장 많이 쓰입니다. Pub/Sub으로 즉시 갱신하되, 혹시 놓쳐도 TTL이 만료되면 자동으로 최신 값을 가져옵니다.
공통 교훈 — 스케일아웃 전에 점검할 3가지
3가지 사례의 공통점이 있습니다.
1대에서 "메모리"로 해결했던 것을 N대에서는 "외부 저장소"로 옮겨야 합니다.
점검 목록:
- 락: 메모리 락 → Redis 분산 락
- 세션: 서버 메모리 → Redis 세션 또는 무상태
- 캐시: 서버 로컬 → 외부 캐시 + 무효화 전파
이 3가지를 스케일아웃 전에 점검하면 대부분의 정합성 문제를 막을 수 있습니다. 스케일아웃 후에 발견하면 라이브 장애입니다.
관련 글
게임 서버 스케일아웃이 걱정되거나, 기존 서버 구조가 N대에서 안전한지 확인이 필요하시면 30분 무료 미팅에서 같이 점검해드립니다.
More articles
See more posts →퍼블리셔 미팅 전, 개발사가 준비해야 할 것 — 합격을 가르는 5가지
퍼블리셔 미팅은 기회가 한 번뿐인 경우가 많습니다. 퍼블리셔가 미팅에서 실제로 보는 것은 무엇인지, 플레이 가능한 빌드·핵심 지표·한 장 피치 자료·BM 로드맵을 어떻게 준비해야 하는지, 그리고 미팅에서 떨어지는 흔한 패턴까지 정리했습니다.
방치형·시뮬 게임의 시간 어뷰징 방어 — 18년차가 정리한 6가지 레이어
디바이스 시간 변경 한 번에 오프라인 보상이 무너지는 방치형·시뮬 게임을 위한 시간 위변조 방어 가이드. 5가지 공격 벡터, 6가지 방어 레이어, 외주 의뢰자 검수 체크리스트.
게임 출시 후 매출이 안 나올 때 — D1·ARPU·ROAS로 보는 첫 30일 진단 가이드
모바일 게임 출시 후 매출이 안 나올 때 광고부터 늘리는 건 위험합니다. D1 리텐션·ARPU·ARPPU·ROAS·LiveOps 첫 30일 KPI를 어떤 순서로 진단하고 처방할지, 장르별 기준선과 외주사 운영 협업 모델 비교까지 정리했습니다.
하이퍼캐주얼 게임 서버 필요한가 — 클라이언트로 가능한 기능과 서버가 필요한 5가지 시점
하이퍼캐주얼 게임에 서버가 정말 필요한지 판단하는 가이드. 클라이언트로 가능한 기능, 서버를 부르는 5가지 기능, Firebase·PlayFab BaaS와 자체 서버의 분기점, DAU별 비용 비교.