41. 계정 공용 창고, 세션 단위 락으로 보호된 공유 자원 (C#)

난이도 중 해설 보기 →

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

결함 코드 · C#
// [시나리오] MMORPG 계정 공용 창고 (Account Warehouse, 서버-클라)
// 계정 공용 창고는 그 계정에 속한 모든 캐릭터가 공유하는 저장 공간이다.
// 한 계정으로 여러 캐릭터를 만들 수 있고, PC방/멀티클라이언트 환경에서는
// 같은 계정의 서로 다른 캐릭터가 동시에 접속해 있을 수 있다(캐릭터 A로
// 한 클라이언트, 캐릭터 B로 다른 클라이언트).
//
// 창고 슬롯은 슬롯 번호별로 아이템 수량을 보관하며, 임의의 캐릭터가
// 예치(Deposit)/인출(Withdraw)할 수 있다. 캐릭터별 세션은 각자 독립적인
// 네트워크 처리 스레드에서 요청을 처리한다.
//
// 요구사항:
//   - 같은 슬롯에서 실제로 인출되는 총량은 그 슬롯의 보유 수량을 넘어설 수 없다
//     (슬롯 수량은 음수가 될 수 없다).
//   - 같은 계정의 서로 다른 캐릭터가 동시에 인출을 요청해도 위 규칙이 지켜져야
//     한다(세션이 다르다고 해서 별도 자원으로 취급하면 안 된다).
//
// 과제: (A)(B) 지점을 검토하라. 같은 계정의 두 캐릭터가 동시에 같은 슬롯을
//       인출할 때 어떤 조건에서 요구사항이 깨지는지 밝히고 수정하라.

using System.Collections.Generic;

public sealed class CharacterSession
{
    public long AccountId;
    public long CharacterId;

    // 이 캐릭터 세션 전용 락 — 계정 창고 접근을 이 락으로 보호한다고 가정.
    public readonly object SessionLock = new object();
}

public sealed class AccountWarehouse
{
    public long AccountId;
    // slotId -> 수량
    public Dictionary<int, int> Slots = new Dictionary<int, int>();
}

public sealed class WarehouseService
{
    // accountId -> 창고
    private readonly Dictionary<long, AccountWarehouse> _warehouses = new Dictionary<long, AccountWarehouse>();

    private AccountWarehouse GetWarehouse(long accountId)
    {
        lock (_warehouses)
        {
            if (!_warehouses.TryGetValue(accountId, out var wh))
            {
                wh = new AccountWarehouse { AccountId = accountId };
                _warehouses[accountId] = wh;
            }
            return wh;
        }
    }

    // 캐릭터가 접속한 세션 스레드에서 호출된다. 같은 계정의 다른 캐릭터가
    // 동시에 다른 세션 스레드에서 아래 메서드를 호출할 수 있다.
    public bool Withdraw(CharacterSession session, int slotId, int amount)
    {
        var wh = GetWarehouse(session.AccountId);

        // (A) 세션 단위 락 — 같은 계정이라도 캐릭터(세션)가 다르면 서로 다른 락 객체다.
        lock (session.SessionLock)
        {
            if (!wh.Slots.TryGetValue(slotId, out var qty) || qty < amount)
                return false;

            // (B)
            wh.Slots[slotId] = qty - amount;
        }

        return true;
    }

    public void Deposit(CharacterSession session, int slotId, int amount)
    {
        var wh = GetWarehouse(session.AccountId);

        lock (session.SessionLock)
        {
            wh.Slots.TryGetValue(slotId, out var qty);
            wh.Slots[slotId] = qty + amount;
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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