14. 벌크 스냅샷과 증분 이벤트의 버전 역전

난이도 상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// 시나리오 (서버-서버): 월드 서버 간 상태 동기화. 특정 대상(예: 길드
// 창고, 필드 보스 상태)에 대해 두 종류의 갱신이 도착한다.
//
//   1) 벌크 스냅샷: 다른 리전/샤드가 대상 전체 상태를 통째로 재전송한다
//      (예: 장애 복구 후 재동기화, 샤드 이관). 스냅샷에는 그 스냅샷이
//      "어느 시점까지의 상태를 반영한 것인지"를 나타내는 커서(버전)가
//      함께 실려 온다.
//   2) 증분 이벤트: 대상에 대한 개별 변경 이벤트가 스트림으로 계속
//      도착한다. 각 증분 이벤트는 자신이 전제로 하는 버전(BaseVersion,
//      즉 이 이벤트가 적용되기 직전에 대상이 있어야 할 버전)과, 자신이
//      적용되면 도달하는 버전(ResultVersion)을 함께 싣고 있다.
//
// 스냅샷과 증분 이벤트는 서로 다른 채널로 오기 때문에, 네트워크 지연에
// 따라 도착 순서가 뒤섞일 수 있다(스냅샷이 그보다 나중 버전의 증분
// 이벤트보다 늦게 도착하거나, 반대로 증분 이벤트가 그 이벤트의 기반이 된
// 스냅샷보다 먼저 도착할 수 있다).
//
// 요구사항:
//   - 대상의 최종 상태에 반영되는 버전은 시간이 지나도 뒤로 가면 안 된다
//     (더 낮은 버전의 갱신이 이미 반영된 더 높은 버전을 덮어써서는 안 됨).
//   - 아직 도착하지 않은 스냅샷을 기다리지 않고도, 도착한 증분 이벤트는
//     자신이 적용 가능한 상태가 되면 반영되어야 한다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.

using System.Collections.Generic;

public sealed class TargetState
{
    public string TargetId;
    public long Version;
    public Dictionary<string, object> Fields = new();
}

public sealed class SnapshotMessage
{
    public string TargetId;
    public long Version; // 이 스냅샷이 반영하는 시점의 버전.
    public Dictionary<string, object> FullState;
}

public sealed class IncrementalEvent
{
    public string TargetId;
    public long BaseVersion;   // 이 이벤트가 적용되기 직전에 대상이 있어야 할 버전.
    public long ResultVersion; // 이 이벤트가 적용되면 도달하는 버전.
    public string FieldName;
    public object NewValue;
}

public sealed class TargetSyncStore
{
    private readonly Dictionary<string, TargetState> _states = new();

    // 벌크 스냅샷을 수신했을 때 호출된다.
    public void ApplySnapshot(SnapshotMessage snapshot)
    {
        // (A) 스냅샷을 받으면 현재 상태와 무관하게 그대로 덮어써 반영한다.
        var state = new TargetState
        {
            TargetId = snapshot.TargetId,
            Version = snapshot.Version,
            Fields = new Dictionary<string, object>(snapshot.FullState),
        };
        _states[snapshot.TargetId] = state;
    }

    // 증분 이벤트를 수신했을 때 호출된다.
    public void ApplyIncremental(IncrementalEvent evt)
    {
        if (!_states.TryGetValue(evt.TargetId, out var state))
        {
            // 아직 이 대상에 대한 스냅샷/상태가 하나도 없다.
            state = new TargetState { TargetId = evt.TargetId, Version = 0 };
            _states[evt.TargetId] = state;
        }

        // (B) 이벤트를 받은 순서 그대로 반영한다.
        state.Fields[evt.FieldName] = evt.NewValue;
        state.Version = evt.ResultVersion;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.