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#
내 답안 · 자동 저장

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