39. 우편 회수와 수령의 동시 처리 (이중 지급 / 이중 유실)

난이도 중 해설 보기 →

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

결함 코드 · C#
// 시나리오: 우편함(Mailbox) 시스템. 발신자는 이미 보낸 우편을 아직 수신자가
// 수령하지 않았다면 회수(Recall)할 수 있다 — 회수하면 첨부된 아이템/재화가
// 발신자에게 환불된다. 수신자는 언제든 우편에 첨부된 아이템/재화를 수령
// (Claim)할 수 있다 — 수령하면 첨부물이 수신자에게 지급된다.
//
// 발신자의 회수 요청과 수신자의 수령 요청이 거의 동시에 들어올 수 있다
// (예: 발신자가 실수를 깨닫고 급히 회수를 누르는 순간, 수신자도 마침 우편함을
// 열어 수령을 누름).
//
// 요구사항:
//   - 첨부물은 정확히 한쪽(발신자 환불 또는 수신자 수령)에게만 지급되어야 한다.
//   - 회수와 수령 중 어느 것이 "먼저" 처리되었는지와 무관하게, 같은 우편에 대해
//     환불과 수령이 동시에 성공해서는 안 되고, 둘 다 실패해 첨부물이 허공으로
//     사라져서도 안 된다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.

using System.Threading.Tasks;

public enum MailState { Sent, Claimed, Recalled }

public sealed class MailRecord
{
    public long   MailId;
    public ulong  SenderId;
    public ulong  ReceiverId;
    public long   AttachedGold;
    public MailState State;
}

public interface IMailStore
{
    Task<MailRecord> LoadAsync(long mailId);
    Task SaveAsync(MailRecord mail);
}

public interface IWallet
{
    Task AddGoldAsync(ulong userId, long amount);
}

public sealed class MailService
{
    private readonly IMailStore _store;
    private readonly IWallet _wallet;

    public MailService(IMailStore store, IWallet wallet)
    {
        _store = store;
        _wallet = wallet;
    }

    // 발신자가 우편을 회수한다.
    public async Task<bool> RecallAsync(long mailId, ulong requesterId)
    {
        var mail = await _store.LoadAsync(mailId);
        if (mail == null || mail.SenderId != requesterId)
            return false;

        // (A)
        if (mail.State != MailState.Sent)
            return false; // 이미 수령되었거나 이미 회수됨

        await _wallet.AddGoldAsync(mail.SenderId, mail.AttachedGold);
        mail.State = MailState.Recalled;
        await _store.SaveAsync(mail);
        return true;
    }

    // 수신자가 우편 첨부물을 수령한다.
    public async Task<bool> ClaimAsync(long mailId, ulong requesterId)
    {
        var mail = await _store.LoadAsync(mailId);
        if (mail == null || mail.ReceiverId != requesterId)
            return false;

        // (B)
        if (mail.State != MailState.Sent)
            return false; // 이미 회수되었거나 이미 수령됨

        await _wallet.AddGoldAsync(mail.ReceiverId, mail.AttachedGold);
        mail.State = MailState.Claimed;
        await _store.SaveAsync(mail);
        return true;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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