요즘 퇴근하고 나서 1인 인디 게임을 조금씩 만들고 있어요. 장르는 방치형 로그라이크 쪽이고, 오늘은 진짜 개발자답게(?) 코드 한 줄 안 짜고 명세서만 붙잡고 있었습니다.
근데 오히려 이런 날이 나중에 출시 일정에 더 크게 영향을 주는 것 같아요. 막상 만들기 시작하면 “이거 서버에서 검증해야 하나?”, “밸런스 곡선 너무 급한 거 아닌가?”, “클라 데이터는 어디까지 숨겨야 하지?” 같은 질문들이 계속 튀어나오거든요.
그래서 오늘은 시스템 명세서 v1이랑 밸런싱 곡선표 v1을 한 번에 정리해봤어요. 본업이 10년차 게임 서버 개발자라 그런지, 혼자 만드는 게임이어도 서버 구조랑 보안 쪽 고민을 그냥 지나치기가 어렵더라고요.
오늘 결정한 큰 줄기 5가지
- Unity 클라이언트와 서버를 모두 C# 기반으로 가져가기
- 패킷은 공유 라이브러리로 통일하되, 게임 공식은 일부러 공유하지 않기
- 저장소는 MySQL, 캐시는 Redis로 구성하기
- 안티치트는 처음부터 풀스펙이 아니라 ROI 기준으로 단계 도입하기
- 클라이언트 데이터 테이블은 MessagePack + AES 암호화로 최소 방어선 만들기
하나씩 적어보면 “이걸 지금까지 왜 미뤘지?” 싶을 정도로 머리가 정리됐어요.
1. Unity도 C#, 서버도 C#으로 간 이유
클라이언트는 Unity, 서버는 ASP.NET Core .NET 8로 가기로 했어요. 이유는 생각보다 단순합니다. 패킷 클래스를 양쪽에서 따로 만들면 언젠가는 반드시 어긋나요.
서버 개발을 오래 하다 보면 이런 사고를 진짜 많이 봐요. 클라는 필드를 하나 추가했는데 서버는 아직 모르고 있다거나, enum 순서가 살짝 바뀌었는데 QA에서만 이상하게 터진다거나요. 그래서 이번에는 처음부터 공유 라이브러리를 따로 빼기로 했습니다.
MysteryCreature.Shared/
├─ Packets/ # 클라 ↔ 서버 DTO
├─ Models/ # PlayerSave, Inventory
├─ Enums/ # Slot, Tier, MutationVariant
├─ Constants/ # cap, 쿨다운, 등급
└─ Validation/ # 값 범위 체크
원칙은 간단하게 잡았어요. 서버에서 먼저 정의하고, 공유 라이브러리로 빼고, Unity가 참조한다. 방향을 이렇게 고정하면 책임 소재가 훨씬 깔끔해집니다.
2. 그런데 게임 공식은 일부러 공유하지 않기로 했어요
처음에는 DPS 계산식이나 머지 결과 같은 게임 공식도 전부 공유 라이브러리에 넣을까 했어요. 서버가 같은 코드로 재계산하면 안티치트 검증도 깔끔하니까요.
근데 다시 생각해보니, 클라이언트가 디컴파일되면 그 공식도 같이 노출됩니다. 혼자 만드는 게임에서 보안까지 완벽하게 막을 수는 없지만, 굳이 털기 쉽게 만들어줄 필요도 없겠더라고요.
그래서 결론은 이렇습니다.
- 패킷/모델/enum: 공유 라이브러리 사용
- 게임 공식: 클라이언트와 서버 검증식을 분리
- 서버 역할: 완전 동일 계산보다는 범위, 시간, 논리 검증 중심
물론 DRY 원칙은 살짝 포기해야 해요. 공식이 바뀌면 양쪽을 같이 봐야 하니까요. 그래도 이번 프로젝트에서는 개발 편의성보다 노출 면적을 줄이는 쪽이 더 맞다고 판단했습니다.
3. MySQL + Redis 캐시 전략

방치형 게임은 구조상 읽기가 많아요. 접속할 때 저장 데이터를 읽고, 보상 계산하고, 랭킹이나 상태도 자주 확인하니까요. 그래서 저장소는 MySQL, 캐시는 Redis로 정했습니다.
읽기는 흔한 Cache-Aside 패턴으로 갑니다.
Redis GET
→ miss면 MySQL SELECT
→ Redis SET
→ 클라이언트 반환
쓰기는 순서가 더 중요해요.
MySQL UPDATE 먼저
→ 성공하면 Redis SET
→ 실패하거나 애매하면 Redis DEL
Redis를 먼저 쓰면 MySQL 저장이 실패했을 때 데이터가 꼬일 수 있어요. 그래서 MySQL을 항상 진실의 원천, Redis는 빠르게 읽기 위한 보조 장치로 두기로 했습니다.
멀티 디바이스에서 동시에 저장하는 상황은 saveVersion으로 낙관적 락을 걸 예정이에요.
UPDATE saves
SET data = ?, version = version + 1
WHERE uuid = ? AND version = ?;
다만 랭킹, 세션, 레이트 리미터처럼 Redis 자체가 기준이 되는 데이터는 별도로 볼 생각입니다. 전부 같은 캐시 패턴에 욱여넣으면 오히려 헷갈려요.
4. 안티치트는 풀스펙보다 ROI를 먼저 봤어요
오늘 제일 오래 고민한 부분이 안티치트였어요. 서버 개발자 본능으로는 당연히 서버에서 전부 재계산하고 싶습니다. 근데 현실은 1인 부업 인디예요. 완벽한 구조를 짜다가 출시가 밀리면 그게 더 큰 리스크죠.
| 옵션 | 내용 | 판단 |
|---|---|---|
| A | 범위/시간/논리 검증만 적용 | 가볍지만 핵 대응은 약함 |
| B | 서버에서 매번 공식 재계산 | 강하지만 개발/운영 비용이 큼 |
| C | 검증식부터 시작하고, 인터페이스로 재계산 검증을 나중에 추가 | MVP에 가장 현실적 |
결국 선택은 C입니다. 처음부터 인터페이스는 열어두고, MVP 단계에서는 RangeValidator만 구현하기로 했어요. 매출이 의미 있게 나오면 그때 RecalcValidator를 추가하면 됩니다.
public interface IAntiCheatValidator
{
ValidationResult Validate(GameEventRequest request);
}
public sealed class RangeValidator : IAntiCheatValidator
{
// MVP: 범위, 시간, 논리 검증
}
public sealed class RecalcValidator : IAntiCheatValidator
{
// 매출 이후: 서버 재계산 검증
}
이런 식으로 DI로 갈아끼울 수 있게만 만들어두면, 지금 당장 모든 걸 해결하지 않아도 됩니다. 요즘 계속 느끼는 건데, 좋은 설계는 결정을 미루는 게 아니라 안전하게 미룰 수 있는 구조를 만드는 것에 가까운 것 같아요.
5. 클라이언트 데이터 테이블은 암호화하기로 했어요
CSV 테이블은 빌드 타임에 MessagePack으로 직렬화하고, 그 위에 AES-256-GCM을 한 번 더 씌울 예정입니다.
Editor 평문 CSV
→ Release 빌드 전 MessagePack 이진 직렬화
→ AES-256-GCM 암호화 + nonce
→ StreamingAssets/*.dat 포함
→ IL2CPP 빌드
여기서 중요한 건 모든 데이터를 다 감추겠다는 게 아니에요. 능력치, 광고, IAP처럼 매출이나 밸런스에 직접 영향을 주는 데이터만 우선 암호화합니다. 아이콘 ID나 로컬라이제이션 텍스트까지 숨기는 건 비용 대비 의미가 크지 않다고 봤어요.
그리고 Editor에서는 평문 CSV 그대로 작업합니다. 개발할 때마다 암호화 때문에 발목 잡히면 진짜 오래 못 가요. 자동 암호화는 Release 빌드 타임에만 태우는 쪽으로 정리했습니다.
6. 밸런싱 곡선표 v1도 만들었어요

밸런싱은 일단 온건한 페이스로 잡았어요. 글로벌 유저까지 생각하면 초반부터 너무 빡빡한 곡선은 부담스럽다고 봤습니다.
| 시점 | 목표 진행 | 누적 플레이 감각 |
|---|---|---|
| D1 | 진화 1 → 2 | 약 30분 |
| D7 | 진화 3 → 4 | 약 3시간 |
| D30 | 진화 5 → 6 | 약 10시간 |
| D90 | 진화 8 → 9 | 약 30시간 |
| D180 | 윤회 1회차 | 약 50시간 |
공식은 일단 이렇게 출발합니다.
- 레벨 XP:
15 * lv^1.6 - 진화 능력치:
base * 4^stage - 스테이지 DPS:
5 * 1.10^t - 머지 비용:
100 * 6^tier - 화폐 인플레이션: 전체 곡선을
1.10^t기준으로 정렬
물론 이 숫자들이 정답이라는 뜻은 아니에요. 그냥 시뮬레이터가 때려볼 첫 번째 과녁입니다. D7까지의 진행도, ARPDAU, 윤회 효율 같은 걸 자동으로 돌려보고 곡선을 계속 깎아야 해요.
오늘 느낀 것
오늘은 코드 한 줄 안 짰지만, 오히려 프로젝트가 꽤 앞으로 간 느낌이 들었어요. 1인 인디에서 제일 무서운 건 시간이 부족한 게 아니라 결정을 계속 미루는 것 같거든요.
본업에서 쌓인 경험이 여기서 도움이 되는 지점도 딱 이거였어요. “무조건 좋은 구조”를 고르는 게 아니라, 지금 단계에서 감당 가능한 구조를 고르는 것. 보안도, 캐시도, 밸런싱도 결국 트레이드오프라서요.
다음에는 스킬 15종 명세랑 변이 분기 알고리즘을 잡아볼 예정입니다. Unity를 켜는 건 그 다음이에요. 급하게 화면부터 만들고 싶긴 한데, 이번에는 조금 참고 설계부터 단단하게 가보려고 합니다.