
핫픽스와 동적 패치: 점검 없이 스탯·드롭률 테이블을 실시간 갱신하는 방법
밸런스 테이블을 서버 점검 없이 안전하게 갱신하기 위한 데이터 구조, 배포 절차, 검증과 롤백 전략을 정리한다. Unity·Unreal 클라이언트와 게임 서버 모두에 적용할 수 있는 일반적인 설계다.
왜 런타임 테이블 갱신이 필요한가
라이브 게임의 밸런스 문제는 다음 정기 점검까지 기다려 주지 않는다. 특정 몬스터의 공격력이 비정상적으로 높거나 이벤트 아이템의 드롭률이 잘못 적용되거나 신규 장비의 옵션이 경제를 흔드는 상황이 대표적이다.
이때 서버 프로그램이나 클라이언트 바이너리 자체를 바꾸는 일과 수치 테이블을 바꾸는 일은 분리하는 편이 좋다. 전자는 충분한 테스트와 배포가 필요한 코드 변경이고 후자는 검증된 형식의 데이터를 교체하는 운영 변경이다. 핫픽스 시스템의 목표는 후자를 빠르고 추적 가능하며 되돌릴 수 있게 만드는 것이다.

먼저 정할 경계: 데이터는 동적으로 규칙은 코드로
동적 패치에 넣기 좋은 대상은 값만 바뀌어도 게임 규칙이 유지되는 데이터다.
- 몬스터와 장비의 기본 스탯
- 경험치 보상과 상점 가격
- 드롭 테이블의 가중치와 수량 범위
- 이벤트 기간, 노출 조건, 보상 한도
- 기존 스킬의 계수와 재사용 대기시간
반면 새 네트워크 메시지, 신규 계산식, 저장 데이터 구조 변경, 보안 검증 방식 변경은 코드 배포로 다뤄야 한다. 데이터만 교체한다고 해도 코드가 알지 못하는 필드나 의미를 추가하면 오래된 서버와 클라이언트가 서로 다르게 해석할 수 있다.
따라서 테이블에는 schemaVersion을 두고 실행 파일이 지원하는 스키마 범위를 명시한다. 지원하지 않는 버전이면 적용하지 않고 이전 테이블을 유지해야 한다.
테이블 포맷 설계
사람이 검토하기 쉬운 CSV나 스프레드시트는 제작 원본으로 적합하다. 런타임 배포물은 JSON, MessagePack, Protocol Buffers처럼 파싱 규칙이 명확한 형식을 사용하면 된다. 중요한 것은 형식보다 식별자, 버전, 검증 규칙을 빠뜨리지 않는 일이다.
다음은 드롭 테이블의 단순한 JSON 예시다.
{
"schemaVersion": 1,
"tableVersion": "2026.07.29.3",
"effectiveAtUtc": "2026-07-29T12:00:00Z",
"tables": {
"goblin": {
"rollCount": 1,
"entries": [
{ "itemId": 1001, "weight": 7000, "minCount": 1, "maxCount": 3 },
{ "itemId": 2004, "weight": 2500, "minCount": 1, "maxCount": 1 },
{ "itemId": 9001, "weight": 500, "minCount": 1, "maxCount": 1 }
]
}
}
}
확률을 부동소수점으로 저장하면 편해 보이지만 도구·플랫폼별 반올림 차이와 합계 오차를 다뤄야 한다. 보통은 위 예시처럼 정수 가중치를 사용하고 전체 가중치 중 하나를 뽑는다. weight가 각각 7000, 2500, 500이면 총합은 10000이며 드롭 선택 확률은 다음과 같다.
확률형 상품이나 보상 테이블은 게임 서버가 최종 추첨해야 한다. 클라이언트는 표시용 확률이나 미리보기 데이터를 받을 수 있지만 클라이언트가 결정한 결과를 서버가 신뢰해서는 안 된다.
안전한 갱신 흐름
핫픽스 파일을 내려받은 즉시 전역 테이블을 덮어쓰면 안 된다. 다운로드, 무결성 확인, 파싱, 의미 검증을 모두 통과한 새 스냅샷을 준비한 뒤 한 번에 교체해야 한다.
flowchart LR
A[운영 도구에서 테이블 생성] --> B[정적 검증과 시뮬레이션]
B --> C[서명된 매니페스트 배포]
C --> D[서버가 새 버전 다운로드]
D --> E[해시·서명·스키마 검증]
E --> F[메모리에 새 스냅샷 구성]
F --> G[원자적 참조 교체]
G --> H[버전·적용 결과 기록]
E --> I[거부 및 이전 버전 유지]
매니페스트에는 최소한 테이블 버전, 파일 URL, SHA-256 해시, 생성 시각, 적용 예정 시각을 넣는다. 공개 배포 경로를 신뢰하기 어렵거나 외부 공격 가능성을 고려해야 한다면 매니페스트 자체에 전자서명을 추가한다. 해시는 전송 중 파일 손상을 검출하지만 공격자가 매니페스트와 파일을 함께 바꿀 수 있는 경로에서는 서명까지 필요하다.
서버에서는 스냅샷을 원자적으로 교체한다
요청 처리 도중 테이블의 일부 행만 바뀌면 같은 전투나 드롭 계산 안에서 서로 다른 버전을 읽을 수 있다. 불변 객체로 테이블 스냅샷을 만들고 활성 스냅샷의 참조만 교체하면 이 문제를 단순하게 해결할 수 있다.
C++에서는 shared_ptr 기반의 원자적 로드와 저장을 사용할 수 있다. 아래 코드는 개념을 보여 주는 축약 예시다.
using DropTableMap = std::unordered_map<int, DropTable>;
std::shared_ptr<const DropTableMap> g_activeTables;
std::shared_ptr<const DropTableMap> GetActiveTables() {
return std::atomic_load(&g_activeTables);
}
bool ApplyTables(const std::string& jsonText) {
auto next = ParseDropTables(jsonText);
if (!next || !Validate(*next)) {
return false;
}
std::atomic_store(
&g_activeTables,
std::make_shared<const DropTableMap>(std::move(*next))
);
return true;
}
DropResult RollDrop(int monsterId, Random& random) {
const auto tables = GetActiveTables();
const auto& table = tables->at(monsterId);
return RollWeighted(table, random);
}
한 번 시작한 전투나 던전에 반드시 같은 버전을 유지해야 한다면 입장 시점에 tableVersion 또는 스냅샷 참조를 세션에 고정한다. 반대로 월드 전체에 즉시 반영하는 것이 목적이라면 각 요청이 최신 스냅샷을 읽도록 하면 된다. 어느 쪽이 맞는지는 기술 문제가 아니라 게임 규칙의 선택이다.
Unity 클라이언트의 역할
Unity 클라이언트는 원격 설정을 받아 UI 표기, 콘텐츠 노출, 로컬 시뮬레이션에 쓸 수 있다. 다만 전투 결과와 보상 지급처럼 조작되면 안 되는 결론은 서버가 계산한다.
클라이언트는 현재 적용 버전과 서버가 요구하는 최소 버전을 비교해 다음처럼 동작시키는 편이 실용적이다.
- 표시만 달라지는 테이블은 다음 앱 실행 또는 안전한 화면 전환 때 적용한다.
- 전투 계산에 영향을 주는 로컬 보정값은 매치 시작 전에 고정한다.
- 호환되지 않는 스키마는 무시하고 코드 업데이트가 필요하다는 상태를 서버에서 받는다.
- 캐시 파일이 손상돼도 기본 내장 테이블로 실행할 수 있게 한다.
원격 파일은 Application.persistentDataPath에 임시 파일로 저장한 후 검증이 끝났을 때만 실제 캐시 파일 이름으로 교체한다. 앱이 저장 도중 종료되더라도 마지막으로 검증된 파일을 보존하기 위해서다.
검증은 범위를 넘어 의미까지 확인한다
JSON 문법이 맞는다고 게임 데이터가 안전한 것은 아니다. 배포 전에 다음 검증을 자동화해야 한다.
- 모든
itemId,monsterId가 마스터 데이터에 존재하는지 확인한다. - 가중치는 음수가 아니며 총합이 0이 아닌지 확인한다.
- 최소 수량이 최대 수량보다 작거나 같은지 확인한다.
- 희귀 보상, 재화, 유료 아이템에 별도 상한선을 적용한다.
- 드롭 횟수와 수량의 기대값이 허용 범위를 넘지 않는지 확인한다.
- 이벤트 시작·종료 시간이 유효하고 서로 충돌하지 않는지 확인한다.
기대 지급량은 운영 검토에 특히 유용하다. 한 번의 처치에서 아이템 의 기대 수량은 다음처럼 계산할 수 있다.
실제 테이블에는 독립 추첨, 보장 드롭, 중첩 불가 항목, 플레이어 상태에 따른 보정 등 복잡한 규칙이 추가될 수 있다. 이 경우 운영 도구의 시뮬레이터가 서버와 같은 규칙을 공유하거나 서버 검증 라이브러리를 재사용해야 한다. 별도의 구현을 두면 시간이 지나며 결과가 달라질 가능성이 크다.

롤백과 관측 가능성
핫픽스는 적용보다 되돌리기가 더 중요할 때가 많다. 배포 저장소에는 최근의 정상 버전을 보관하고 운영 도구에서 특정 버전을 다시 활성화할 수 있어야 한다. 단순히 같은 파일을 재업로드하기보다 버전 번호와 적용 이력을 남기는 편이 원인 추적에 유리하다.
서버 로그와 메트릭에는 다음 정보를 남긴다.
- 서버 인스턴스별 현재 테이블 버전
- 다운로드, 검증, 적용 실패 횟수와 실패 사유
- 버전별 드롭 결과와 재화 생성량
- 적용 시각과 적용한 운영자 또는 자동화 작업 ID
드롭률 변경 뒤에는 절대 수치만 보지 말고 이전 버전 대비 변화도 확인한다. 예를 들어 희귀 아이템 획득량이 급증했다면 실제 드롭률 변경인지 특정 보스 이용자 수 증가인지 중복 지급 버그인지 분리해서 판단해야 한다.
점진 적용으로 위험을 줄인다
모든 서버에 동시에 적용하는 방식은 빠르지만 실수의 영향도 즉시 커진다. 가능하다면 새 버전을 다음 순서로 확대한다.
- 스테이징 환경에서 스키마 검증과 대량 추첨 시뮬레이션을 수행한다.
- 내부 서버 또는 소수의 게임 서버에 적용한다.
- 오류율과 재화 생성량을 확인한다.
- 이상이 없으면 전체 서버에 적용한다.
지역 서버나 샤드를 나눠 운영한다면 매니페스트에 대상 그룹을 넣어 카나리 배포를 구현할 수 있다. 다만 플레이어가 서버를 이동할 수 있는 게임에서는 보상 규칙이 달라지는 기간을 짧게 유지하고 보상 지급 로그에 적용 버전을 함께 기록하는 것이 좋다.
마무리
실시간 테이블 갱신은 파일을 내려받아 다시 읽는 기능만으로 완성되지 않는다. 불변 스냅샷 교체로 일관성을 지키고 해시와 스키마·의미 검증으로 잘못된 데이터를 막고 버전 기록과 롤백으로 운영 위험을 낮춰야 한다.
처음에는 몬스터 스탯이나 드롭률처럼 범위가 명확한 테이블 하나부터 시작하는 편이 좋다. 해당 흐름이 안정되면 이벤트 설정과 상점 가격으로 확장하되 코드 변경이 필요한 규칙까지 데이터 패치로 해결하려 하지 않는 경계를 유지해야 한다.


