16. 버프/디버프 중첩·만료 타이밍 경합 (서버 권위)

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 버프/디버프 중첩·만료 타이밍 경합 (서버 권위)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 스킬/공격이 적중하면 대상에게 버프(또는 디버프)가 적용된다. 같은 버프가
//     다시 적용되면 "중첩(stack)"이 쌓이고 지속시간이 갱신(refresh)된다.
//   - 각 버프는 최대 중첩 수(MaxStack)와 만료 시각(ExpireAt)을 가진다.
//   - 서버에는 만료 스윕 타이머가 주기적으로 돌며 만료된 버프를 제거한다.
//   - 만료 스윕(타이머 스레드)과 버프 적용/공격 처리(로직 스레드)는 같은 대상의
//     버프 목록을 동시에 건드릴 수 있다.
//   - 스탯(공격력/이동속도 등)은 현재 활성 버프들로부터 매 틱 재계산된다.
//
// 요구사항:
//   - 버프 중첩은 MaxStack 을 절대 넘으면 안 된다.
//   - 만료된 버프의 효과가 스탯 계산에 반영되면 안 되고, 아직 유효한 버프가
//     "갱신과 만료가 겹쳐" 사라지면 안 된다(유령 만료/유실 갱신 금지).
//   - 만료 판정은 서버 권위의 단조(monotonic) 시간 기준이어야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 깨지는지(동시 인터리빙 포함)
//   설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class Buff
{
    public int      BuffId;
    public int      Stack;
    public int      MaxStack;
    public DateTime ExpireAt;       // 만료 시각
    public int      PerStackPower;  // 중첩 1당 효과량
}

public class Unit
{
    public long Id;
    // buffId -> Buff
    public Dictionary<int, Buff> Buffs = new Dictionary<int, Buff>();
    public int AttackPower;         // 매 틱 재계산되는 캐시값
}

public class BuffService
{
    private readonly int _durationMs;

    public BuffService(int durationMs) { _durationMs = durationMs; }

    // 스킬 적중 시 호출: 버프 적용(또는 중첩/갱신)
    public void ApplyBuff(Unit u, int buffId, int maxStack, int perStackPower)
    {
        // (A) 기존 버프 조회/중첩
        if (u.Buffs.TryGetValue(buffId, out var b))
        {
            // 중첩 증가 + 지속시간 갱신
            if (b.Stack < b.MaxStack)
                b.Stack++;
            b.ExpireAt = DateTime.UtcNow.AddMilliseconds(_durationMs);
        }
        else
        {
            u.Buffs[buffId] = new Buff
            {
                BuffId = buffId,
                Stack = 1,
                MaxStack = maxStack,
                ExpireAt = DateTime.UtcNow.AddMilliseconds(_durationMs),
                PerStackPower = perStackPower
            };
        }
    }

    // 만료 스윕 타이머가 주기적으로 호출
    public void SweepExpired(Unit u)
    {
        // (B) 만료된 버프 제거
        foreach (var kv in u.Buffs)
        {
            if (kv.Value.ExpireAt <= DateTime.UtcNow)
                u.Buffs.Remove(kv.Key);
        }
    }

    // 매 틱 스탯 재계산
    public void RecalcStats(Unit u)
    {
        int power = 0;
        // (C) 현재 버프들로 스탯 합산
        foreach (var b in u.Buffs.Values)
            power += b.Stack * b.PerStackPower;

        u.AttackPower = power;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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