49. 장비 내구도 소모·파괴 처리의 원자성 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// [시나리오]
// MMORPG 장비 내구도 시스템. 플레이어가 피격/공격할 때마다 착용 장비의 내구도가 소모되고,
// 내구도가 0이 되면 장비는 "파괴" 처리된다. 파괴 시 인벤토리에서 제거되고, 보상 정책상
// 파괴 보상(파편 아이템)이 1회 지급되며 클라이언트에 파괴 알림이 전송된다.
//
// [요구사항]
// - 한 번의 내구도 소모 요청은 정확히 소모량만큼만 내구도를 줄인다.
// - 하나의 장비 인스턴스에 대해 파괴 처리(제거 + 파편 지급 + 알림)는 정확히 1회만 일어난다.
// - 전투 중에는 여러 스레드(피격 처리 워커, 스킬 처리 워커, 지속 피해 틱)가 같은 캐릭터의
// 같은 장비에 대해 동시에 내구도 소모를 요청할 수 있다.
//
// [과제]
// 아래 코드의 (A), (B) 지점을 중심으로 이 코드가 요구사항을 만족하지 못하는 조건을 찾고,
// 재현 시나리오와 수정안을 제시하시오.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace GameServer.Content
{
public sealed class EquipmentItem
{
public long ItemId;
public int Durability; // 남은 내구도
public int MaxDurability;
public bool Destroyed;
public int ShardReward = 3; // 파괴 시 지급 파편 수
}
public sealed class InventoryService
{
public void Remove(long ownerId, long itemId) { /* ... */ }
public void AddShards(long ownerId, int count) { /* ... */ }
}
public sealed class DurabilityService
{
private readonly Dictionary<long, EquipmentItem> _equipped = new();
private readonly InventoryService _inventory = new();
private readonly object _lock = new object();
public EquipmentItem GetEquipped(long ownerId, int slot)
{
lock (_lock)
{
return _equipped.TryGetValue(ownerId * 100 + slot, out var it) ? it : null;
}
}
// 전투 워커들이 동시에 호출한다.
public async Task ConsumeDurabilityAsync(long ownerId, int slot, int amount)
{
var item = GetEquipped(ownerId, slot);
if (item == null) return;
// (A)
if (item.Destroyed) return;
int remaining = item.Durability - amount;
item.Durability = remaining < 0 ? 0 : remaining;
if (item.Durability > 0)
return;
// (B)
item.Destroyed = true;
await PersistAsync(item);
_inventory.Remove(ownerId, item.ItemId);
_inventory.AddShards(ownerId, item.ShardReward);
Notify(ownerId, item.ItemId);
}
private Task PersistAsync(EquipmentItem item) => Task.Delay(5);
private void Notify(long ownerId, long itemId) { /* 클라 알림 */ }
}
} 결함 코드 · C++
// [시나리오]
// MMORPG 장비 내구도 시스템(C++ 전투 서버). 피격/공격/지속피해 틱마다 착용 장비의
// 내구도가 소모되고, 0이 되면 장비가 "파괴"되어 인벤토리에서 제거되고 파편 보상이
// 1회 지급되며 클라이언트에 알림이 나간다.
//
// [요구사항]
// - 한 소모 요청은 정확히 소모량만큼만 내구도를 줄인다.
// - 하나의 장비 인스턴스에 대한 파괴 처리(제거 + 파편 지급 + 알림)는 정확히 1회.
// - 여러 워커 스레드가 같은 캐릭터의 같은 장비에 동시 접근할 수 있다.
//
// [과제]
// (A), (B) 지점을 중심으로 요구사항이 깨지는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
namespace gs {
struct EquipmentItem {
std::int64_t itemId = 0;
int durability = 0;
int maxDurability = 0;
bool destroyed = false;
int shardReward = 3;
};
class InventoryService {
public:
void remove(std::int64_t /*ownerId*/, std::int64_t /*itemId*/) {}
void addShards(std::int64_t /*ownerId*/, int /*count*/) {}
};
class DurabilityService {
public:
EquipmentItem* getEquipped(std::int64_t ownerId, int slot) {
std::lock_guard<std::mutex> lk(mutex_);
auto it = equipped_.find(ownerId * 100 + slot);
return it == equipped_.end() ? nullptr : it->second.get();
}
// 여러 워커 스레드가 동시에 호출한다.
void consumeDurability(std::int64_t ownerId, int slot, int amount) {
EquipmentItem* item = getEquipped(ownerId, slot);
if (item == nullptr) return;
// (A)
if (item->destroyed) return;
int remaining = item->durability - amount;
item->durability = remaining < 0 ? 0 : remaining;
if (item->durability > 0) return;
// (B)
item->destroyed = true;
inventory_.remove(ownerId, item->itemId);
inventory_.addShards(ownerId, item->shardReward);
eraseEquipped(ownerId, slot);
notify(ownerId, item->itemId);
}
private:
void eraseEquipped(std::int64_t ownerId, int slot) {
std::lock_guard<std::mutex> lk(mutex_);
equipped_.erase(ownerId * 100 + slot);
}
void notify(std::int64_t /*ownerId*/, std::int64_t /*itemId*/) {}
std::mutex mutex_;
std::unordered_map<std::int64_t, std::unique_ptr<EquipmentItem>> equipped_;
InventoryService inventory_;
};
} // namespace gs
int main() { return 0; } 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.