클라우드 세이브 충돌 QA: 로컬·원격 병합 규칙을 시나리오별로 검증하는 무결성 테스트

클라우드 세이브 충돌의 원인을 로컬·원격 리비전과 해시로 분류하고 자동 병합·사용자 선택·복구 규칙을 시나리오별 테스트와 검증 오라클로 점검하는 실무 QA 가이드입니다.

✓ 복사 완료!
챕터자동화 테스트 & 회귀 검증
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
클라우드 세이브 충돌 QA: 로컬·원격 병합 규칙을 시나리오별로 검증하는 무결성 테스트

핵심 요약

클라우드 세이브 충돌은 updated_at만 비교하지 말고 content_revision, 부모 리비전, payload_hash를 기준으로 분류해야 합니다. 같은 데이터는 중복 제거하고 한쪽이 다른 쪽의 조상인 경우에는 자동 병합하며 분기된 세이브는 필드별 병합 정책과 무결성 검증을 통과한 경우에만 병합합니다. 통화·인벤토리처럼 손실이나 복제가 치명적인 데이터는 자동 병합보다 보류와 복구 지점을 우선해야 합니다.

클라우드 세이브 충돌은 왜 발생하는가?

클라우드 세이브 충돌은 두 기기가 같은 세이브 슬롯을 서로 다른 시점에 수정한 뒤 동기화할 때 발생합니다. 대표적인 원인은 다음과 같습니다.

  • 오프라인 상태에서 PC와 콘솔이 각각 게임을 진행함
  • 저장 직후 게임이 종료되거나 네트워크가 끊겨 업로드가 완료되지 않음
  • 패치 전후의 schema_version이 다름
  • 기기 시간이 어긋나 updated_at 순서가 실제 저장 순서와 달라짐
  • 클라우드 공급자가 재시도 과정에서 동일 업로드를 여러 번 전달함
  • 다운로드한 파일 일부가 손상되었거나 중간 버전으로 되돌아감

따라서 최신 시간의 파일을 선택한다는 규칙은 충분하지 않습니다. QA는 먼저 충돌 유형을 판정하고 그 유형에 맞는 기대 결과를 정의해야 합니다.

세이브 충돌 판정에 필요한 메타데이터

세이브 본문과 함께 다음 메타데이터를 저장하면 충돌 재현과 자동 판정이 쉬워집니다.

필드역할테스트 기준
slot_id세이브 슬롯 식별자다른 슬롯 데이터가 섞이지 않아야 함
save_id논리적으로 같은 세이브인지 식별새 게임과 기존 세이브를 구분
schema_version데이터 구조 버전마이그레이션 가능 여부 판정
content_revision저장 순서를 나타내는 단조 증가 리비전동일 계보에서는 감소하면 안 됨
parent_revision현재 저장이 기반으로 삼은 리비전조상·분기 관계 판정
device_id저장을 생성한 기기 식별동일 리비전의 동률 처리
updated_at운영 로그용 실제 시각충돌 우선순위의 단독 기준으로 사용하지 않음
payload_hash정규화된 본문의 무결성 확인동일 데이터와 변조를 구분

본문 해시는 JSON 키 순서를 고정하고 숫자 표현과 null 처리 규칙을 정한 뒤 계산해야 합니다.

H=SHA−256(CanonicalSerialize(payload))H = SHA-256(CanonicalSerialize(payload))

암호화는 기밀성을 제공하지만 데이터가 올바르게 전송되었다는 뜻은 아닙니다. 변조 탐지가 필요하다면 서버 검증용 HMAC 또는 서명까지 별도로 검토해야 합니다. 클라이언트가 가진 비밀키만으로 HMAC을 계산하면 치팅 방지 수단이 되기 어렵기 때문에 권위 있는 검증은 서버에서 수행해야 합니다.

로컬·원격 세이브 병합 규칙은 어떻게 정의할까?

먼저 충돌을 다음 네 가지 상태로 분류합니다.

상태판정기본 동작
동일 데이터content_revision과 payload_hash가 모두 같음중복 제거 후 성공 처리
로컬이 원격의 조상 이후로컬 리비전이 원격의 기반보다 앞서고 분기가 없음로컬 업로드 또는 원격 갱신
원격이 로컬의 조상 이후원격 리비전이 로컬의 기반보다 앞서고 분기가 없음원격 다운로드
분기된 데이터두 세이브가 같은 부모에서 서로 다른 변경을 가짐필드별 병합 또는 사용자 선택

updated_at은 로그와 분석에 활용하되 최종 승자를 정하는 단독 규칙으로 사용하지 않는 것이 안전합니다. 기기 시계가 틀리거나 서버 재시도 순서가 바뀌면 오래된 데이터가 최신 데이터처럼 보일 수 있기 때문입니다.

필드별 병합 정책 예시

전체 파일을 한 번에 덮어쓰지 말고 데이터 성격에 따라 정책을 나눕니다.

데이터권장 정책주의점
체크포인트 번호단조 증가 조건을 만족할 때 max이전 상태로 돌아가는 기능과 충돌할 수 있음
퀘스트 상태상태 머신의 허용 전이만 적용완료와 실패를 단순 문자열 비교로 합치지 않음
아이템고유 item_instance_id 기준 집합 병합같은 아이템 복제 여부를 서버에서 검증
재화·경험치자동 합산·max 금지트랜잭션 로그 또는 권위 서버 값 사용
설정·그래픽 옵션명시적인 마지막 수정 규칙타임스탬프와 기기 우선순위를 함께 기록
플레이 시간파생 값으로 재계산두 파일의 값을 단순 합산하지 않음
업적서버에서 검증된 이벤트 집합으로 병합클라이언트 플래그만 신뢰하지 않음

자동 병합 결과는 원본 두 개를 삭제한 뒤 저장해서는 안 됩니다. local_before_merge, remote_before_merge, merged_candidate를 모두 보존하는 복구 지점을 만들고 검증 실패 시 원본으로 되돌릴 수 있어야 합니다.

시나리오별 무결성 QA 테스트

각 테스트는 입력 세이브 두 개와 기대 결과인 검증 오라클을 함께 가져야 합니다. 검증 오라클은 단순히 앱이 실행되는지 확인하는 것이 아니라 리비전, 해시, 아이템 수량, 재화 잔액, 퀘스트 상태까지 확인해야 합니다.

ID시나리오기대 결과무결성 오라클
C-01로컬과 원격이 동일한 파일충돌 없이 중복 제거병합 후 해시가 원본 해시와 같음
C-02로컬이 원격보다 최신인 선형 이력로컬을 유지하고 업로드리비전 증가, 원격 슬롯 갱신
C-03원격이 로컬보다 최신인 선형 이력원격을 다운로드로컬 로드 결과가 원격 해시와 일치
C-04같은 부모에서 서로 다른 설정을 변경설정 정책에 따라 결정선택된 필드만 변경되고 나머지는 보존
C-05두 기기에서 서로 다른 고유 아이템 획득집합 병합 또는 보류item_instance_id 중복 없음
C-06두 기기에서 같은 재화를 변경자동 합산하지 않음잔액이 검증된 권위 값과 일치
C-07한쪽 세이브의 해시 불일치손상 데이터 격리손상 파일이 활성 슬롯으로 승격되지 않음
C-08schema_version이 낮은 로컬 세이브마이그레이션 후 비교마이그레이션 전후 핵심 값 보존
C-09네트워크가 업로드 중 반복 단절재시도는 멱등적으로 처리동일 save_id와 리비전이 중복 생성되지 않음
C-10사용자가 충돌 화면에서 로컬을 선택로컬 원본을 복구원격 데이터가 삭제되지 않고 복구 가능

C-05: 아이템 집합 병합 검증

아이템을 단순 배열 인덱스로 병합하면 두 기기에서 생성된 아이템이 덮어써질 수 있습니다. 각 아이템에 고유 식별자를 부여하고 다음 조건을 검사합니다.

  1. 두 입력의 item_instance_id를 수집한다.
  2. 동일 ID가 서로 다른 속성을 가지면 충돌로 분류한다.
  3. 신규 ID는 집합에 추가한다.
  4. 서버가 허용한 생성 이벤트와 수량 범위를 검증한다.
  5. 병합 후 정규화된 해시를 다시 계산한다.

동일한 ID가 서로 다른 아이템으로 나타나는 경우에는 어느 쪽을 선택할지 추측하지 말고 복구 큐로 보내는 편이 안전합니다.

C-06: 재화 중복 지급 검증

재화는 local_gold + remote_gold 또는 max(local_gold, remote_gold)로 병합하면 중복 지급이나 손실이 발생할 수 있습니다. 재화 변경을 이벤트로 기록할 수 있다면 다음처럼 이벤트의 고유 키를 기준으로 중복 제거합니다.

public sealed record CurrencyEvent(
    string EventId,
    string AccountId,
    long Delta,
    long Sequence
);

public static long CalculateBalance(
    long verifiedBase,
    IEnumerable<CurrencyEvent> events)
{
    return verifiedBase + events
        .DistinctBy(e => e.EventId)
        .OrderBy(e => e.Sequence)
        .Sum(e => e.Delta);
}

테스트에서는 같은 EventId를 양쪽 세이브에 넣고 한 번만 반영되는지 확인합니다. Sequence가 뒤섞이거나 음수 Delta가 허용 범위를 벗어나는 경우에는 병합을 중단하고 서버 검증 실패로 기록해야 합니다.

충돌 병합을 어떻게 단계별로 검증할까?

1단계: 입력과 계보를 고정한다

테스트 픽스처에 다음 값을 명시합니다.

  • schema_version
  • save_id, slot_id
  • content_revision, parent_revision
  • device_id
  • 로컬·원격의 정규화 전 JSON
  • 예상 payload_hash
  • 네트워크 상태와 패치 버전

테스트 실행 전에 입력 해시를 기록하면 픽스처가 무의식적으로 변경되는 문제를 찾을 수 있습니다.

2단계: 충돌 판정 결과를 검증한다

병합 함수는 바로 파일을 쓰기보다 판정 결과를 반환하도록 분리하는 것이 좋습니다.

public enum SaveDecision
{
    Deduplicate,
    KeepLocal,
    KeepRemote,
    Merge,
    ManualRecovery
}

public sealed record SaveResult(
    SaveDecision Decision,
    string ResultHash,
    long ResultRevision,
    bool IsValid
);

각 시나리오에서 Decision이 예상 값과 같은지 확인하고 IsValid가 거짓인 결과가 활성 세이브로 저장되지 않는지 검사합니다.

3단계: 병합 후 불변 조건을 검사한다

모든 자동 병합 결과에는 다음 불변 조건을 적용합니다.

  • slot_id와 save_id가 입력과 일치한다.
  • schema_version이 현재 빌드가 지원하는 범위에 있다.
  • content_revision이 입력 리비전보다 작아지지 않는다.
  • 정규화 후 계산한 payload_hash가 저장된 해시와 같다.
  • 아이템 고유 ID가 중복되지 않는다.
  • 재화가 허용 범위를 벗어나지 않는다.
  • 퀘스트 상태가 정의된 상태 머신 전이를 벗어나지 않는다.
  • 병합에 사용하지 않은 원본 세이브가 복구 지점에 남아 있다.

네트워크 단절과 패치 버전 차이는 어떻게 재현할까?

충돌 QA는 정상 네트워크만으로는 부족합니다. 업로드 직전, 업로드 중간, 서버 응답 수신 직후에 연결을 끊고 재시도해야 합니다. 재시도 요청에는 save_id, content_revision, operation_id를 포함해 같은 요청이 여러 번 처리되어도 결과가 한 번만 반영되는지 확인합니다.

패치 검증에서는 다음 조합을 최소 단위로 구성합니다.

클라이언트세이브검증 항목
이전 빌드이전 스키마정상 로드
이전 빌드새 스키마명확한 호환 불가 처리
새 빌드이전 스키마마이그레이션 후 값 보존
새 빌드새 스키마정상 저장·동기화

마이그레이션은 한 번에 최신 버전으로만 이동하지 말고 지원 범위를 명확히 해야 합니다. 예를 들어 schema_version = 3에서 5로 이동할 때 3→4, 4→5 변환을 각각 테스트하고 변환 후 역직렬화가 가능한지 확인합니다.

프로퍼티 기반 테스트와 회귀 테스트를 함께 운영하기

고정 시나리오는 알려진 회귀 버그를 잡는 데 유리하고 프로퍼티 기반 테스트는 예상하지 못한 조합을 찾는 데 유리합니다. 다음 속성을 무작위 세이브에 반복 적용할 수 있습니다.

  • 동일한 입력을 두 번 병합해도 결과가 달라지지 않는다.
  • 병합 순서를 바꿔도 정책상 순서 비의존 필드는 같은 결과를 낸다.
  • 병합 결과를 다시 병합해도 아이템과 이벤트가 중복되지 않는다.
  • 유효하지 않은 입력은 항상 ManualRecovery 또는 명시적 오류를 반환한다.
  • 저장과 로드 사이에 해시가 변하지 않는다.

다만 재화나 퀘스트처럼 순서가 의미를 가지는 데이터는 가환성이나 결합성을 무조건 요구하면 안 됩니다. 필드별 정책이 순서 의존적인지 테스트 명세에 표시해야 합니다.

라이브 운영에서 확인할 충돌 지표

빌드 검증이 끝난 뒤에는 운영 로그로 다음 지표를 추적합니다.

  • 세이브 충돌 발생률: 충돌 세션 수 / 동기화 세션 수
  • 자동 병합 성공률
  • 수동 복구 전환율
  • 해시 불일치 비율
  • 마이그레이션 실패율
  • 재시도 요청의 중복 처리율
  • 복구 지점 생성 및 복원 성공률

로그에는 세이브 본문 전체나 개인정보를 남기지 말고 save_id의 익명화된 식별자, 리비전, 스키마 버전, 해시, 판정 코드, 클라이언트 빌드만 기록하는 것이 좋습니다.

자주 묻는 질문 (FAQ)

클라우드 세이브 충돌에서 시간만 비교하면 안 되는 이유는 무엇인가요?

기기 시계 오차와 네트워크 재시도 때문에 실제 저장 순서와 updated_at이 달라질 수 있기 때문입니다. 부모 리비전과 단조 증가하는 콘텐츠 리비전을 우선 사용해야 합니다.

로컬과 원격 데이터를 항상 자동으로 합치면 안 되나요?

아이템·재화·퀘스트처럼 중복 지급이나 상태 손실이 발생할 수 있는 데이터는 안전한 병합 규칙이 없으면 자동 병합하면 안 됩니다. 이 경우 원본 보존 후 사용자 선택이나 서버 복구 절차로 전환해야 합니다.

해시가 같으면 세이브가 완전히 안전한가요?

해시는 동일한 정규화 데이터인지 확인하는 도구입니다. 서버 권위 검증, 이벤트 중복 제거, 스키마 마이그레이션 검증까지 대신하지는 않습니다.

정리

클라우드 세이브 무결성 QA의 핵심은 충돌을 하나의 예외 화면으로 취급하지 않고 데이터 계보와 필드 정책에 따라 분해하는 것입니다. content_revision과 parent_revision으로 충돌 유형을 판정하고 payload_hash로 전송 무결성을 확인한 뒤 아이템·재화·퀘스트별 불변 조건을 검사해야 합니다. 마지막으로 원본 복구 지점과 멱등적 재시도를 포함한 시나리오를 빌드 회귀 테스트와 라이브 지표에 연결하면 세이브 손실과 중복 지급을 함께 줄일 수 있습니다.