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#
내 답안 · 자동 저장

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