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++
// ============================================================================
// [코드리뷰 문제] C++ - 버프/디버프 중첩·만료 타이밍 경합 (서버 권위)
// ----------------------------------------------------------------------------
// 시나리오:
// - 스킬/공격이 적중하면 대상에게 버프(또는 디버프)가 적용된다. 같은 버프가
// 다시 적용되면 중첩(stack)이 쌓이고 지속시간이 갱신된다.
// - 각 버프는 최대 중첩(maxStack)과 만료 시각(expireAt)을 가진다.
// - 만료 스윕 타이머(타이머 스레드)와 버프 적용/공격 처리(로직 스레드)는 같은
// 대상의 버프 목록을 동시에 건드릴 수 있다.
// - 스탯은 현재 활성 버프들로부터 매 틱 재계산된다.
//
// 요구사항:
// - 중첩은 maxStack 을 절대 넘으면 안 된다.
// - 만료된 버프 효과가 스탯에 반영되거나, 갱신과 만료가 겹쳐 유효 버프가
// 사라지면 안 된다(유령 만료/유실 갱신 금지).
// - 만료 판정은 서버 권위의 단조(monotonic) 시간 기준이어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 깨지는지(경합 인터리빙·반복자
// 무효화 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
#include <ctime>
struct Buff {
int buffId;
int stack;
int maxStack;
int64_t expireAt; // time(nullptr) 기준 만료 (초)
int perStackPower;
};
struct Unit {
int64_t id;
std::unordered_map<int, Buff> buffs; // buffId -> Buff
int attackPower = 0; // 매 틱 재계산되는 캐시값
};
class BuffService {
public:
explicit BuffService(int durationSec) : durationSec_(durationSec) {}
// 스킬 적중 시: 버프 적용(또는 중첩/갱신)
void ApplyBuff(Unit& u, int buffId, int maxStack, int perStackPower) {
int64_t now = ::time(nullptr);
// (A) 기존 버프 조회/중첩
auto it = u.buffs.find(buffId);
if (it != u.buffs.end()) {
Buff& b = it->second;
if (b.stack < b.maxStack)
b.stack++;
b.expireAt = now + durationSec_; // 갱신
} else {
u.buffs[buffId] = Buff{buffId, 1, maxStack, now + durationSec_, perStackPower};
}
}
// 만료 스윕 타이머가 주기적으로 호출
void SweepExpired(Unit& u) {
int64_t now = ::time(nullptr);
// (B) 만료된 버프 제거
for (auto it = u.buffs.begin(); it != u.buffs.end(); ++it) {
if (it->second.expireAt <= now)
u.buffs.erase(it);
}
}
// 매 틱 스탯 재계산
void RecalcStats(Unit& u) {
int power = 0;
// (C) 현재 버프들로 스탯 합산
for (auto& kv : u.buffs)
power += kv.second.stack * kv.second.perStackPower;
u.attackPower = power;
}
private:
int durationSec_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.