개발관련

Unity 게임 번역기 개발기 #4: 개발은 디테일이다

삽질연구소 소장 2025. 10. 12. 12:30

API 키 보안 (평문? 미친 짓)

Claude API 키를 평문으로 저장? 그건 미친 짓이다.

config.json에 API 키 저장하고 GitHub에 올려서 API 키 털린 뉴스 많이 봤다.

5단계 보안 계층을 구축했다.

# security/secure_storage.py

# 1. 하드웨어 기반 키 생성
def _get_hardware_key():
    mac = uuid.getnode()              # MAC 주소
    cpu = platform.processor()        # CPU
    system = platform.system()        # OS
    return hashlib.sha256(f"{mac}{cpu}{system}".encode()).digest()

# 2. AES-256 암호화
from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher = Fernet(key)
encrypted = cipher.encrypt(api_key.encode())

# 3. XOR 난독화
hw_key = _get_hardware_key()
obfuscated = bytes(a ^ b for a, b in zip(encrypted, hw_key))

# 4. 바이너리 패딩
padding = os.urandom(random.randint(16, 64))
padded = obfuscated + padding

# 5. HMAC 무결성 검증
import hmac
signature = hmac.new(hw_key, padded, hashlib.sha256).digest()
final_data = signature + padded

# .credentials.bin 저장
with open('.credentials.bin', 'wb') as f:
    f.write(final_data)

파일을 복사해도 다른 PC에서는 복호화 불가능.

MAC 주소, CPU 정보가 다르면 복호화 실패. 내 PC가 아니면 못 연다.

Excel 검수 워크플로우

번역을 외주 준다면 그사람에게 Python 코드 보여주면 안 된다. Excel 주면 된다.

# core/excel_manager.py

class TranslationEntry:
    def __init__(self, file_path, line, japanese, korean):
        filename = Path(file_path).name
        # MD5 ID 생성 (파일명:라인:원문)
        self.id = hashlib.md5(f"{filename}:{line}:{japanese}".encode()).hexdigest()[:12]
        self.file_path = file_path
        self.line_number = line
        self.japanese = japanese
        self.korean = korean

class ExcelManager:
    def export_to_excel(self, entries, output_path):
        df = pd.DataFrame([{
            'ID': e.id,
            '파일': e.file_path,
            '라인': e.line_number,
            '일본어': e.japanese,
            '한국어': e.korean
        } for e in entries])

        # 스타일링 (한국어 열 넓게)
        with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
            df.to_excel(writer, index=False)
            worksheet = writer.sheets['Sheet1']
            worksheet.column_dimensions['E'].width = 50

    def import_from_excel(self, excel_path):
        df = pd.read_excel(excel_path)
        entries_map = {e.id: e for e in self.entries}

        # ID 기반 매칭
        for _, row in df.iterrows():
            if row['ID'] in entries_map:
                entries_map[row['ID']].korean = row['한국어']

        return list(entries_map.values())

    def apply_to_files(self, entries):
        # 라인 번호 기반 교체
        for file_path, file_entries in groupby(entries, key=lambda e: e.file_path):
            with open(file_path, 'r', encoding='utf-8') as f:
                lines = f.readlines()

            for entry in file_entries:
                lines[entry.line_number] = entry.korean + '\n'

            with open(file_path, 'w', encoding='utf-8') as f:
                f.writelines(lines)

워크플로우:

  1. 번역 완료 → Excel 내보내기
  2. 번역가: Excel에서 수정 (편함)
  3. Excel 가져오기 → ID로 정확히 매칭
  4. 파일에 자동 반영
  5. 게임 적용

Bundle 패킹 (한 줄이 중요하다)

번역된 텍스트를 게임에 적용한다.

# core/bundle_packer.py

class BundlePacker:
    def pack_and_apply(self, game_path, translated_files):
        bundles = self._find_target_bundles(game_path)

        for bundle_path in bundles:
            # 백업 (중요!)
            backup_path = bundle_path.with_suffix('.bundle.backup')
            if not backup_path.exists():
                shutil.copy(bundle_path, backup_path)
                # 실수해도 복구 가능

            # 로드
            env = UnityPy.load(str(bundle_path))

            # 번역 매핑
            translated_dict = {Path(f).stem: f for f in translated_files}

            # 적용
            for obj in env.objects:
                if obj.type.name == 'TextAsset':
                    data = obj.read()
                    if data.name in translated_dict:
                        with open(translated_dict[data.name], 'r', encoding='utf-8') as f:
                            data.m_Script = f.read()  # 문자열!
                        data.save()

            # 저장
            with open(bundle_path, 'wb') as f:
                f.write(env.file.save())

중요:

  • data.m_Script = content ✅ (문자열)
  • data.m_Script = content.encode() ❌ (bytes 할당 오류)

한 줄 차이로 3시간 디버깅했다. 문자열 직접 할당해야 한다.

게임 언어 감지 (자동화가 답이다)

게임 구조를 자동으로 감지한다.

# core/game_language_detector.py

class GameLanguageDetector:
    def detect_game_format(self, game_path):
        streaming = game_path / "StreamingAssets"

        # Naninovel 감지
        if streaming.exists():
            bundles = list(streaming.glob("**/*.bundle"))
            if bundles:
                language_folders = self._detect_language_folders(bundles)
                chapter_structure = self._detect_chapter_structure(bundles)
                return {
                    'format': 'Naninovel',
                    'has_language_folders': language_folders,
                    'has_chapter_structure': chapter_structure
                }

        # 일반 Unity 감지
        if (game_path / "data.unity3d").exists():
            return {'format': 'Unity Single Bundle'}

        assets = list(game_path.glob("*.assets"))
        if assets:
            return {'format': 'Unity Asset Split'}

        return {'format': 'Unknown'}  # 포기

사용자: 게임 폴더만 선택
프로그램: 알아서 구조 파악

魔法少女ノ魔女裁判 최종 결과

챕터1 번역 완료

  • 61개 스크립트 파일
  • 약 5,000개 대사

비용 비교

Qwen 로컬 모델: 무료
- 품질: 어색함
- 시간: 챕터당 5시간
- 전기세: ???

Claude API: $0.50
- 품질: 자연스러움
- 시간: 챕터당 30분
- Prompt Caching + Translation Memory

품질 비교 (결정적 차이)

원문: 辛すぎる

Qwen:   너무 매운     (X) 문맥 이해 실패
Claude: 너무 힘들어   (O) 완벽

Claude 압도적 승리. 돈값 한다.

최종 통계

코드 품질

모듈화: 2,600줄 → 7개 모듈 (평균 377줄)
컴파일: 100% 성공
순환 참조: 0건

성능

번역 속도: ~2초/대사 (Claude)
캐싱: 80-90% 중복 방지
API 비용: 90% 절감 (Prompt Caching 덕분)

지원 범위

✅ Naninovel 게임 (魔法少女ノ魔女裁判 완료)
✅ 일반 Unity 게임 (AssetsTools.NET)
✅ Excel 검수 워크플로우
✅ 5단계 API 키 보안
✅ 자동 백업
✅ 다중 API 엔진 (Claude, GPT-4, DeepL)

배운 것

  1. 로컬 모델 vs API
    • 로컬: 무료, 품질 낮음, GPU 불탄다
    • API: 유료, 품질 높음, 최적화로 90% 절감 가능
  2. 보안은 다층 방어
    하드웨어 키 + AES + XOR + 패딩 + HMAC.
    평문 저장? 그건 자살 행위.
  3. Excel 워크플로우
    아직은 사람 손이 타야 품질이 좋아진다.
  4. 자동화가 핵심
    게임 구조 감지, 백업, 번역 모두 자동화.
    사용자는 클릭만.
  5. 디테일이 생명
    data.m_Script = content vs content.encode()
    한 줄 차이로 3시간 날린다.

시리즈 정리

1부: Naninovel 번역

  • UnityPy로 Bundle 파싱
  • Ruby 태그 제거
  • Qwen AI 문맥 번역 (어색했다)

2부: API 모델 전환

  • 로컬 모델 한계 깨달음
  • Claude API 선택 (Prompt Caching 사기)
  • Translation Memory (돈 아끼기)
  • 범용 Unity 게임 지원

3부: 리팩토링

  • 2,600줄 괴물 → 7개 모듈
  • 폴더 구조 최적화 (이름이 중요)
  • 순환 참조 제거 (Lazy import)

4부: 일단 완성

  • 5단계 API 키 보안 (평문은 NO)
  • Excel 검수 워크플로우
  • 자동 백업 시스템
  • 魔法少女ノ魔女裁判 완료

기술 스택

언어: Python 3.10+
GUI: PyQt6
Unity: UnityPy, AssetsTools.NET (pythonnet으로 호출)
AI: Claude API (Qwen 7B는 품질 문제로 폐기)
보안: cryptography (AES-256)
데이터: SQLite, pandas, openpyxl

마지막 한마디

게임 하나 번역하려다 번역기 만들었다.

  • 로컬 모델로 시작
  • API로 전환 (돈 쓰기)
  • 코드 2,600줄 괴물 탄생
  • 리팩토링으로 구원
  • 일단 완성

魔法少女ノ魔女裁判, 이제 한글로 플레이한다.

일단 여기서 개발 멈춤.
원하는 게임 번역했으니 만족

내가 게임을 많이 하는건 아니라 내가 원하는 것만 하면 됐지

코드는 공개했으니 원하면 가져다가 사용해도 됨

 

https://github.com/hasjin/game-translator.git