게임 로어(Lore) 데이터 무결성 검증과 대규모 커밋 자동 헬스체크 스크립트 구축

대규모 RPG 및 내러티브 게임에서 발생하는 로어 데이터 참조 깨짐과 퀘스트 순환 오류를 방지하기 위해 고성능 파이썬 기반 무결성 헬스체크 스크립트와 CI/CD 자동화 파이프라인을 구축하는 실무 가이드를 제공합니다.

✓ 복사 완료!
챕터Git & LFS 워크플로
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
게임 로어(Lore) 데이터 무결성 검증과 대규모 커밋 자동 헬스체크 스크립트 구축

핵심 요약

RPG와 내러티브 게임의 기획 데이터에는 퀘스트 분기, 아이템 ID, 인물 관계, 대사 스크립트가 쌓일 수 있습니다. 대규모 커밋에서 참조 키 누락(Dangling Reference)이나 순환 의존성이 생기면 게임 진행 오류나 데이터 처리 문제가 발생할 수 있습니다. Python 3.11+, Pydantic 2, networkx를 사용해 스키마와 참조 관계를 검증하고 그래프 순환을 탐지하는 기본 헬스체크 스크립트를 구현한 뒤, Git Hook과 GitHub Actions로 자동 검증 게이트를 구성합니다. 예제는 순차 처리 기준이며 처리량은 실제 데이터와 환경에서 측정해야 합니다.


1. 대규모 로어(Lore) 데이터 관리에서 무결성 오류는 왜 발생하는가?

게임의 세계관을 구성하는 기획 데이터(Lore Repository)는 단순 텍스트 모음이 아닐 수 있습니다. 인물 ID, 퀘스트 노드, 장소 식별자, 아이템 드롭 테이블, 다국어 로컬라이제이션 키가 서로 참조하는 구조라면 스키마와 관계 검증이 필요합니다. 팀은 Excel, Google Sheets, Notion 등에서 작업한 데이터를 JSON, YAML, CSV로 변환해 Git에 저장하는 방식을 선택할 수 있습니다.

그러나 대규모 패치나 마일스톤 빌드를 앞두고 수백 개의 데이터 파일이 한 번에 커밋될 때 다음과 같은 문제가 발생합니다.

오류 유형발생 원인게임 내 영향도검출 난이도
Dangling Reference (고아 참조)인물/퀘스트 ID 변경 시 대상 엔티티 미수정해당 참조를 사용하는 기능의 오류 가능성중 (사용 경로에 따라 발견 시점이 다름)
Circular Dependency (순환 참조)퀘스트 선행 조건 상호 의존 또는 종료 조건 없는 대사 루프진행 불가나 반복 처리 등 게임 규칙 오류 가능성상 (데이터 구조와 실행 경로에 따라 다름)
Schema Inconsistency (타입 불일치)수치 필드에 문자열 삽입, 필수 필드 누락데이터 파서 크래시, 클라이언트 부팅 실패하 (빌드 단계 즉시 크래시)
Localization Key Mismatch로어 텍스트 키와 번역 테이블 키 불일치인게임 텍스트 깨짐(MISSING_STRING_10204)중 (QA 육안 검수 의존)

이러한 오류를 런타임 클라이언트 실행 후 QA 플레이를 통해 찾는 것은 비용이 매우 큽니다. 따라서 Git 커밋 및 PR(Pull Request) 시점에 자동으로 데이터 헬스체크를 수행하는 게이트웨이가 필수적입니다.

로어 데이터 무결성 검증 아키텍처 다이어그램


2. 로어 데이터 검증 시스템의 3단계 아키텍처

헬스체크 파이프라인은 데이터 검증을 3단계로 나누어 구성할 수 있습니다. 단일 파일 검증부터 글로벌 그래프 관계 검증까지 분리하면 오류 원인을 구분하기 쉽습니다.

flowchart TD
    A["기획 데이터 커밋/PR 생성"] --> B{"검증 모드 판별"}
    B -->|"단일/소규모 커밋"| C["Git Diff 기반 증분 검증"]
    B -->|"대규모 배치/릴리즈"| D["전체 데이터셋 풀 배치 검증"]
    C --> E["1단계: JSON/YAML 정적 스키마 검증"]
    D --> E
    E --> F["2단계: 외래키 및 상호 참조 무결성 검증"]
    F --> G["3단계: DAG 대사/퀘스트 순환 루프 탐지"]
    G --> H{"검증 결과 판정"}
    H -->|"Success (무결성 통과)"| I["CI 빌드 통과 및 머지 승인"]
    H -->|"Failure (오류 검출)"| J["PR 코멘트 자동 등록 및 머지 차단"]
  1. 1단계 (정적 스키마 검증): 각 파일이 정의된 스키마(Data Types, Required Fields, Range Constraints)를 준수하는지 검증합니다.
  2. 2단계 (외래키 및 상호 참조 검증): 퀘스트에 지정된 reward_item_id가 items.json에 실제로 존재하는지 NPC 대사의 speaker_id가 characters.json에 유효한지 전역 인덱스를 대조합니다.
  3. 3단계 (그래프 순환 탐지): 퀘스트 선행/후행 관계와 대사 분기 그래프를 방향 그래프(Directed Graph)로 모델링하여 순환 참조(CycleCycle)를 탐지합니다. 도달 불가능 노드는 별도의 시작 노드와 실행 규칙을 모델링해야 판정할 수 있습니다.

3. 자동 헬스체크 기준 스크립트 구현 (Step-by-Step)

Python의 pydantic과 networkx를 사용해 데이터 스키마와 참조를 검사하는 기준 도구를 만듭니다. 데이터량이 커지면 먼저 프로파일링하고 병렬화의 효과를 별도로 측정합니다.

Step 1: 데이터 모델 및 스키마 정의 (schema.py)

from pydantic import BaseModel, Field
from typing import List, Optional

class DialogueNode(BaseModel):
    node_id: str = Field(..., description="대사 노드 고유 ID")
    speaker_id: str = Field(..., description="화자 NPC ID")
    text_key: str = Field(..., description="로컬라이제이션 스트링 키")
    next_nodes: List[str] = Field(default_factory=list, description="다음 대사 노드 ID 목록")

class QuestData(BaseModel):
    quest_id: str = Field(..., pattern=r"^QST_[0-9]{5}$")
    title_key: str
    prerequisite_quests: List[str] = Field(default_factory=list)
    target_npc_id: str
    dialogues: List[DialogueNode]

Step 2: 참조 검증 및 순환 루프 탐지 엔진 (checker.py)

그래프의 정점 수 ∣V∣|V|와 간선 수 ∣E∣|E|에 대해 순환 탐지를 수행합니다. 그래프 검사 비용은 O(∣V∣+∣E∣)O(|V| + |E|)이며, 파일 읽기·JSON 파싱·스키마 검사 비용은 별도로 측정해야 합니다.

Tgraph=O(∣V∣+∣E∣)T_{\text{graph}} = O(|V| + |E|)
import argparse
import json
import glob
import sys
from pathlib import Path
from typing import Set, List
import networkx as nx
from pydantic import ValidationError
from schema import QuestData

class LoreIntegrityChecker:
    def __init__(self, data_root: str):
        self.data_root = Path(data_root)
        self.valid_npc_ids: Set[str] = set()
        self.valid_string_keys: Set[str] = set()
        self.valid_quest_ids: Set[str] = set()
        self.quest_graph = nx.DiGraph()
        self.errors: List[str] = []

    def load_global_indices(self):
        """참조 검증을 위한 글로벌 마스터 ID 테이블 로드"""
        with open(self.data_root / "characters.json", "r", encoding="utf-8") as f:
            npcs = json.load(f)
            self.valid_npc_ids = {npc["id"] for npc in npcs}

        with open(self.data_root / "localization_keys.json", "r", encoding="utf-8") as f:
            locs = json.load(f)
            self.valid_string_keys = set(locs.keys())

    def validate_quest_file(self, file_path: Path):
        try:
            with open(file_path, "r", encoding="utf-8") as f:
                raw_data = json.load(f)
        except (OSError, json.JSONDecodeError) as e:
            self.errors.append(f"[FILE ERROR] {file_path.name}: {e}")
            return

        if not isinstance(raw_data, dict):
            self.errors.append(f"[SCHEMA ERROR] {file_path.name}: 최상위 JSON 값은 객체여야 합니다")
            return

        try:
            quest = QuestData(**raw_data)
        except ValidationError as e:
            self.errors.append(f"[SCHEMA ERROR] {file_path.name}: {e.json()}")
            return

        self.valid_quest_ids.add(quest.quest_id)

        # 1. 외래키 참조 무결성 검증
        if quest.target_npc_id not in self.valid_npc_ids:
            self.errors.append(f"[DANGLING NPC] {file_path.name}: NPC '{quest.target_npc_id}' 존재하지 않음")

        if quest.title_key not in self.valid_string_keys:
            self.errors.append(f"[MISSING LOC] {file_path.name}: 스트링 키 '{quest.title_key}' 누락")

        # 2. 퀘스트 선행 조건 그래프 노드/간선 추가
        self.quest_graph.add_node(quest.quest_id)
        for prereq in quest.prerequisite_quests:
            self.quest_graph.add_edge(prereq, quest.quest_id)

        # 3. 대사 트리 내부 순환 검사
        dialogue_graph = nx.DiGraph()
        dialogue_node_ids = {node.node_id for node in quest.dialogues}
        dialogue_graph.add_nodes_from(dialogue_node_ids)
        for node in quest.dialogues:
            if node.speaker_id not in self.valid_npc_ids:
                self.errors.append(f"[DANGLING SPEAKER] 노드 {node.node_id} 화자 ID 불일치")
            for next_node in node.next_nodes:
                if next_node not in dialogue_node_ids:
                    self.errors.append(f"[DANGLING DIALOGUE] {file_path.name}: 다음 노드 '{next_node}'가 존재하지 않음")
                dialogue_graph.add_edge(node.node_id, next_node)
            if node.text_key not in self.valid_string_keys:
                self.errors.append(f"[MISSING LOC] 노드 {node.node_id}: 스트링 키 '{node.text_key}' 누락")

        try:
            cycle = nx.find_cycle(dialogue_graph, orientation="original")
            self.errors.append(f"[DIALOGUE CYCLE] {file_path.name} 대사 트리 순환 탐지: {cycle}")
        except nx.NetworkXNoCycle:
            pass

    def validate_global_dependencies(self):
        """퀘스트 간의 글로벌 순환 참조 검사"""
        for prereq, quest_id in self.quest_graph.edges:
            if prereq not in self.valid_quest_ids:
                self.errors.append(f"[DANGLING QUEST] 선행 퀘스트 '{prereq}'가 존재하지 않음 (대상: {quest_id})")
        try:
            cycle = nx.find_cycle(self.quest_graph, orientation="original")
            self.errors.append(f"[QUEST GLOBAL CYCLE] 퀘스트 선행 관계 순환 발견: {cycle}")
        except nx.NetworkXNoCycle:
            pass

    def run(self) -> bool:
        self.load_global_indices()
        quest_files = list(self.data_root.glob("quests/*.json"))
        
        for q_file in quest_files:
            self.validate_quest_file(q_file)

        self.validate_global_dependencies()

        if self.errors:
            print(f"❌ 총 {len(self.errors)}건의 무결성 오류가 발견되었습니다:", file=sys.stderr)
            for err in self.errors:
                print(f"  - {err}", file=sys.stderr)
            return False
        
        print("✅ 모든 로어 데이터의 무결성 검증을 성공적으로 통과했습니다.")
        return True

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", default="./game_data")
    args = parser.parse_args()

    checker = LoreIntegrityChecker(data_root=args.root)
    success = checker.run()
    sys.exit(0 if success else 1)

4. Git Hook 및 CI/CD 파이프라인 통합 전략

검증 스크립트는 로컬 작업 환경과 중앙 CI 파이프라인에 이중으로 배치되어야 합니다.

GitHub Actions CI 워크플로우 구성 (.github/workflows/lore-check.yml)

name: Lore Repository Integrity Check

on:
  pull_request:
    paths:
      - 'game_data/**'
  push:
    branches:
      - main

jobs:
  integrity-check:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Set up Python 3.11
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'

      - name: Install Dependencies
        run: |
          python -m pip install --upgrade pip
          pip install pydantic networkx

      - name: Execute Integrity Healthcheck
        run: |
          python scripts/lore_healthcheck.py --root ./game_data

CI 파이프라인 내 헬스체크 결과 및 PR 차단 게이트 화면

성능 최적화: Git Diff를 이용한 증분(Incremental) 검사

데이터 파일이 수만 개에 달할 경우 매 커밋마다 전체 데이터를 파싱하면 CI 파이프라인이 지연됩니다. 다음 명령어로 변경된 파일만 식별하여 대상 파일 및 그 파일을 참조하는 연관 파일만 부분 검사하도록 파이프라인을 최적화할 수 있습니다.

# PR 대상 브랜치와의 차이점 파일 목록 추출
git diff --name-only origin/main...HEAD -- 'game_data/**/*.json'

5. 자주 묻는 질문 (FAQ)

Q1. 로어 스트링과 번역 텍스트를 매 커밋마다 검증하면 느려지지 않나요?

데이터 크기와 스키마에 따라 다릅니다. 우선 CI 실행 시간과 단계별 프로파일을 측정하고, 증분 검사나 캐시, 병렬 처리는 정확성과 유지 비용을 검토한 뒤 도입하세요. 이 예제에는 병렬 처리와 성능 벤치마크가 포함되어 있지 않습니다.

Q2. 퀘스트 분기나 대사 트리에서 의도된 루프와 버그성 무한 루프는 어떻게 구분하나요?

현재 예제는 순환이 있으면 모두 경고합니다. 상점 재진입처럼 의도된 순환을 허용하려면 종료 조건이나 허용 간선을 데이터 모델에 추가하고, 검사기가 해당 규칙을 적용하도록 별도로 확장해야 합니다.

Q3. 기획자가 Git CLI에 익숙하지 않은 환경에서는 어떻게 적용하나요?

Google Sheets나 Excel의 내보내기 매크로(AppSheet/VBA) 실행 시 로컬 CLI 검증기를 백그라운드에서 호출하게 하거나 Webhook을 통해 중앙 스테이징 서버에서 즉각적인 Slack/Discord 알림으로 오류 리포트를 반환하도록 구성합니다.


6. 결론: 무결성 검증 자동화가 라이브 서비스에 주는 가치

로어 리포지토리의 무결성 검증 파이프라인 구축은 단순한 QA 업무 자동화를 넘어 라이브 게임의 안정성을 지키는 핵심 방어선입니다.

기획 데이터 변경 사항을 빌드 전에 자동으로 검증하면 런타임 크래시와 퀘스트 프리징 같은 치명적인 버그를 원천 차단할 수 있습니다. 데이터 유형을 좁혀 파이프라인을 단계적으로 도입하고, 빌드 환경과 기획·엔지니어링 간의 검토 절차를 함께 정리하세요.