26. 만료(기간제) 아이템 자동 삭제와 사용이 동시에 일어나는 상황

난이도 중 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 만료(기간제) 아이템 자동 삭제와 사용이 동시에 일어나는 상황
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
//   - 기간제 아이템(예: 7일 경험치 부스터, 이벤트 코스튬, 30분 물약)은 만료 시각이
//     지나면 백그라운드 스윕(sweep) 워커가 주기적으로 인벤토리에서 자동 삭제한다.
//   - 동시에 플레이어는 그 아이템을 "사용(use)" 할 수 있다. 사용 핸들러는
//     존재 확인 -> 만료 확인 -> 효과 적용 -> 수량 차감(0이면 슬롯 제거) 순으로 처리한다.
//   - 스윕 워커와 사용 요청(그리고 더블클릭으로 인한 동시 사용 요청)은 서로 다른
//     스레드에서 같은 플레이어의 같은 아이템에 동시에 닿을 수 있다.
//
// 요구사항:
//   - 만료된 아이템은 효과가 적용되면 안 된다(만료 후 사용 금지).
//   - 한 번의 사용은 정확히 한 번만 효과를 적용하고 수량을 한 번만 차감해야 한다
//     (효과 이중 적용·이중 차감·음수 수량 금지).
//   - 스윕 삭제와 사용이 같은 아이템에 동시에 닿아도 자료구조가 손상되거나
//     "효과는 적용됐는데 아이템은 사라진"/"삭제됐는데 또 삭제하는" 모순이 없어야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 만료 후 사용·효과 이중 적용·수량 손상·
//   순회 중 삭제 예외가 동시 인터리빙으로 발생하는지 설명하고, 수정안과 더 나은 설계를
//   제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class ItemInstance
{
    public long Uid;          // 인스턴스 고유 id
    public int  ItemId;
    public int  Count;
    public long ExpireAtMs;   // 만료 절대시각(ms). 0 = 무기한
}

public class TimedItemService
{
    // playerId -> (uid -> 아이템)
    private readonly Dictionary<long, Dictionary<long, ItemInstance>> _inv = new();
    private readonly Func<long> _nowMs;
    private readonly Action<long, int> _applyEffect;   // (playerId, itemId)

    public TimedItemService(Func<long> nowMs, Action<long, int> applyEffect)
    {
        _nowMs = nowMs;
        _applyEffect = applyEffect;
    }

    // 플레이어가 기간제 아이템 사용 (네트워크 스레드/워커에서 호출, 더블클릭 시 동시 진입 가능)
    public bool UseItem(long playerId, long uid)
    {
        if (!_inv.TryGetValue(playerId, out var bag)) return false;
        if (!bag.TryGetValue(uid, out var item)) return false;

        // (A) 이 흐름(검사 → 효과 적용 → 차감)을 검토하라
        if (item.ExpireAtMs != 0 && item.ExpireAtMs <= _nowMs())
            return false;                      // 만료됐으면 사용 불가

        _applyEffect(playerId, item.ItemId);   // 먼저 효과 적용
        item.Count--;                          // 그 다음 수량 차감
        if (item.Count <= 0)
            bag.Remove(uid);
        return true;
    }

    // 백그라운드 스윕: 만료된 아이템을 자동 삭제 (타이머 스레드에서 주기 호출)
    public void SweepExpired(long playerId)
    {
        if (!_inv.TryGetValue(playerId, out var bag)) return;
        long now = _nowMs();

        // (B) 이 순회/삭제 패턴과 사용 경로의 관계를 검토하라
        foreach (var kv in bag)
        {
            var it = kv.Value;
            if (it.ExpireAtMs != 0 && it.ExpireAtMs <= now)
                bag.Remove(kv.Key);
        }
    }

    // 아이템 지급(획득/구매/우편 수령 등)
    public void AddItem(long playerId, ItemInstance item)
    {
        // (C) 이 동시 접근 경로를 검토하라
        if (!_inv.TryGetValue(playerId, out var bag))
        {
            bag = new Dictionary<long, ItemInstance>();
            _inv[playerId] = bag;
        }
        bag[item.Uid] = item;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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