11. 스킬 사용 검증과 쿨다운 (서버 권위)

난이도 하 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 스킬 사용 검증과 쿨다운 (서버 권위)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 플레이어가 스킬을 쓰면 클라이언트가 C_UseSkill 패킷을 보낸다.
//     payload = [int skillId][long clientCooldownEndMs][int manaCost]
//   - 서버는 스킬을 시전 처리하고(데미지/효과는 생략), 다음 사용 가능 시각까지
//     쿨다운을 건다. 쿨다운이 안 끝났으면 시전을 거부해야 한다.
//   - 마나가 부족하면 시전할 수 없다.
//   - 클라이언트는 연출(쿨다운 게이지)을 부드럽게 하려고 자기가 계산한
//     "쿨다운 종료 시각(clientCooldownEndMs)" 과 스킬 비용(manaCost)을 함께 보낸다.
//   - 클라이언트는 신뢰할 수 없다(치터/매크로/패킷 변조 가능).
//   - 같은 플레이어의 패킷이라도 여러 IO 스레드에서 동시에 처리될 수 있다
//     (더블클릭/매크로 연타로 같은 스킬 패킷이 거의 동시에 두 번 도착).
//
// 요구사항:
//   - 쿨다운/마나/스킬 보유 여부는 서버가 권위 있게 검증해야 한다.
//   - 쿨다운 중에는 절대 재시전되면 안 된다(연타로도 뚫리면 안 됨).
//   - 마나 차감과 쿨다운 적용과 시전은 함께 일어나야 한다(부분 적용 금지).
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용되는지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class SkillDef
{
    public int  SkillId;
    public long CooldownMs;     // 서버가 아는 진짜 쿨다운
    public int  ManaCost;       // 서버가 아는 진짜 마나 비용
}

public class Player
{
    public long Id;
    public int  Mana;
    // skillId -> 다음 사용 가능 시각(ms, Unix epoch)
    public Dictionary<int, long> CooldownEndMs = new Dictionary<int, long>();
}

public class SkillService
{
    private readonly Dictionary<int, SkillDef> _defs;          // skillId -> def
    private readonly Dictionary<long, Player>  _players;

    public SkillService(Dictionary<int, SkillDef> defs, Dictionary<long, Player> players)
    {
        _defs = defs;
        _players = players;
    }

    private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    // C_UseSkill 처리. 시전 성공 시 true.
    public bool UseSkill(long playerId, int skillId, long clientCooldownEndMs, int manaCost)
    {
        Player p = _players[playerId];
        SkillDef def = _defs[skillId];

        long now = NowMs();

        // (A) 쿨다운 검사: 클라가 보낸 종료 시각으로 판단
        if (p.CooldownEndMs.TryGetValue(skillId, out long end))
        {
            if (now < clientCooldownEndMs)
                return false;   // 아직 쿨다운 중
        }

        // (B) 마나 검사/차감: 클라가 보낸 manaCost 사용
        if (p.Mana < manaCost)
            return false;
        p.Mana -= manaCost;

        // (C) 시전 처리(데미지/효과는 생략)
        CastEffect(p, skillId);

        // (D) 쿨다운 적용: 클라가 보낸 종료 시각을 그대로 저장
        p.CooldownEndMs[skillId] = clientCooldownEndMs;

        return true;
    }

    private void CastEffect(Player p, int skillId)
    {
        // 데미지 계산/투사체 생성 등 (생략)
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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