포스트

'마녀:귀찮아!' DevLog 07 — 유저 컨텐츠 보호의 아키텍처

WM에 UGC 샌드박스 검증 시스템을 도입한 기록. 플레이어 생성 컨텐츠를 안전하게 검증하는 설계, 구현, 테스트 전략.

'마녀:귀찮아!' DevLog 07 — 유저 컨텐츠 보호의 아키텍처

머리말


WM에 유저 생성 컨텐츠(UGC) 시스템을 본격 도입하기로 했다. 플레이어가 자신만의 아이템, 스토리, 월드를 만들 수 있게 하면 게임의 생명력이 길어진다. 하지만 유저 데이터는 신뢰할 수 없다 — 악의적이든 실수든, 잘못된 컨텐츠가 게임을 부셔버릴 수 있다.

그래서 검증 시스템을 설계했다. 이 글은 그 과정을 기록한다.

문제: 신뢰할 수 없는 입력


기존에는 개발자가 만든 아이템, NPC, 스토리만 로드했다. 데이터는 모두 git에 있고, 우리가 검수했다.

하지만 유저 생성 컨텐츠는 다르다:

  • 플레이어가 로컬 파일을 직접 수정할 수 있다.
  • JSON을 손으로 조작해 불가능한 아이템을 만들 수 있다 (체력 -999, 공격력 999999).
  • 게임 엔진이 예상하지 못한 필드가 들어올 수 있다.

결론: 유저 데이터는 철저히 검증해야 한다.

설계: 3단계 방어선


Phase A: 스키마 검증

먼저 JSON 구조 자체가 게임 도메인 모델과 맞는지 확인한다.

  • UGCDataSO (ScriptableObject): WM의 아이템/NPC/스토리 데이터 정의
  • UGCJsonSerializer: JSON을 읽을 때 스키마 체크
    • 필수 필드 누락? → 거부
    • 예상치 못한 필드? → 경고 (향후 호환성)
    • 필드 타입 불일치? → 거부
1
2
3
4
5
6
7
8
9
10
11
12
13
// 의사코드
public bool ValidateSchema(JObject userJson)
{
    foreach (var field in domainSchema.Fields)
    {
        if (field.IsRequired && !userJson.ContainsKey(field.Name))
            return false; // 필수 필드 누락
        
        if (userJson[field.Name].Type != field.Type)
            return false; // 타입 불일치
    }
    return true;
}

Phase B: 시드 데이터 로딩

공식 샘플 데이터(seed data)를 먼저 로드해 기준을 정한다.

  • SeedDataSO: 게임에서 “정상” 데이터의 범위 정의
  • SeedLoader: 샘플 JSON을 읽고 범위 저장
    • 아이템 가격 범위: 10~10000
    • 스텟 범위: 체력 1~100, 공격력 0~50

유저 데이터는 이 범위 내에 있어야 한다.

Phase C: 샌드박스 검증

실제 유저 파일을 로드할 때, 위 기준들을 적용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public bool ValidateUGCData(UGCData userData)
{
    // 1. 스키마 검증
    if (!ValidateSchema(userData.Json))
        return false;
    
    // 2. 범위 검증 (시드 기준)
    if (userData.ItemPrice < seedData.MinPrice || 
        userData.ItemPrice > seedData.MaxPrice)
        return false; // "해킹된" 가격
    
    // 3. 논리 검증
    if (userData.MagicCost > userData.ManaCap)
        return false; // 불가능한 마나 소비
    
    return true;
}

구현 세부


파일 구조

1
2
3
4
5
6
7
8
9
10
11
12
Assets/
├── Domain/SDK/
│   ├── UGCJsonSerializer.cs    # JSON 파싱 + 스키마 검증
│   ├── UGCDataSO.cs            # 데이터 정의
│   └── ...
├── Domain/
│   ├── SeedDataSO.cs           # 시드 데이터 저장소
│   └── SeedLoader.cs           # 시드 로딩 로직
└── Tests/
    ├── UGCSerializerTests.cs
    ├── SandboxValidationTests.cs
    └── ... (18개 단위테스트)

테스트

  • Roundtrip 테스트: JSON → Object → JSON가 같은가?
  • 스키마 테스트: 잘못된 필드는 거부하는가?
  • 샌드박스 테스트: 해킹된 값(체력 -1, 공격력 9999)은 거부하는가?
  • 호환성 테스트: 향후 필드 추가 시 기존 파일도 로드 가능한가?

총 18개 테스트, 모두 통과.

배운 점


1. 도메인과 직렬화를 분리하자

처음에는 UGCData 클래스 하나에 모든 것을 때려박았다. 검증, 변환, 저장을 섞었다.

나중에 분리했다:

  • UGCData = 순수 도메인 객체 (검증 로직 X)
  • UGCJsonSerializer = JSON ↔ 객체 변환만
  • SandboxValidator = 비즈니스 규칙 검증

결과: 각각 테스트 가능해졌고, 나중에 XML이나 Binary 형식도 쉽게 추가 가능.

2. 시드 데이터는 관례다

“정상 데이터의 범위”를 정하는 건 코드가 아니라 관례다. 시드 파일에 명시하니 기획자도 이해하고, 데이터 변경도 쉬워졌다.

3. 거부는 명확하게

검증 실패 시 “거부” 또는 “경고”를 명확히 구분했다:

  • 거부 (Critical): 게임이 깨질 수 있음 (필수 필드 누락, 타입 불일치)
  • 경고 (Warning): 호환성 문제일 수 있음 (예상치 못한 필드)

로그를 이렇게 나누니 디버깅이 훨씬 명확해졌다.

다음


  • Phase D: 런타임 상호작용 검증 (플레이어가 생성한 아이템을 실제로 사용할 때 추가 검증)
  • Phase E: 플레이어 에디터 UI (JSON 수정 대신 게임 안에서 아이템 생성)
  • 공식 샘플 라이브러리 배포 (플레이어가 쉽게 시작할 수 있는 템플릿)

메모

  • 참고: JSON Schema (Draft 7) 스펙 비교 — WM의 간소화된 접근과 차이
  • 키워드: data validation, domain-driven design, test-driven development, asmdef isolation
  • 다음 편 아이디어: 플레이어가 만든 데이터를 어떻게 게임에 반영할지 (로딩, 캐싱, 버전 관리)

To Be Continued..

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.