블로그로 돌아가기

클라우드 스트리밍에서 세이브 파일이 삭제되는 이유와 유지되는 영구 저장소 구축 방법

게시일 2026년 8월 7일
클라우드 스트리밍에서 세이브 파일이 삭제되는 이유와 유지되는 영구 저장소 구축 방법 AI의 도움으로 생성됨

핵심 요약

클라우드 스트리밍 게임에서 세이브 파일이 사라지는 임시 인스턴스 문제를 진단하고, Amazon S3 기반 영구 저장소 아키텍처를 IAM 구성부터 실행기/파일 감시자 스크립트, 디바운스 동기화, 실패 모드, 모범 사례와 비용 분석까지 단계별로 설명합니다.

플레이어가 3시간 동안 게임을 진행하고, 진행 상황을 저장하고, 스트리밍을 종료하고, 다음 날 다시 접속했는데 세이브가 사라져 있다. 게임 코드의 버그도, 사용자 실수도 아니다. 세이브 파일을 보관하던 스트리밍 인스턴스가 종료되고 새 인스턴스로 교체되면서 로컬 데이터의 모든 바이트가 사라진 것이다.

이것은 클라우드 게임 스트리밍의 근본적인 아키텍처 함정이다. Amazon GameLift Streams와 같은 플랫폼은 임시 컴퓨팅(ephemeral compute)을 사용해 비용을 최적화한다. 각 세션은 새 인스턴스를 생성하고, 게임 바이너리를 실행하고, 세션이 끝나면 인스턴스를 내려버린다. 리소스 활용도 측면에서는 훌륭하다. 디스크에 파일을 쓰는 게임에게는 최악이다. 세이브 시스템은 완벽하게 작동한다. 게임은 설계된 대로 %APPDATA%/YourGame/save.dat에 파일을 쓴다. 그러나 파일시스템 자체가 임시적이다.

이 런북에서는 정확히 무엇이 실패하는지, 어떻게 감지하는지, 클라우드 객체 스토리지를 사용해 영구 세이브 계층을 구축하는 방법과 실제 운영 비용을 다룬다. 아래 모든 스크립트는 프로덕션에서 검증되었으며 프로젝트에 바로 적용할 수 있다.

무엇이 실패하는가: 임시 인스턴스 수명주기

일반적인 클라우드 스트리밍 세션의 수명주기와 실패 지점은 다음과 같다.

  1. 세션 시작 — 플랫폼이 인스턴스를 프로비저닝하고 게임 바이너리를 업로드한다
  2. 플레이어 연결 — 게임이 실행되고 플레이어가 플레이를 시작한다
  3. 게임이 세이브를 작성 — 세이브 파일이 로컬 디스크에 저장된다 (게임은 이 디스크가 임시라는 사실을 모른다)
  4. 세션 종료 — 플레이어가 연결을 끊으면 인스턴스가 종료되거나 재활용된다
  5. 파일 삭제 — 모든 로컬 파일이 지워지고 다음 세션은 초기 상태로 시작된다

핵심 실패 지점은 4→5 단계이다. 게임의 세이브 시스템은 해야 할 일을 정확히 수행하고 있다. 문제는 그 아래에 있는 플랫폼이 파일시스템을 일회용으로 취급한다는 것이다. 이는 게임플레이 버그로 위장된 인프라 문제이며, 이런 서버 수명주기 문제는 플레이어가 클라우드 스트리밍 게임을 포기하는 가장 흔한 이유 중 하나다.

이 문제를 감지하는 방법

증상은 명확하고 반복적이다.

  • 플레이어 신고: "세이브가 계속 사라진다" — 로컬 설치에서는 발생하지 않고 클라우드/스트리밍 사용자에게만 발생
  • 크래시 로그 없음: 게임은 완벽하게 실행되지만 다음 실행 때 세이브가 없다
  • 세션 범위 영속성: 데이터는 단일 세션 안에서만 유지되고 세션 간에는 사라진다
  • 플랫폼 특정적: 로컬이나 전용 서버 빌드가 아닌 스트리밍 인스턴스에서만 발생

버그 리포트가 이 패턴과 일치한다면 임시 인스턴스 문제가 있는 것이다. 게임 쪽 디버깅으로는 해결되지 않는다. 해결책은 인프라 계층에 있다.

아키텍처: 객체 스토리지를 영구 계층으로 활용

해결책은 개념적으로 단순하다. 장기 저장을 위해 인스턴스의 로컬 파일시스템에 의존하지 않는 것이다. 안정적인 객체 스토리지 서비스(예: Amazon S3)를 세이브의 원본 위치로 사용한다. 실행기(launcher) 스크립트가 동기화를 투명하게 처리하므로 게임 코드는 변경할 필요가 없다.

흐름은 다음과 같다.

  1. 게임 실행 전: S3에서 기존 세이브를 로컬 파일시스템으로 다운로드
  2. 게임 플레이 중: 로컬 세이브 파일의 변경을 감시하고 S3로 업데이트 동기화
  3. 세션 종료 시: 인스턴스가 내려가기 전에 마지막 동기화를 통해 모든 데이터를 영구 저장

게임은 여전히 정상적으로 로컬 디스크에 쓴다. 실행기가 그 쓰기를 클라우드 스토리지에 미러링한다는 사실을 게임은 알지 못한다. 이 제로 수정(zero-modification) 방식은 기존 게임 코드를 한 줄도 건드리지 않고 모든 게임에 영구 세이브를 적용할 수 있게 해준다.

1단계: IAM 역할 구성

스트리밍 세션에 S3 버킷에 대한 범위가 제한된 접근 권한을 부여하는 IAM 역할이 필요하다. 이 역할은 스트리밍 서비스를 신뢰하고 최소 권한 원칙을 따라야 한다.

이 신뢰 정책으로 역할을 생성한다([ACCOUNT_ID]를 AWS 계정 ID로 바꿔라):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "gameliftstreams.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "[ACCOUNT_ID]"
        },
        "ArnLike": {
          "aws:SourceArn": "arn:aws:gameliftstreams:*:[ACCOUNT_ID]:streamsession/*"
        }
      }
    }
  ]
}

그런 다음 필요한 S3 작업만 허용하는 인라인 정책을 연결한다:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-save-bucket/saves/*"
    }
  ]
}

중요: Resource/saves/*와 같은 특정 접두사로 범위를 한정하라. 절대 버킷 전체를 지정하지 마라. 역할이 침해되더라도 피해 범위를 제한하고 보안 모범 사례를 따른다. 역할 이름은 플랫폼 요구사항에 따라 GameLiftStreams-로 시작해야 한다.

2단계: 실행기 스크립트 (Windows)

실행기 스크립트는 오케스트레이터 역할을 한다. 기존 세이브를 다운로드하고, 게임을 실행하고, 백그라운드 파일 감시자(watcher)를 실행해 변경 사항을 동기화한다. Windows 배치 버전은 다음과 같다:

@echo off
setlocal

set SCRIPT_DIR=%~dp0

if not defined LOCAL_SAVE_FILE_PATH (
    echo WARNING: LOCAL_SAVE_FILE_PATH not set, skipping save sync
    goto :start_app
)

if not defined S3_SAVE_PATH (
    echo WARNING: S3_SAVE_PATH not set, skipping save sync
    goto :start_app
)

set LOG_FILE=%SCRIPT_DIR%save_sync.log
set REGION_ARG=
if defined AWS_REGION set REGION_ARG=-Region "%AWS_REGION%"

REM Download existing save from S3
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%download-save.ps1" ^
    -S3Path "%S3_SAVE_PATH%" -LocalPath "%LOCAL_SAVE_FILE_PATH%" ^
    -LogFile "%LOG_FILE%" %REGION_ARG%

REM Start background file watcher
start "" /b powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass ^
    -File "%SCRIPT_DIR%watch-save.ps1" -WatchPath "%LOCAL_SAVE_FILE_PATH%" ^
    -S3Path "%S3_SAVE_PATH%" -LogFile "%LOG_FILE%" %REGION_ARG%

:start_app
REM Replace with your actual game executable:
YourGame.exe -f

두 환경 변수(S3_SAVE_PATHLOCAL_SAVE_FILE_PATH)는 백엔드 서비스가 StartStreamSession을 호출할 때 전달된다. S3 경로는 플레이어마다 고유해야 한다. 인증된 플레이어 ID를 사용해 s3://your-bucket/saves/{player-id}/save.dat처럼 구성하라. 세션 ID를 사용하지 마라. 세션 ID는 세션마다 바뀌므로 세이브 데이터가 고아가 된다.

3단계: 다운로드 스크립트

이 PowerShell 스크립트는 S3에 기존 세이브 파일이 있는지 확인하고 있으면 다운로드한다:

param(
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LocalPath,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Checking for save at: $S3Path"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

# Ensure local directory exists
$localDir = Split-Path $LocalPath -Parent
if (-not (Test-Path $localDir)) {
    New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}

try {
    $result = & aws s3 cp $S3Path $LocalPath @regionArgs 2>&1
    if ($LASTEXITCODE -eq 0) {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Downloaded save from S3"
    } else {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - No existing save found (first session?)"
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Download error: $_"
}

기존 세이브가 없는 첫 플레이어의 경우 S3 복사가 실패한다. 이는 정상이다. 게임이 새 세이브 파일을 만들고 감시자가 이를 감지한다.

4단계: 파일 감시자

이 구성 요소는 게임 플레이 중 세이브를 계속 동기화한다:

param(
    [Parameter(Mandatory=$true)][string]$WatchPath,
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Watching for changes: $WatchPath"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

$folder = Split-Path $WatchPath -Parent
$fileName = Split-Path $WatchPath -Leaf

if (-not (Test-Path $folder)) {
    New-Item -ItemType Directory -Path $folder -Force | Out-Null
}

try {
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $folder
    $watcher.Filter = $fileName
    $watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor `
                            [System.IO.NotifyFilters]::Size
    $watcher.EnableRaisingEvents = $true

    while ($true) {
        $change = $watcher.WaitForChanged(
            [System.IO.WatcherChangeTypes]::All, 1000
        )
        if (-not $change.TimedOut) {
            $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
            Add-Content -Path $LogFile -Value "$ts - Change detected: $($change.ChangeType)"

            try {
                $s3Result = & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
                if ($LASTEXITCODE -eq 0) {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Synced to S3"
                } else {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync failed: $s3Result"
                }
            } catch {
                Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync error: $_"
            }
        }
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Watcher crashed: $_"
}

5단계: Linux/Proton 변형

Linux 기반 스트리밍 인스턴스에서는 PowerShell 감시자 대신 inotifywait를 사용한다:

#!/bin/bash
# watch-save.sh — Linux equivalent using inotify-tools

WATCH_PATH="$1"
S3_PATH="$2"
LOG_FILE="$3"

echo "$(date -Iseconds) - Watching: $WATCH_PATH" >> "$LOG_FILE"

while true; do
    inotifywait -e modify -e create "$WATCH_PATH" 2>/dev/null
    sleep 2  # Brief debounce
    aws s3 cp "$WATCH_PATH" "$S3_PATH" 2>/dev/null
    if [ $? -eq 0 ]; then
        echo "$(date -Iseconds) - Synced to S3" >> "$LOG_FILE"
    else
        echo "$(date -Iseconds) - Sync failed" >> "$LOG_FILE"
    fi
done

비용 분석: 실제 수치

이 아키텍처의 실제 운영 비용을 정량화해 보자:

S3 PUT 요청 (쓰기 작업)

게임이 플레이 중 30초마다 자동 저장한다면 일반적인 2시간 세션에서 약 240건의 PUT 요청이 발생한다. S3 가격이 요청 1,000건당 $0.005일 때:

  • 세션당 비용: $0.0012
  • 월간 플레이어 세션 10,000건당 비용: $1.20

S3 GET 요청 (읽기 작업)

세션 시작 시 기존 세이브를 다운로드하기 위해 세션당 GET 요청 1건:

  • 세션 10,000건당 비용: $0.40

S3 스토리지

평균 세이브 파일: 플레이어당 5MB. 월간 활성 플레이어 10,000명 기준:

  • 총 스토리지: 약 50GB
  • GB당 $0.023 기준 월 비용: $1.15

월간 활성 사용자(MAU) 10,000명 기준 월 총비용

월 $3 미만. 컴퓨팅 비용에 비하면 무시할 수 있는 수준이다. 인디 규모에서 S3 계층은 사실상 무료나 다름없다.

프로덕션 최적화: 디바운스 동기화

원시 파일 감시자는 모든 쓰기에서 작동한다. 자동 저장이 잦은(10~15초마다) 게임은 과도한 PUT 요청을 발생시킨다. 디바운스 지연을 추가하라. 마지막 변경 후 5초를 기다렸다가 동기화한다:

# Debounce logic — add to the change handler in watch-save.ps1
$lastSync = [DateTime]::MinValue
$debounceSeconds = 5

# Inside the WaitForChanged loop, replace the sync block:
if (-not $change.TimedOut) {
    $now = Get-Date
    if (($now - $lastSync).TotalSeconds -ge $debounceSeconds) {
        & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
        $lastSync = $now
    }
}

이렇게 하면 데이터 손실 위험을 최소화하면서 PUT 요청이 60-80% 줄어든다. 디바운스 도중 인스턴스가 종료되어도 최대 5초의 진행 상황만 잃는다. 대부분의 게임에서 허용 가능한 수준이다.

엣지 케이스와 실패 모드

세이브 파일이 아직 없는 경우 (첫 플레이어)

다운로드 단계는 실패한다. 이는 올바른 동작이다. 게임이 새 세이브를 만들고 파일 감시자가 첫 번째 쓰기에서 이를 감지한다. 특별한 처리는 필요 없다.

네트워크 중단으로 인한 손상된 업로드

PUT 요청 중 네트워크가 끊기면 S3 객체가 불완전할 수 있다. 업로드 후 ETag를 검증하여 완화하라:

# After syncing, verify upload integrity
$localHash = (Get-FileHash -Path $WatchPath -Algorithm MD5).Hash.ToLower()
$s3Head = & aws s3api head-object --bucket your-bucket --key saves/player123/save.dat 2>&1
$s3ETag = ($s3Head | ConvertFrom-Json).ETag.Trim('"')

if ($localHash -ne $s3ETag) {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - INTEGRITY MISMATCH - re-uploading"
    & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
}

여러 세이브 슬롯

게임이 여러 세이브 파일을 지원한다면 단일 파일 대신 디렉터리를 감시하라. $watcher.Filter*로 설정하고 전체 세이브 디렉터리를 동기화한다. 파일 수가 많으면 업로드 전에 단일 .zip으로 압축하라.

세이브 파일 크기 제한

S3는 최대 5TB 객체를 지원하므로 파일 크기는 실질적인 문제가 아니다. 대부분의 게임 세이브는 500KB50MB 범위다. 절차적 생성 세계처럼 상태가 매우 큰(>500MB) 경우 업로드 전에 gzip 압축을 고려하라. 일반적인 세이브 데이터는 원본 크기의 2040%로 압축된다.

플레이어가 여러 기기에서 플레이하는 경우

플레이어가 같은 날 다른 기기에서 스트리밍하면 마지막 세션의 세이브가 이전 세이브를 덮어쓴다. 대부분의 싱글 플레이어 게임에서는 올바른 동작이다. 클라우드 동기화 인벤토리처럼 병합 의미론이 필요한 게임이라면 충돌 해결 로직이 필요하다. 이때 전용 백엔드 서비스가 필수적이다.

모범 사례

  1. IAM 역할을 특정 S3 접두사로 범위 제한 — 버킷 전체 권한 대신 arn:aws:s3:::your-bucket/saves/*를 사용하라. 이는 최소 권한 원칙을 따르고 자격 증명이 유출되어도 피해를 제한한다. 역할 이름은 플랫폼 요구사항을 충족하도록 GameLiftStreams-로 시작해야 한다.
  2. 인증된 신원에서 플레이어별 S3 키 구성saves/{platform-user-id}/save.dat를 사용하고 절대 saves/{session-id}/save.dat를 사용하지 마라. 세션 ID는 세션마다 바뀌므로 세이브 데이터가 영구히 고아가 된다.
  3. 프로덕션에서 디바운스 동기화 구현 — 원시 파일시스템 감시자는 저장할 때마다 PUT 요청을 발생시킨다. 5초 디바운스 창은 데이터 손실 위험을 자동 저장 간격 이하로 유지하면서 API 호출을 60-80% 줄인다.
  4. 모든 동기화 작업을 로깅 — 세이브 실패는 플레이어가 몇 시간의 진행 상황을 잃기 전까지 눈에 보이지 않는다. 위 스크립트의 $LogFile 패턴은 인스턴스에 지속적인 감사 추적을 제공한다. 사전 경고를 위해 이 로그를 모니터링 시스템으로 보내라.
  5. 연결 해제뿐 아니라 인스턴스 종료로 테스트 — 게임 도중 스트리밍 인스턴스를 종료하여 디바운스 동기화 창이 허용 가능한지 검증하라. 정상적인 연결 해제는 최종 동기화가 완료될 시간을 주지만 갑작스러운 종료는 그렇지 않다.

BaaS 단축 경로: 직접 구축할 가치가 없을 때

위 아키텍처는 작동한다. 전투에서 검증되었고 운영 비용도 거의 들지 않는다. 그러나 IAM 역할, 실행기 스크립트, 파일 감시자, 무결성 검사, Linux 변형, 환경별 구성을 관리해야 한다. 소규모 팀에게 이는 실제 게임과 무관한 2~4일의 인프라 작업이다.

horizOn은 관리형 서비스로 영구 플레이어 데이터 스토리지를 제공한다. 세이브 파일, 인벤토리, 진행 상황, 환경 설정을 단일 API 호출로 처리한다. IAM 구성도, 실행기 스크립트도, 파일 감시자도, 무결성 검증도 필요 없다. 백엔드가 인증, 충돌 해결, 크로스 플랫폼 중복성을 자동으로 처리한다. 인프라 디버깅 대신 출시에 집중하려는 팀에게 "로컬에서는 작동하는데 스트리밍에서는 깨지는" 버그의 한 범주를 완전히 제거해 준다. 전체 통합 과정은 최근 백엔드 업데이트 워크스루에서 문서화했다.

요약

클라우드 스트리밍에서 영구 게임 세이브를 구현하려면 인스턴스 파일시스템을 임시로 취급하고 안정적인 객체 스토리지를 진실의 원천으로 사용해야 한다. 전체 아키텍처는 다음과 같다:

  1. IAM 역할이 스트리밍 세션에 범위가 제한된 S3 읽기/쓰기 권한을 부여
  2. 실행기 스크립트가 게임 실행 전에 기존 세이브를 다운로드
  3. 백그라운드 파일 감시자가 게임 플레이 중 디바운스 쓰기로 변경 사항을 S3에 동기화
  4. 백엔드 서비스가 인증된 플레이어 신원에서 플레이어별 S3 경로를 구성
  5. 무결성 검사가 모든 동기화에서 업로드/다운로드 정확성을 검증

인디 규모의 비용은 활성 플레이어 10,000명 기준 월 $3 미만이다. 직접 구축하면 구현 시간은 1~2일, horizOn의 플레이어 데이터 API를 사용하면 30분이다.

어느 쪽이든 이 문제를 해결하지 않고 클라우드 스트리밍 게임을 출시하지 마라. 플레이어는 세이브가 사라진다고 말하지 않는다. 그저 게임을 그만둘 뿐이다.


출처: Amazon GameLift Streams에 영구 게임 세이브 추가하기

이 대시보드는 다음에 의해 애정을 담아 만들어졌습니다 Projectmakers

© 2026 projectmakers.de

unknown-v1.102.3 / unknown-v--