게임 개발 소스코드 보안과 세이브 파일 변조 방지의 경계

클라이언트 게임에서 지켜야 할 정보와 신뢰할 수 없는 데이터를 구분하고 세이브 파일 변조를 줄이는 실용적인 설계 원칙을 정리한다.

✓ 복사 완료!
챕터데이터 저장 & 직렬화
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
게임 개발 소스코드 보안과 세이브 파일 변조 방지의 경계

핵심 요약

PC와 모바일 기기에 배포한 게임 클라이언트는 결국 사용자가 실행하고 분석할 수 있는 프로그램이다. 난독화와 무결성 검사는 분석 비용을 높일 수 있지만 클라이언트 안에 들어간 비밀값이나 판정 로직을 영구적으로 숨겨 주지는 않는다.

따라서 보안 설계의 출발점은 ‘숨길 수 있는가’보다 ‘변조되어도 어떤 피해가 발생하는가’에 둬야 한다. 순위, 재화, 결제, 멀티플레이 판정처럼 다른 플레이어 또는 서비스 데이터에 영향을 주는 값은 서버가 최종 권한을 가져야 한다.

flowchart LR
    A[게임 클라이언트] --> B{데이터 성격}
    B -->|로컬 진행과 설정| C[세이브 파일]
    B -->|재화·랭킹·보상| D[서버 권한 데이터]
    C --> E[무결성 검증과 복구 정책]
    D --> F[서버 검증과 감사 로그]

이 구분은 작은 싱글플레이 게임에도 도움이 된다. 로컬 세이브가 변조되더라도 플레이 경험 안에서 끝나는지 도전 과제나 클라우드 동기화처럼 외부 보상으로 이어지는지를 먼저 따져 볼 수 있다.

클라이언트와 서버의 신뢰 경계를 구분해 표현한 게임 데이터 보안 개념도

소스코드 보호는 ‘비밀 관리’와 ‘분석 비용’으로 나눈다

저장소와 빌드 환경부터 보호한다

게임 프로젝트에는 소스코드 외에도 배포 키, 스토어 인증서, 서버 토큰, 분석 서비스 키가 섞이기 쉽다. 특히 키가 한 번 Git 이력에 들어가면 삭제 커밋만으로는 노출이 해결되지 않는다.

  • 비밀값은 환경 변수나 전용 비밀 관리 서비스에서 주입한다.
  • 저장소에 올리면 안 되는 파일은 .gitignore와 사전 커밋 검사로 차단한다.
  • 팀원과 자동 빌드 계정에는 필요한 범위만 권한을 부여한다.
  • 노출 가능성이 생긴 키는 즉시 폐기하고 새 키로 교체한다.

클라이언트에 포함되는 API 키는 공격자가 추출할 수 있다고 가정해야 한다. 정말 비밀이어야 하는 키로 권한을 부여하는 구조라면 해당 작업을 서버로 옮기는 편이 안전하다.

난독화는 방어의 한 겹일 뿐이다

C# 기반 엔진의 관리 코드나 스크립트 자산은 비교적 분석하기 쉬울 수 있다. 난독화, 심볼 제거, 릴리스 빌드 설정은 역공학의 비용을 높이는 데 유용하지만 보안 경계를 대신하지는 못한다.

예를 들어 isPremiumUser 같은 값을 클라이언트에만 저장하고 그 값으로 콘텐츠 접근을 결정하면 값의 이름을 바꿔도 근본적인 문제가 남는다. 접근 권한을 서버가 확인하거나 오프라인 게임이라면 변조를 허용 가능한 게임 규칙으로 설계하는 편이 더 현실적이다.

세이브 파일은 암호화보다 검증 정책이 중요하다

세이브 파일을 평문 JSON으로 두면 사용자가 쉽게 수정할 수 있다. 그렇다고 단순 암호화를 넣는다고 해결되지는 않는다. 복호화 키가 클라이언트에 있으면 충분한 시간과 의지가 있는 공격자는 키와 처리 흐름을 찾을 수 있기 때문이다.

가장 실용적인 목표는 다음 세 가지다.

  1. 우연한 손상과 단순 편집을 빠르게 감지한다.
  2. 비정상 데이터를 불러올 때 게임이 망가지지 않게 한다.
  3. 온라인 가치가 있는 데이터는 서버에서 다시 검증한다.

저장할 값의 범위를 줄인다

세이브에는 결과보다 재현 가능한 상태를 저장하는 편이 검증하기 쉽다. 예를 들어 경험치 총합, 소지 골드, 해금 목록을 모두 독립적으로 저장하면 값 사이의 모순이 생길 수 있다. 가능하면 퀘스트 완료 기록처럼 원인이 되는 상태를 저장하고 파생값은 불러올 때 다시 계산한다.

{
  "saveVersion": 3,
  "playerLevel": 12,
  "completedQuestIds": ["q_intro", "q_forest"],
  "inventory": [
    { "itemId": "potion_small", "count": 4 }
  ]
}

불러오는 시점에는 버전, 배열 길이, 수량 범위, 존재하는 아이템 ID인지 여부를 검증한다. 검증 실패 시 즉시 종료하기보다 마지막 정상 세이브로 되돌리거나 새 세이브를 제안하는 복구 경로를 준비하는 것이 좋다.

무결성 값은 단순 변조의 문턱을 높인다

서버가 없는 싱글플레이 게임에서는 세이브 본문과 함께 해시 기반 메시지 인증 코드(HMAC)를 저장할 수 있다. 아래 예시는 개념 설명용 의사 코드다.

string payload = Serialize(saveData);
string mac = HmacSha256(payload, localKey);
WriteFile(new SaveEnvelope {
    payload = payload,
    mac = mac
});

SaveEnvelope envelope = ReadFile();
if (!FixedTimeEquals(envelope.mac, HmacSha256(envelope.payload, localKey)))
{
    RestoreBackupOrCreateNewSave();
}

다만 localKey가 게임 클라이언트에 있으므로 이 방식은 전문적인 변조를 완전히 막지 못한다. 이 한계를 문서와 운영 정책에 명확히 적어 두면 보안 기능의 기대치를 과도하게 잡는 일을 피할 수 있다.

세이브 데이터 본문과 무결성 값을 검증한 뒤 복구 경로로 분기하는 흐름

온라인 기능이 있다면 서버 검증으로 옮긴다

클라우드 세이브, 리더보드, 시즌 보상, 거래 기능이 있다면 클라이언트가 보낸 수치를 그대로 신뢰해서는 안 된다. 서버는 가능한 한 행동 또는 이벤트를 받고 게임 규칙에 따라 결과를 계산하거나 검증해야 한다.

예를 들어 클라이언트가 gold: 999999를 전송하게 하기보다 ‘스테이지 4를 정상 완료했다’는 요청을 보내고 서버가 스테이지 보상과 일일 획득 한도를 확인한 뒤 재화를 반영하는 방식이 낫다. 요청에는 사용자 인증, 재전송 방지용 식별자, 처리 결과를 남길 감사 로그도 필요하다.

서버 검증에도 한계는 있다. 오프라인 플레이를 허용하면 나중에 동기화되는 데이터를 어떻게 다룰지 정해야 한다. 이때는 오프라인 진행을 별도 재화로 제한하거나 동기화 시점에 보상 상한과 이상치 검사를 적용하는 식으로 게임 규칙과 보안을 함께 설계한다.

출시 전 확인할 최소 점검표

데이터와 권한

  • 클라이언트에 장기 비밀키나 관리자 권한 토큰이 들어 있지 않은가?
  • 재화, 순위, 구매 권한의 최종 판정이 서버에 있는가?
  • 저장 데이터의 버전 변경과 손상 복구를 고려했는가?

변조와 운영

  • 비정상 수량, 알 수 없는 ID, 과도한 배열 크기를 거부하는가?
  • 중요한 요청에 인증과 재전송 방지 처리가 있는가?
  • 의심스러운 변경을 조사할 로그와 계정 제재 기준이 있는가?

마무리

소스코드 보호와 세이브 변조 방지는 하나의 도구로 끝나는 문제가 아니다. 클라이언트는 신뢰하지 않는다는 전제 아래, 피해가 큰 데이터는 서버가 권한을 갖고 로컬 세이브는 검증과 복구에 집중하는 구성이 기본이 된다.

인디 팀이라면 처음부터 복잡한 치트 방지 체계를 도입하기보다 데이터 분류와 서버 권한 경계를 먼저 문서화해 두는 편이 비용 대비 효과가 크다. 이후 게임의 경제 시스템과 온라인 기능이 커질수록 검증 규칙과 운영 도구를 단계적으로 보강하면 된다.