
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)
워크플로우:
- 번역 완료 → Excel 내보내기
- 번역가: Excel에서 수정 (편함)
- Excel 가져오기 → ID로 정확히 매칭
- 파일에 자동 반영
- 게임 적용
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)
배운 것
- 로컬 모델 vs API
- 로컬: 무료, 품질 낮음, GPU 불탄다
- API: 유료, 품질 높음, 최적화로 90% 절감 가능
- 보안은 다층 방어
하드웨어 키 + AES + XOR + 패딩 + HMAC.
평문 저장? 그건 자살 행위. - Excel 워크플로우
아직은 사람 손이 타야 품질이 좋아진다. - 자동화가 핵심
게임 구조 감지, 백업, 번역 모두 자동화.
사용자는 클릭만. - 디테일이 생명
data.m_Script = contentvscontent.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줄 괴물 탄생
- 리팩토링으로 구원
- 일단 완성
魔法少女ノ魔女裁判, 이제 한글로 플레이한다.
일단 여기서 개발 멈춤.
원하는 게임 번역했으니 만족
내가 게임을 많이 하는건 아니라 내가 원하는 것만 하면 됐지
코드는 공개했으니 원하면 가져다가 사용해도 됨
'개발관련' 카테고리의 다른 글
| Unity 게임 번역기 개발기 #6: 최적화와 크로스플랫폼 (0) | 2025.10.19 |
|---|---|
| Unity 게임 번역기 개발기 #5: RPG 메이커? 그것도 된다 (0) | 2025.10.18 |
| Unity 게임 번역기 개발기 #3: 코드가 괴물이 되다 (0) | 2025.10.11 |
| Unity 게임 번역기 개발기 #2: 돈을 쓰기로 했다 (0) | 2025.10.10 |
| Unity 게임 번역기 개발기 #1: 이게 시작이었다 (0) | 2025.10.09 |