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++
// ============================================================================
// [코드리뷰 문제] C++ - 스킬 사용 검증과 쿨다운 (서버 권위)
// ----------------------------------------------------------------------------
// 시나리오:
// - 플레이어가 스킬을 쓰면 클라이언트가 C_UseSkill 패킷을 보낸다.
// payload = [int skillId][int64 clientCooldownEndMs][int manaCost]
// - 서버는 스킬을 시전 처리하고(데미지/효과는 생략), 다음 사용 가능 시각까지
// 쿨다운을 건다. 쿨다운이 안 끝났으면 시전을 거부해야 한다.
// - 마나가 부족하면 시전할 수 없다.
// - 클라이언트는 연출(쿨다운 게이지)을 부드럽게 하려고 자기가 계산한
// "쿨다운 종료 시각(clientCooldownEndMs)" 과 스킬 비용(manaCost)을 함께 보낸다.
// - 클라이언트는 신뢰할 수 없다(치터/매크로/패킷 변조 가능).
// - 같은 플레이어의 패킷이라도 여러 IO 스레드에서 동시에 처리될 수 있다
// (더블클릭/매크로 연타로 같은 스킬 패킷이 거의 동시에 두 번 도착).
//
// 요구사항:
// - 쿨다운/마나/스킬 보유 여부는 서버가 권위 있게 검증해야 한다.
// - 쿨다운 중에는 절대 재시전되면 안 된다(연타로도 뚫리면 안 됨).
// - 마나 차감과 쿨다운 적용과 시전은 함께 일어나야 한다(부분 적용 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용되는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <chrono>
#include <unordered_map>
struct SkillDef
{
int skillId;
int64_t cooldownMs; // 서버가 아는 진짜 쿨다운
int manaCost; // 서버가 아는 진짜 마나 비용
};
struct Player
{
int64_t id;
int mana;
// skillId -> 다음 사용 가능 시각(ms, Unix epoch)
std::unordered_map<int, int64_t> cooldownEndMs;
};
class SkillService
{
public:
SkillService(std::unordered_map<int, SkillDef>* defs,
std::unordered_map<int64_t, Player>* players)
: defs_(defs), players_(players) {}
// C_UseSkill 처리. 시전 성공 시 true.
bool UseSkill(int64_t playerId, int skillId,
int64_t clientCooldownEndMs, int manaCost)
{
Player& p = (*players_)[playerId];
SkillDef& def = (*defs_)[skillId];
int64_t now = NowMs();
// (A) 쿨다운 검사: 클라가 보낸 종료 시각으로 판단
auto it = p.cooldownEndMs.find(skillId);
if (it != p.cooldownEndMs.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:
static int64_t NowMs()
{
using namespace std::chrono;
return duration_cast<milliseconds>(
system_clock::now().time_since_epoch()).count();
}
void CastEffect(Player& /*p*/, int /*skillId*/) { /* 생략 */ }
std::unordered_map<int, SkillDef>* defs_;
std::unordered_map<int64_t, Player>* players_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.