16. 해설 (C#) — 서버 간 시계 차이로 인한 만료/쿨다운 판정 오류 (서버-서버)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 서버 간 시계 차이로 인한 만료/쿨다운 판정 오류 (서버-서버)
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜 / 서버-서버):
// - 존 서버 A 가 버프(또는 쿨다운)를 부여하면, 만료 시각을 계산해 메시지에 실어
// 존 서버 B(플레이어가 이동해 갈 대상 서버)로 보낸다.
// - B 는 받은 메시지로 버프를 자기 시뮬레이션에 적용하고, 매 틱 만료 여부를 판정한다.
// - A 와 B 는 물리적으로 다른 머신이고, 각자의 시스템 시계는 NTP 로 맞추지만
// 수십~수백 ms (때로 더 큰) 오차가 있을 수 있다(시계 동기 오차/드리프트).
// - 쿨다운도 같은 방식이다: A 가 "이 스킬은 t 시각까지 쿨다운" 이라고 B 에 넘긴다.
//
// 요구사항:
// - 플레이어가 A→B 로 넘어가도 버프 잔여시간/쿨다운 잔여시간이 "같게" 유지돼야 한다
// (서버 시계 차이로 버프가 즉시 사라지거나 영원히 안 끝나면 안 됨).
// - 만료/쿨다운 판정은 단일 서버 시계가 아니라 "합의된 시간 기준" 으로 해야 한다.
// - 직렬화 값은 두 머신이 같게 해석돼야 한다(폭/엔디안/단위 일관).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 잘못 판정되는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
public struct BuffGrantMsg
{
public int BuffId;
public long ExpireAtMs; // "만료 시각"(절대값)
}
public struct CooldownMsg
{
public int SkillId;
public long ReadyAtMs; // "사용 가능해지는 시각"(절대값)
}
public static class TimeAuthority
{
// 송신 측(서버 A): 지속시간으로부터 만료 "절대 시각" 을 만든다.
public static BuffGrantMsg MakeBuffGrant(int buffId, int durationMs)
{
// (A)
long now = WallClockMs();
return new BuffGrantMsg { BuffId = buffId, ExpireAtMs = now + durationMs };
}
public static CooldownMsg MakeCooldown(int skillId, int cooldownMs)
{
long now = WallClockMs();
return new CooldownMsg { SkillId = skillId, ReadyAtMs = now + cooldownMs };
}
// 수신 측(서버 B): 자기 시계로 만료/쿨다운을 판정한다.
public static bool IsBuffExpired(in BuffGrantMsg g)
{
// (B)
return WallClockMs() >= g.ExpireAtMs;
}
public static bool IsSkillReady(in CooldownMsg c)
{
// (C)
return WallClockMs() >= c.ReadyAtMs;
}
// 남은 시간(클라 표시/연출용)
public static long RemainingMs(in BuffGrantMsg g) => g.ExpireAtMs - WallClockMs();
// 각 서버의 시스템(벽)시계
private static long WallClockMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 서버 간 시계 차이로 인한 만료/쿨다운 판정 오류 (서버-서버)
// ----------------------------------------------------------------------------
// 시나리오 (프로토콜 / 서버-서버):
// - 존 서버 A 가 버프(또는 쿨다운)를 부여하면, 만료 시각을 계산해 메시지에 실어
// 존 서버 B(플레이어가 이동해 갈 대상 서버)로 보낸다.
// - B 는 받은 메시지로 버프를 자기 시뮬레이션에 적용하고, 매 틱 만료 여부를 판정한다.
// - A 와 B 는 물리적으로 다른 머신이고, 각자의 시스템 시계는 NTP 로 맞추지만
// 수십~수백 ms (때로 더 큰) 오차가 있을 수 있다(시계 동기 오차/드리프트).
// - 쿨다운도 같은 방식이다: A 가 "이 스킬은 t 시각까지 쿨다운" 이라고 B 에 넘긴다.
//
// 요구사항:
// - 플레이어가 A→B 로 넘어가도 버프 잔여시간/쿨다운 잔여시간이 "같게" 유지돼야 한다
// (서버 시계 차이로 버프가 즉시 사라지거나 영원히 안 끝나면 안 됨).
// - 만료/쿨다운 판정은 단일 서버 시계가 아니라 "합의된 시간 기준" 으로 해야 한다.
// - 직렬화 값은 두 머신이 같게 해석돼야 한다(폭/엔디안/단위 일관).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 잘못 판정되는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <chrono>
// 서버-서버로 오가는 버프 부여 메시지(직렬화 대상)
struct BuffGrantMsg {
std::int32_t buffId;
std::int64_t expireAtMs; // "만료 시각"(절대값)
};
// 서버-서버로 오가는 쿨다운 메시지
struct CooldownMsg {
std::int32_t skillId;
std::int64_t readyAtMs; // "사용 가능해지는 시각"(절대값)
};
class TimeAuthority {
public:
// 송신 측(서버 A): 지속시간으로부터 만료 "절대 시각" 을 만든다.
static BuffGrantMsg MakeBuffGrant(std::int32_t buffId, std::int32_t durationMs) {
// (A)
std::int64_t now = WallClockMs();
return BuffGrantMsg{ buffId, now + durationMs };
}
static CooldownMsg MakeCooldown(std::int32_t skillId, std::int32_t cooldownMs) {
std::int64_t now = WallClockMs();
return CooldownMsg{ skillId, now + cooldownMs };
}
// 수신 측(서버 B): 자기 시계로 만료/쿨다운을 판정한다.
static bool IsBuffExpired(const BuffGrantMsg& g) {
// (B)
return WallClockMs() >= g.expireAtMs;
}
static bool IsSkillReady(const CooldownMsg& c) {
// (C)
return WallClockMs() >= c.readyAtMs;
}
// 남은 시간(클라 표시/연출용)
static std::int64_t RemainingMs(const BuffGrantMsg& g) {
return g.expireAtMs - WallClockMs();
}
private:
static std::int64_t WallClockMs() {
using namespace std::chrono;
// 각 서버의 시스템(벽)시계
return duration_cast<milliseconds>(
system_clock::now().time_since_epoch()).count();
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.