8. 길드 자금고 동시 출금

난이도 상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 길드 자금고(금고) 동시 출금
// ----------------------------------------------------------------------------
// 시나리오:
//   - 길드는 공용 금고(GuildBank)를 가진다. 자금은 골드 잔액 하나.
//   - 길드원은 입금(Deposit)할 수 있고, 권한이 있는 임원(officer)은 출금(Withdraw).
//   - 출금에는 두 가지 한도가 있다:
//       (1) 금고 잔액을 초과해 출금 불가.
//       (2) 임원 1인당 "일일 출금 한도(dailyLimit)" 가 있어 하루 누적 출금이
//           그 이상이면 거부.
//   - 출금 로그(audit log)에 모든 출금을 기록한다.
//   - 여러 IO 스레드가 동시에 같은 길드의 입출금 패킷을 처리한다
//     (여러 임원이 동시에 출금 시도, 한 임원이 두 클라이언트로 동시 시도 등).
//
// 요구사항:
//   - 금고 잔액은 음수가 될 수 없다(초과 출금 금지).
//   - 임원별 일일 한도를 초과 출금할 수 없다.
//   - 출금 로그의 합계는 실제 잔액 변화와 정확히 일치해야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class WithdrawLog
{
    public long OfficerId;
    public long Amount;
    public DateTime At;
}

public class GuildBank
{
    public long GuildId;
    public long Balance;                                  // (A) 금고 잔액
    public long DailyLimit;                               // 임원 1인 일일 한도
    // 임원별 오늘 누적 출금
    public Dictionary<long, long> WithdrawnToday = new(); // (B)
    public List<WithdrawLog> Logs = new();                // (C)
    public readonly object Lock = new object();
}

public class GuildBankService
{
    private readonly Dictionary<long, GuildBank> _banks;
    public GuildBankService(Dictionary<long, GuildBank> banks) { _banks = banks; }

    public bool Deposit(long guildId, long playerId, long amount)
    {
        if (amount <= 0) return false;
        GuildBank bank = _banks[guildId];
        lock (bank.Lock)
        {
            bank.Balance += amount;                       // (D)
        }
        return true;
    }

    public bool Withdraw(long guildId, long officerId, long amount)
    {
        if (amount <= 0) return false;
        GuildBank bank = _banks[guildId];

        // (E) 잔액 확인
        if (bank.Balance < amount)
            return false;

        // (F) 일일 한도 확인
        long usedToday = bank.WithdrawnToday.TryGetValue(officerId, out var u) ? u : 0;
        if (usedToday + amount > bank.DailyLimit)
            return false;

        // (G) 금고에서 차감
        lock (bank.Lock)
        {
            bank.Balance -= amount;
        }

        // (H) 임원 누적/로그 갱신
        bank.WithdrawnToday[officerId] = usedToday + amount;
        bank.Logs.Add(new WithdrawLog {
            OfficerId = officerId, Amount = amount, At = DateTime.UtcNow
        });
        return true;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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