Quick Flow
게임 결과 저장은 match finished event를 받았다는 사실보다, server가 결과를 한 번 확정한 뒤 재화·기록·보상 변경을 같은 operation으로 기록하는 일이 중요합니다. network retry나 process 재시작 때문에 같은 완료 요청이 다시 와도 operation ID가 같으면 결과를 한 번만 적용합니다.
final tick에서 winner·score·reward를 authoritative state로 확정
-> resultOperationId 생성 또는 조회
-> transaction: match result 기록 + player reward ledger 기록
-> commit
-> client에 완료 event 전송
-> retry는 같은 operation ID의 이미 확정된 결과를 반환| 저장 대상 | 저장 시점 | 중복 방지 기준 |
|---|---|---|
| 매치 결과 | round 또는 match 확정 뒤 | matchId와 result version |
| 재화·보상 | 결과와 같은 transaction | reward operation ID |
| 연결 중 진행 상태 | checkpoint가 필요할 때만 | player·instance·checkpoint version |
| 위치·속도 같은 tick state | 매 tick 저장하지 않음 | replay·복구 요구가 있을 때만 별도 설계 |
확정 전과 확정 후를 나누기
client 화면에 victory 연출을 먼저 보일 수 있어도 reward는 server가 승패·중도 이탈·anti-cheat 검증을 끝낸 뒤 확정합니다. 결과를 먼저 DB에 쓰고 나중에 reward를 쓰는 두 독립 요청으로 나누면, 중간 실패에서 "결과는 승리인데 보상이 없음" 또는 반대 상태가 생깁니다. 같은 저장소에서 가능한 변경은 transaction으로 묶습니다.
모든 simulation tick을 관계형 DB에 쓰는 방식은 보통 필요한 내구성과 비용을 맞추지 못합니다. 실시간 월드 state는 memory와 snapshot·replay log 정책으로 다루고, account progression·inventory·match history처럼 process가 사라져도 남아야 하는 결론을 persistence boundary로 둡니다. 어떤 state를 잃어도 재계산할 수 있는지부터 구분합니다.
match 종료 callback이 여러 번 실행될 수 있다고 가정합니다. shutdown, retry worker, admin recovery가 같은 match를 다시 처리해도 finishMatch(matchId)가 새 reward를 만들면 안 됩니다. server는 terminal result를 읽고 이미 확정된 operation이면 같은 결과를 반환합니다.
멱등성 키를 저장하기
멱등성(idempotency)은 같은 요청을 여러 번 처리해도 최종 결과가 한 번 처리한 것과 같게 만드는 성질입니다. rewardOperationId를 생성해 ledger의 unique key로 두고, transaction 안에서 result와 ledger insert를 함께 시도합니다. unique conflict는 새로운 reward를 더하는 신호가 아니라, 이전에 확정한 결과를 조회하는 경로가 됩니다.
function persistResult(result: FinalResult) {
return db.transaction(async (tx) => {
const inserted = await tx.results.insertIfAbsent(result);
if (!inserted) return tx.results.findByMatchId(result.matchId);
await tx.rewardLedger.insert({
operationId: result.operationId,
playerId: result.playerId,
amount: result.reward,
});
return result;
});
}이 예시의 insertIfAbsent()는 matchId 또는 result operation ID의 database unique constraint를 이용해 한 transaction만 새 결과를 만들게 한다는 전제입니다. conflict가 난 transaction은 다시 조회해 이미 확정된 결과를 반환합니다. memory에서 먼저 seen set을 확인하는 방식만으로는 process가 재시작되거나 여러 worker가 동시에 실행될 때 중복을 막지 못합니다. 다른 DB나 message queue를 함께 쓴다면 transaction 경계를 넘어선 event delivery를 어떻게 복구할지도 별도 설계합니다.
저장 실패 뒤의 행동
commit 실패를 client에게 성공으로 알리면 다음 login에서 결과가 사라집니다. 반대로 commit 결과를 모른 채 retry하면 중복 지급 위험이 있습니다. transaction 결과를 확인할 수 없을 때는 같은 operation ID로 상태를 다시 조회하고, recovery worker가 미완료 match를 재처리할 수 있게 terminal state와 오류 사유를 남깁니다.
개인정보·비밀값·raw token을 match result와 함께 무분별하게 저장하지 않습니다. debugging에 필요한 match ID, player ID, rule version, server build, tick range는 남기되, 입력 packet 전체나 채팅 원문은 보존 기간·접근 권한을 따로 정합니다.
"성공 응답을 보내지 못했으니 다시 지급한다"는 안전한 retry가 아닙니다. 보상은 네트워크 응답이 아니라 server가 보관한 operation ID와 transaction 결과를 기준으로 한 번만 확정해야 합니다.
참고 링크
2 sources