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++
// ============================================================================
// 시나리오 (서버-서버)
// ----------------------------------------------------------------------------
// 월드 서버 간 상태 동기화. 특정 대상(예: 길드 창고, 필드 보스 상태)에 대해
// 두 종류의 갱신이 도착한다.
//
// 1) 벌크 스냅샷: 다른 리전/샤드가 대상 전체 상태를 통째로 재전송한다
// (예: 장애 복구 후 재동기화, 샤드 이관). 스냅샷에는 그 스냅샷이
// "어느 시점까지의 상태를 반영한 것인지"를 나타내는 커서(버전)가
// 함께 실려 온다.
// 2) 증분 이벤트: 대상에 대한 개별 변경 이벤트가 스트림으로 계속
// 도착한다. 각 증분 이벤트는 자신이 전제로 하는 버전(baseVersion,
// 즉 이 이벤트가 적용되기 직전에 대상이 있어야 할 버전)과, 자신이
// 적용되면 도달하는 버전(resultVersion)을 함께 싣고 있다.
//
// 스냅샷과 증분 이벤트는 서로 다른 채널로 오기 때문에, 네트워크 지연에
// 따라 도착 순서가 뒤섞일 수 있다(스냅샷이 그보다 나중 버전의 증분
// 이벤트보다 늦게 도착하거나, 반대로 증분 이벤트가 그 이벤트의 기반이 된
// 스냅샷보다 먼저 도착할 수 있다).
//
// 요구사항:
// - 대상의 최종 상태에 반영되는 버전은 시간이 지나도 뒤로 가면 안 된다
// (더 낮은 버전의 갱신이 이미 반영된 더 높은 버전을 덮어써서는 안 됨).
// - 아직 도착하지 않은 스냅샷을 기다리지 않고도, 도착한 증분 이벤트는
// 자신이 적용 가능한 상태가 되면 반영되어야 한다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
// ============================================================================
#include <cstdint>
#include <string>
#include <unordered_map>
struct TargetState
{
std::string targetId;
int64_t version = 0;
std::unordered_map<std::string, std::string> fields;
};
struct SnapshotMessage
{
std::string targetId;
int64_t version = 0; // 이 스냅샷이 반영하는 시점의 버전.
std::unordered_map<std::string, std::string> fullState;
};
struct IncrementalEvent
{
std::string targetId;
int64_t baseVersion = 0; // 이 이벤트가 적용되기 직전에 대상이 있어야 할 버전.
int64_t resultVersion = 0; // 이 이벤트가 적용되면 도달하는 버전.
std::string fieldName;
std::string newValue;
};
class TargetSyncStore
{
public:
// 벌크 스냅샷을 수신했을 때 호출된다.
void ApplySnapshot(const SnapshotMessage& snapshot)
{
// (A) 스냅샷을 받으면 현재 상태와 무관하게 그대로 덮어써 반영한다.
TargetState state;
state.targetId = snapshot.targetId;
state.version = snapshot.version;
state.fields = snapshot.fullState;
states_[snapshot.targetId] = std::move(state);
}
// 증분 이벤트를 수신했을 때 호출된다.
void ApplyIncremental(const IncrementalEvent& evt)
{
auto it = states_.find(evt.targetId);
if (it == states_.end())
{
// 아직 이 대상에 대한 스냅샷/상태가 하나도 없다.
TargetState state;
state.targetId = evt.targetId;
state.version = 0;
it = states_.emplace(evt.targetId, std::move(state)).first;
}
// (B) 이벤트를 받은 순서 그대로 반영한다.
TargetState& state = it->second;
state.fields[evt.fieldName] = evt.newValue;
state.version = evt.resultVersion;
}
private:
std::unordered_map<std::string, TargetState> states_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.