인디 멀티플레이어 게임에 70만 명이 동시 접속하면 벌어지는 일 (그리고 살아남는 법)
핵심 요약
인디 게임이 바이럴되어 70만 명 동시 접속 시 벌어지는 장애 순서와 P2P 아키텍처의 비용 절감 효과, 그리고 로비 및 매치메이킹 시스템을 10배 부하에도 견디게 설계하는 구체적인 백엔드 패턴을 분석합니다.
모든 인디 개발자는 하룻밤 사이에 바이럴 히트를 꿈꿉니다. 게임이 Twitch에서 폭발하고, Steam 동시 접속자가 일주일 만에 200명에서 20만 명으로 치솟으며, 갑자기 업계의 화제가 됩니다. 하지만 그 환상에서 아무도 알려주지 않는 것은, 새벽 3시에 매치메이킹 서비스에 불이 붙고, 로비 데이터베이스에 쓰기 충돌 오류가 발생하며, Discord에는 단 한 게임도 연결할 수 없는 플레이어들로 가득 찼을 때 백엔드가 어떻게 되는지입니다.
이것은 가상의 이야기가 아닙니다. 2022년 말 Goose Goose Duck이 바이럴되었을 때, Gaggle Studios—이전에 대형 히트작이 없었던 소규모 팀—는 동시 접속자가 70만 명을 돌파하는 것을 지켜봤습니다. 그들의 백엔드는 버텼습니다. 무한한 리소스가 있었기 때문이 아니라, 초기에 특정 아키텍처 결정을 내려서 그 급증을 견딜 수 있었기 때문입니다.
이 글에서는 그 결정들이 정확히 무엇이었는지, 멀티플레이어 게임이 바이럴될 때 가장 먼저 무엇이 고장 나는지, 그리고 트래픽이 도착하기 전에 자신의 프로젝트에 적용할 수 있는 구체적인 패턴을 분석합니다.
바이럴 멀티플레이어 스파이크의 해부학
실제로 고장 나는 것들 (순서대로)
멀티플레이어 게임이 예상 부하의 10~100배에 도달하면, 장애는 예측 가능한 순서로 연쇄적으로 발생합니다. 이 순서를 이해하는 것이 중요합니다. 올바른 순서로 시스템을 강화해야 하기 때문입니다.
1. 인증 및 로그인 (처음 48시간 동안 정상 부하의 5~15배)
플레이하려는 모든 플레이어는 먼저 인증을 거쳐야 합니다. Steam 인증 게임의 경우 Steam의 백엔드가 대부분의 무거운 작업을 처리하지만, 서버는 여전히 티켓을 검증하고, 플레이어 프로필을 생성하거나 가져오고, 세션 토큰을 반환해야 합니다. 각 인증 요청이 기본 데이터베이스에 접근한다면 문제가 발생합니다. 분당 5만 건의 로그인 요청이 폭주하여 각각 세션 생성을 위해 PostgreSQL에 쓰기 작업을 수행하면, 90초 이내에 커넥션 풀이 포화 상태가 됩니다.
2. 로비 검색 및 매치메이킹 (정상 부하의 10~50배)
이것이 실제로 플레이어 경험을 망치는 첫 번째 도미노입니다. 20만 명의 플레이어가 동시에 로비를 탐색할 때, 로비 목록 조회 패턴이 "초당 수백 건의 읽기"에서 "초당 수만 건의 읽기"로 바뀝니다. 로비 상태가 기본 관계형 데이터베이스에 저장되어 있다면, 복제 지연을 따라잡지 못하는 읽기 복제본과 씨름하게 되어, 이미 가득 찬 방이 아직 사용 가능한 것으로 표시되는 오래된 로비 데이터를 반환하게 됩니다.
3. 로비 생성 및 참가 작업 (쓰기 중심 스파이크)
모든 새 게임 로비는 하나의 쓰기 작업입니다. 로비에 참가하는 모든 플레이어는 쓰기 작업(플레이어 목록 업데이트)입니다. 플레이어가 나가는 것도 쓰기 작업입니다. Goose Goose Duck 트래픽이 최고조에 달했을 때, 이는 초당 수천 건의 로비 상태 변경을 의미했습니다. Gaggle Studios는 실제 게임플레이에 P2P 모델을 사용했지만, 로비 조정은 여전히 중앙화가 필요했습니다. 플레이어가 직접 연결하기 전에 서로를 찾을 수 있어야 하기 때문입니다.
4. NAT traversal 및 P2P 연결 설정
바로 여기서 P2P 아키텍처의 한계가 드러납니다. STUN/TURN 인프라가 있더라도 P2P 연결은 실패합니다. 릴레이 폴백 없이 P2P 연결 성공률은 업계 평균적으로 플레이어 쌍의 약 7585%입니다. 나머지 1525%는 TURN 릴레이 서버가 필요합니다. 70만 명의 동시 접속자가 초당 수천 건의 연결을 시도하는 상황에서, 대부분의 인디 팀이 구축해 본 적 없는 릴레이 인프라가 필요합니다.
P2P가 올바른 선택이었던 이유 (그리고 더 이상 아닐 때)
Gaggle Studios는 Goose Goose Duck의 실제 게임플레이에 P2P를 선택했으며, 세션당 2~16명이 참여하는 소셜 디덕션 게임에게는 이것이 진정으로 올바른 결정이었습니다. 그 이유와 트레이드오프가 발생하는 지점은 다음과 같습니다.
P2P 비용 모델
수학을 생각해 보세요. 16인 매치가 15분 동안 진행되는 전용 서버 아키텍처를 저렴한 클라우드 인스턴스(공유 vCPU 기준 약 $0.04/시간)에서 운영하면 매치당 약 $0.01의 비용이 듭니다. 피크 시간대에 50만 개의 동시 매치를 가정하면, 컴퓨팅 비용만 시간당 $5,000입니다. 이는 하루에 $120,000에 달합니다.
P2P는 이 컴퓨팅 비용을 호스트 플레이어의 머신으로 전가합니다. 인프라 비용은 조정 레이어(매치메이킹 서버, 로비 상태, 인증, STUN/TURN 릴레이)로 떨어집니다. Goose Goose Duck의 경우, 플레이어 수가 급증해도 인프라 비용을 관리 가능한 수준으로 유지할 수 있었습니다.
P2P 신뢰성 한계
그러나 P2P는 전용 서버에는 없는 장애 모드를 도입합니다:
- 호스트 마이그레이션: 호스트 플레이어가 연결을 끊으면, 세션은 다른 피어에게 권한을 이전해야 합니다. 소셜 디덕션 게임의 경우, 호스트 마이그레이션이 잘못되면 투표 상태가 손실되고, 역할 할당이 동기화되지 않으며, 매치가 망가집니다. 일반적인 호스트 마이그레이션 시퀀스는 다음과 같습니다:
// Simplified peer-to-peer host migration logic
// When the current host becomes unreachable
void OnHostUnreachable(float timeoutSeconds = 3.0f) {
// 1. All peers detect host disconnect via heartbeat timeout
// 2. Each peer independently evaluates whether it should become the new host
TArray<FPlayerInfo> remainingPeers = GetConnectedPeers();
FPlayerInfo newHost = SelectNewHost(remainingPeers); // Lowest latency, highest bandwidth
if (newHost.PlayerId == GetLocalPlayerId()) {
// This peer becomes the new host
BecomeHost();
// Reconstruct authoritative game state from local cache
GameState = ReconstructFromLastKnownState();
// Tell all other peers to connect to the new host
BroadcastHostMigration(newHost.Address);
// Resume gameplay - votes, timers, and role assignments must survive this transition
ResumeSessionWithReconciledState();
} else {
// Wait for migration signal, then connect to new host
ConnectToNewHost(newHost.Address, timeoutSeconds);
}
}
각 단계는 잠재적인 실패 지점입니다. 두 피어가 동시에 자신이 호스트가 되어야 한다고 결정하면(분할 뇌 시나리오), 조정할 수 없는 두 개의 분기된 게임 상태가 생성됩니다.
NAT traversal 실패: 대칭형 NAT 또는 통신사 등급 NAT 뒤에 있는 플레이어는 직접 연결을 설정할 수 없습니다. TURN 릴레이 인프라가 이 플레이어들을 수용해야 합니다. 대규모环境中, 70만 명의 동시 접속자 중 15%는 10만 5천 명의 플레이어가 릴레이 트래픽을 필요로 함을 의미합니다. 그리고 릴레이 대역폭은 비싸며, 일반적으로 GB당 $0.05~$0.10입니다.
치트 취약성: 호스트 플레이어의 머신이 권위적입니다. 모든 클라이언트 측 데이터는 조작될 수 있습니다. 캐주얼 파티 게임의 경우 경쟁 슈팅 게임보다 덜 치명적이지만, 여전히 경험을 저하시킵니다. 서버 권위적 설계(예: Fortnite 서버 최적화 제안에서 사용된)는 전체 익스플로잇 범주를 제거하지만 전용 컴퓨팅 리소스가 필요합니다.
매치메이킹 레이어: 예상 피크의 10배를 위해 구축하기
이 섹션은 대부분의 인디 개발자에게 필요하지만 때가 늦을 때까지 건너뛰는 부분입니다. 매치메이킹 시스템은 게임의 현관문입니다. 느리면 플레이어가 떠나고, 고장 나면 플레이어가 게임을 할 수 없습니다.
로비 상태 관리 아키텍처
Goose Goose Duck의 로비 시스템은 대규모에서 다음 작업을 처리해야 했습니다:
- 로비 탐색 (읽기 중심): 플레이어가 사용 가능한 로비를 필터링하고 나열함
- 로비 생성 (쓰기): 게임 설정, 지역, 수용 인원이 포함된 새 로비 레코드
- 로비 참가 (조건부 쓰기): 원자적 작업—수용 인원 확인, 플레이어 추가, 또는 실패
- 로비 탈퇴 (쓰기 + 삭제 가능): 플레이어 제거, 빈 로비 삭제
- 로비 설정 업데이트 (쓰기): 호스트가 게임 매개변수 수정
다음은 부하 상태에서 가장 실패하기 쉬운 원자적 참가 작업을 처리하는 간소화된 로비 관리자입니다:
import asyncio
from dataclasses import dataclass, field
from typing import Optional
import uuid
@dataclass
class Lobby:
lobby_id: str
host_id: str
max_players: int
players: list = field(default_factory=list)
region: str = "us-east"
game_settings: dict = field(default_factory=dict)
created_at: float = 0.0
class LobbyManager:
def __init__(self, cache_client, db_client):
self.cache = cache_client # Redis or similar
self.db = db_client # PostgreSQL or similar
self.MAX_LOBBIES_PER_REGION = 10000
self.LOBBY_TTL_SECONDS = 3600 # Auto-cleanup stale lobbies
async def join_lobby(self, lobby_id: str, player_id: str) -> dict:
"""
Atomic join operation using Redis optimistic locking.
Prevents the race condition where two players simultaneously
join a lobby that has one slot remaining.
"""
cache_key = f"lobby:{lobby_id}"
# Use a Lua script for atomic check-and-modify in Redis
# This is the critical path—under viral load, this single
# operation runs thousands of times per second
lua_script = """
local key = KEYS[1]
local player_id = ARGV[1]
local max_players = tonumber(ARGV[2])
local lobby_data = redis.call('HGETALL', key)
if #lobby_data == 0 then
return {-1, "lobby_not_found"}
end
-- Parse the player count from the hash
local current_players = tonumber(redis.call('HGET', key, 'player_count'))
if current_players == nil then
return {-1, "corrupted_state"}
end
if current_players >= max_players then
return {0, "lobby_full"}
end
-- Atomic increment and add player
redis.call('HINCRBY', key, 'player_count', 1)
redis.call('SADD', key .. ':players', player_id)
redis.call('EXPIRE', key, 3600)
return {1, "joined"}
"""
result = await self.cache.eval(
lua_script,
keys=[cache_key],
args=[player_id, str(self.MAX_PLAYERS)]
)
status_code, message = result
if status_code == -1:
raise LobbyNotFoundException(message)
elif status_code == 0:
raise LobbyFullException(message)
# Async write to persistent DB (non-blocking, eventual consistency is fine here)
asyncio.create_task(self._persist_join(lobby_id, player_id))
return {"status": "joined", "lobby_id": lobby_id}
async def _persist_join(self, lobby_id: str, player_id: str):
"""Background persistence—lobby state in Redis is the source of truth for joins.
DB only lags by milliseconds but is not on the critical path."""
await self.db.execute(
"UPDATE lobbies SET player_count = player_count + 1, "
"updated_at = NOW() WHERE lobby_id = $1",
lobby_id
)
await self.db.execute(
"INSERT INTO lobby_players (lobby_id, player_id, joined_at) "
"VALUES ($1, $2, NOW()) ON CONFLICT DO NOTHING",
lobby_id, player_id
)
여기서 핵심 세부 사항은 Redis의 Lua 스크립트입니다. GET을 수행하고, 애플리케이션 코드에서 수용 인원을 확인한 다음 POST를 수행하는 순진한 구현은 15명의 플레이어가 동시에 16인 로비에 참가할 수 있는 경쟁 조건을 만들어 17명의 플레이어와 깨진 게임 로직을 초래합니다. Lua 스크립트는 Redis 내부에서 원자적으로 실행되므로, 초당 수천 건의 작업에서도 경쟁 조건이나 참가 누락이 발생하지 않습니다.
연결 핸드오프: 로비에서 게임플레이로
로비가 가득 차면, 게임은 중앙 집중식 로비 조정에서 P2P 게임플레이로 전환해야 합니다. 이 핸드오프는 대부분의 인디 멀티플레이어 게임에서 지연 스파이크 또는 완전한 실패를 초래하는 지점입니다.
작동하는 패턴은 다음과 같습니다:
- 호스트 플레이어가 WebSocket 또는 UDP 수신 소켓을 엽니다
- 서버(로비 시스템)가 호스트의 IP와 포트를 모든 피어에게 배포합니다
- 피어가 STUN을 통해 직접 P2P 연결을 시도합니다
- STUN이 N초 이내에 실패하면 TURN 릴레이로 폴백합니다
- 모든 피어가 연결되었다고 보고하면 호스트가 게임 시작을 알립니다
이 핸드오프 중 실시간 통신을 위해, WebSocket 연결은 HTTP 폴링보다 훨씬 안정적이며, 특히 8~16명의 클라이언트에게 연결 상태 업데이트를 동시에 푸시해야 할 때 그렇습니다.
바이럴 스파이크 중 트래픽 셰이핑
Goose Goose Duck 팀이 한 가장 현명한 일 중 하나는 피크 트래픽 중 기대치를 관리한 것입니다. 백엔드가 용량에 도달했을 때, 두 가지 옵션이 있습니다: 모든 것을 예측 불가능하게 저하시키거나(무작위 연결 끊김, 손상된 로비 상태, 타임아웃 오류), 또는 우아한 저하를 구현하는 것입니다.
우아한 저하 패턴
연결 큐잉: 로비 서버가 용량에 도달했을 때 플레이어를 거부하는 대신, 실시간 위치 카운터가 있는 가상 큐에 배치합니다. 플레이어는 2분 정도 기다릴 의향이 있습니다. 하지만 알 수 없는 "서버 오류" 메시지는 용납하지 않습니다.
// C# connection queue with position feedback
public class ConnectionQueue
{
private readonly ConcurrentQueue<string> _queue = new();
private readonly SemaphoreSlim _admissionGate;
private readonly int _maxConcurrentSessions;
public ConnectionQueue(int maxConcurrentSessions)
{
_maxConcurrentSessions = maxConcurrentSessions;
_admissionGate = new SemaphoreSlim(maxConcurrentSessions, maxConcurrentSessions);
}
public async Task<QueueResult> TryEnterQueue(string playerId)
{
int position = _queue.Count + 1;
_queue.Enqueue(playerId);
// Estimate wait time: assume ~30 second average session search time
// at current throughput
int estimatedWaitSeconds = (position / _maxConcurrentSessions) * 30;
if (_admissionGate.CurrentCount > 0)
{
await _admissionGate.WaitAsync();
_queue.TryDequeue(out _);
return new QueueResult { Admitted = true, Position = 0 };
}
return new QueueResult
{
Admitted = false,
Position = position,
EstimatedWaitSeconds = estimatedWaitSeconds
};
}
}
지역별 부하 분산: US-East가 과부하되었지만 EU-West에 여유가 있다면, 연결을 완전히 거부하는 대신 지연 시간 경고와 함께 새 US 플레이어를 EU로 리디렉션합니다. 소셜 디덕션 게임에서 120ms 핑은 거의 눈에 띄지 않습니다. 이들은 프레임 완벽한 격투 게임이 아닙니다.
로비 생성 속도 제한: 피크 부하 동안, 로비 생성을 플레이어당 30초에 하나로 제한합니다. 이는 봇 기반 로비 스팸(Goose Goose Duck에서 실제 문제였음)을 방지하고 로비 데이터베이스에 대한 쓰기 압력을 줄입니다.
비용 분석: 바이럴 규모의 실제 비용
이제 실제 숫자를 적용해 보겠습니다. 다양한 백엔드 아키텍처를 사용한 Goose Goose Duck 규모의 바이럴 이벤트에 대한 대략적인 비용 모델입니다:
*중앙 집중식 로비 조정을 사용한 P2P (Goose Goose Duck이 사용한 방식):*
| 구성 요소 | 월간 비용 (최대 동시 접속자 70만 명 기준) |
|---|---|
| 로비/매치메이킹 서버 (c5.2xlarge 인스턴스 12개, 오토스케일링) | $3,500–$5,000 |
| 로비 상태용 Redis 클러스터 (3노드, r6g.xlarge) | $1,800 |
| TURN 릴레이 서버 (트래픽의 15%, 약 10만 명) | $8,000–$15,000 |
| 영구 상태용 PostgreSQL (RDS Multi-AZ) | $600 |
| 대역폭 (로비 조정, 하루 약 2TB) | $1,200 |
| 합계 | $15,100–$23,600/월 |
완전 전용 서버 (모든 매치가 클라우드 VM에서 실행):
| 구성 요소 | 월간 비용 (최대 동시 접속자 70만 명 기준) |
|---|---|
| 게임 서버 (~5만 개 동시 매치 × $0.04/시간) | $1,440,000/월 |
| 매치메이킹 및 로비 | $5,000 |
| 데이터베이스 인프라 | $2,000 |
| 합계 | ~$1,447,000/월 |
비용 차이는 두 자릿수입니다. 코스메틱으로 수익을 내는 무료 플레이 게임의 경우, 전용 서버 모델은 첫날부터 공격적인 수익화를 하지 않는 한 파산으로 가는 직접적인 경로입니다. P2P는 게으른 아키텍처가 아니라 의도적인 재정적 결정입니다.
그러나 비용 절감에는 트레이드오프가 따릅니다. 치트 심각도가 증가합니다. 연결 품질은 호스트에 따라 다릅니다. 그리고 조정 인프라는 게임의 모든 매치에 대한 단일 장애 지점이기 때문에 무적이어야 합니다.
자신의 프로젝트에 대해 이러한 트레이드오프를 평가하고 있다면, horizOn은 로비 관리, 매치메이킹, 플레이어 인증 및 세션 상태를 포함한 조정 레이어를 처리하므로 인프라가 아닌 게임플레이에 집중할 수 있습니다. 이 플랫폼은 특히 인디 팀이 백엔드 엔지니어링에 몇 달을 할애하지 않고 확장할 수 있도록 이 사용 사례를 위해 구축되었습니다.
바이럴 성장에서 살아남기 위한 5가지 백엔드 아키텍처 패턴
다음은 필요해지기 전에 구현해야 하는 구체적인 패턴입니다. 트래픽 스파이크 중에 이들을 개조하는 것은 백엔드 화재를 백엔드 장례식으로 만드는 길이기 때문입니다.
1. 로비 상태와 게임 상태 분리
로비 조정 시스템과 실제 게임플레이 네트워킹은 확장 프로필이 다른 별개의 시스템입니다. 로비 상태는 읽기 집약적이고 쓰기는 중간 정도이며 캐싱(Redis)이 유리합니다. 게임 상태는 고빈도, 저지연이며 호스트 머신이나 전용 서버에 속합니다. 이 둘을 하나의 데이터베이스에 혼합하는 것은 확장성의 함정입니다.
2. 용량에 민감한 쓰기에 원자적 작업 사용
위에서 보여준 로비 참가 작업은 Redis Lua를 통해 원자적입니다. 게임 방이 초과 수용되는지 여부를 결정하는 모든 것에 애플리케이션 수준 잠금에 의존하지 마십시오. 초당 2,000건의 참가에서 10ms의 경쟁 조건이라도 20개의 초과 판매 로비가 발생합니다.
3. 필요해지기 전에 연결 큐잉 구현
예상 대기 시간이 60초인 큐는 플레이어의 70~80%를 유지합니다. 일반적인 "연결 실패" 오류는 거의 0%를 유지합니다. 초기 아키텍처에 큐 시스템을 구축하십시오. 트래픽이 적을 때는 비활성화할 수 있지만, 트래픽이 급증할 때는 충분히 빠르게 구축할 수 없습니다.
4. 로비-게임 전환을 별도로 모니터링
대부분의 모니터링 시스템은 "전체 온라인 플레이어 수"와 "오류율"을 추적합니다. 전환 지점에 대한 특정 메트릭이 필요합니다: 가득 찬 로비 중 게임플레이로 성공적으로 전환되는 비율은? 이 수치가 95% 아래로 떨어지면 STUN/TURN 인프라 또는 P2P 홀 펀칭 로직이 실패하고 있는 것입니다. 이 메트릭은 다른 어떤 것보다 플레이어 이탈을 더 정확하게 예측합니다.
5. 우아한 저하 사다리 구축
저하 조건을 미리 정의하십시오:
- 초록 (용량의 80% 미만): 완전한 기능, 제한 없음
- 노랑 (용량의 80~95%): 로비 생성 속도 제한 활성화, 플레이어가 기존 로비에 참가하도록 유도
- 주황 (용량의 95~100%): 연결 큐잉 활성화, 매치메이킹 필터 비활성화, 교차 지역 매치 허용
- 빨강 (용량 초과): 전체 큐잉, 새 연결에 대한 정적 폴백 페이지, 기존 세션 우선 처리
이러한 임계값을 인프라 구성에 기록하십시오. 각 경계에서 알림을 설정하십시오. "바이럴 순간"과 "바이럴 재앙"의 차이는 빨강에 도달하기 전에 주황에 도달하는지 여부입니다.
더 큰 교훈
Goose Goose Duck 이야기는 멀티플레이어 게임 아키텍처에 대한 근본적인 사실을 보여줍니다: P2P와 전용 서버 사이의 결정은 품질 결정이 아니라, 연쇄적인 결과를 초래하는 경제적 및 아키텍처 결정입니다. P2P는 Gaggle Studios에게 잠재적으로 수백만 달러의 서버 비용을 절약해 주었지만, 강력한 조정 레이어, 세심한 로비 관리, 그리고 특정 품질 트레이드오프를 수용하려는 의지가 필요했습니다.
멀티플레이어 아키텍처를 계획하는 인디 개발자에게 교훈은 분명합니다: 평균이 아닌 피크를 위해 설계하십시오. 게임이 바이럴되는 날, 백엔드는 정상 부하의 50~100배를 경험할 것입니다. 그 규모에서 테스트하지 않았다면, 준비가 되지 않은 것입니다.
로비 및 매치메이킹 레이어부터 시작하십시오. 원자적 로비 작업을 제대로 구현하십시오. 연결 큐잉을 구축하십시오. 지역 장애 조치를 구현하십시오. 이것들이 바이럴 순간을 성공 사례로 만들지, 사후 분석으로 만들지를 결정하는 구성 요소입니다.
몇 달 간의 백엔드 엔지니어링을 건너뛰고 이미 대규모에서 검증된 조정 레이어와 함께 출시하고 싶다면, horizOn은 로비 관리, 매치메이킹 및 세션 상태를 즉시 제공합니다—서버가 버틸 수 있을지 고민하는 대신 게임을 재미있게 만드는 데 집중할 수 있습니다. API 문서를 확인하여 아키텍처에 어떻게 맞는지 알아보십시오.
출처: Staying Lean: How We Built the World's Biggest Social Deduction Game