42. 소비 아이템 공유 쿨다운 그룹의 동시 사용 우회 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: MMORPG 전투 서버 — 소비 아이템(포션) 사용 처리 (C#)
//
// 회복/버프 포션은 "쿨다운 그룹"으로 묶인다. 예를 들어 모든 HP 포션은
// 하나의 그룹(예: 그룹 7)을 공유하고, 그 그룹은 10초 쿨다운을 가진다.
// 서로 다른 종류의 HP 포션이라도 같은 그룹이면 쿨다운을 공유해야 한다.
//
// 요구사항:
// - 같은 쿨다운 그룹은 쿨다운이 지나기 전에는 다시 사용할 수 없다.
// - 아이템 개수는 사용할 때마다 정확히 1개씩만 줄어야 한다(음수 불가).
// - 한 플레이어가 여러 클라이언트/여러 패킷으로 동시에 사용 요청을 보내도
// 위 규칙이 지켜져야 한다.
//
// 아래는 사용 요청 처리기다. 수신 스레드 풀에서 같은 플레이어의 요청이
// 동시에 들어올 수 있다. 표시된 지점 (A)(B) 를 중심으로 검토하라.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public sealed class ConsumableService
{
private sealed class Player
{
public Dictionary<int, int> ItemCount = new(); // itemId -> 개수
public Dictionary<int, DateTime> GroupLastUse = new(); // groupId -> 마지막 사용 시각
}
private readonly Dictionary<long, Player> _players = new();
private readonly Dictionary<int, int> _itemGroup; // itemId -> groupId
private readonly Dictionary<int, double> _groupCooldown; // groupId -> 초
public ConsumableService(Dictionary<int, int> itemGroup, Dictionary<int, double> groupCooldown)
{
_itemGroup = itemGroup;
_groupCooldown = groupCooldown;
}
public async Task<bool> UseItemAsync(long playerId, int itemId)
{
Player p = _players[playerId];
int group = _itemGroup[itemId];
double cd = _groupCooldown[group];
// (A) 쿨다운 검사
if (p.GroupLastUse.TryGetValue(group, out DateTime last))
{
if ((DateTime.UtcNow - last).TotalSeconds < cd)
return false; // 아직 쿨다운 중
}
if (!p.ItemCount.TryGetValue(itemId, out int cnt) || cnt <= 0)
return false; // 재고 없음
// 효과 적용(회복/버프)은 DB·버프 매니저 호출을 포함해 약간의 지연이 있다.
await ApplyEffectAsync(playerId, itemId);
// (B) 재고 차감 + 쿨다운 갱신
p.ItemCount[itemId] = cnt - 1;
p.GroupLastUse[group] = DateTime.UtcNow;
return true;
}
private Task ApplyEffectAsync(long playerId, int itemId) => Task.Delay(5);
} 결함 코드 · C++
// 시나리오: MMORPG 전투 서버 — 소비 아이템(포션) 사용 처리 (C++)
//
// 회복/버프 포션은 "쿨다운 그룹"으로 묶인다. 같은 그룹(예: 모든 HP 포션)은
// 쿨다운을 공유한다. 서로 다른 포션이라도 같은 그룹이면 함께 쿨다운에 걸려야 한다.
//
// 요구사항:
// - 같은 쿨다운 그룹은 쿨다운이 지나기 전에는 다시 사용할 수 없다.
// - 아이템 개수는 사용 시 정확히 1개씩만 줄어야 한다(음수 불가).
// - 여러 워커 스레드가 같은 플레이어의 요청을 동시에 처리할 수 있다.
//
// 아래는 사용 요청 처리기다. 표시된 지점 (A)(B) 를 중심으로 검토하라.
#include <cstdint>
#include <chrono>
#include <unordered_map>
using Clock = std::chrono::steady_clock;
struct Player {
std::unordered_map<int, int> itemCount; // itemId -> 개수
std::unordered_map<int, Clock::time_point> groupLastUse; // groupId -> 마지막 사용
};
class ConsumableService {
public:
ConsumableService(std::unordered_map<int,int> itemGroup,
std::unordered_map<int,double> groupCooldown)
: itemGroup_(std::move(itemGroup)), groupCooldown_(std::move(groupCooldown)) {}
bool UseItem(std::int64_t playerId, int itemId) {
Player& p = players_[playerId];
int group = itemGroup_[itemId];
double cd = groupCooldown_[group];
auto now = Clock::now();
// (A) 쿨다운 검사
auto it = p.groupLastUse.find(group);
if (it != p.groupLastUse.end()) {
double elapsed = std::chrono::duration<double>(now - it->second).count();
if (elapsed < cd)
return false; // 쿨다운 중
}
auto ci = p.itemCount.find(itemId);
if (ci == p.itemCount.end() || ci->second <= 0)
return false; // 재고 없음
int remaining = ci->second;
// 효과 적용(회복/버프 매니저 호출) — 약간의 처리 지연이 있다.
ApplyEffect(playerId, itemId);
// (B) 재고 차감 + 쿨다운 갱신
p.itemCount[itemId] = remaining - 1;
p.groupLastUse[group] = Clock::now();
return true;
}
private:
void ApplyEffect(std::int64_t, int) { /* 회복/버프 적용 */ }
std::unordered_map<std::int64_t, Player> players_;
std::unordered_map<int, int> itemGroup_;
std::unordered_map<int, double> groupCooldown_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.