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++
// ============================================================================
// 시나리오 (서버-클라)
// ----------------------------------------------------------------------------
// 우편함(Mailbox) 시스템. 발신자는 이미 보낸 우편을 아직 수신자가 수령하지
// 않았다면 회수(Recall)할 수 있다 — 회수하면 첨부 골드가 발신자에게
// 환불된다. 수신자는 언제든 우편 첨부 골드를 수령(Claim)할 수 있다.
//
// 발신자의 회수 요청 핸들러와 수신자의 수령 요청 핸들러는 서버의 워커
// 스레드 풀에서 각각 다른 스레드가 처리할 수 있다. 같은 우편에 대해 두
// 요청이 거의 동시에 들어올 수 있다.
//
// 요구사항:
// - 첨부 골드는 정확히 한쪽(발신자 환불 또는 수신자 수령)에만 지급되어야
// 한다.
// - 어느 요청이 먼저 도착했는지와 무관하게, 둘 다 성공하거나 둘 다
// 실패해서는 안 된다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
// ============================================================================
#include <cstdint>
#include <mutex>
#include <unordered_map>
enum class MailState { Sent, Claimed, Recalled };
struct MailRecord
{
int64_t mailId;
uint64_t senderId;
uint64_t receiverId;
int64_t attachedGold;
MailState state;
};
class MailStore
{
public:
MailRecord* Find(int64_t mailId)
{
auto it = table_.find(mailId);
return it == table_.end() ? nullptr : &it->second;
}
void Insert(const MailRecord& mail) { table_[mail.mailId] = mail; }
private:
std::unordered_map<int64_t, MailRecord> table_;
};
class Wallet
{
public:
void AddGold(uint64_t userId, int64_t amount)
{
balances_[userId] += amount;
}
private:
std::unordered_map<uint64_t, int64_t> balances_;
};
class MailService
{
public:
MailService(MailStore* store, Wallet* wallet) : store_(store), wallet_(wallet) {}
// 발신자가 우편을 회수한다. 워커 스레드에서 호출됨.
bool Recall(int64_t mailId, uint64_t requesterId)
{
MailRecord* mail = store_->Find(mailId);
if (!mail || mail->senderId != requesterId)
return false;
// (A)
if (mail->state != MailState::Sent)
return false; // 이미 수령되었거나 이미 회수됨
wallet_->AddGold(mail->senderId, mail->attachedGold);
mail->state = MailState::Recalled;
return true;
}
// 수신자가 우편 첨부 골드를 수령한다. 다른 워커 스레드에서 호출될 수 있음.
bool Claim(int64_t mailId, uint64_t requesterId)
{
MailRecord* mail = store_->Find(mailId);
if (!mail || mail->receiverId != requesterId)
return false;
// (B)
if (mail->state != MailState::Sent)
return false; // 이미 회수되었거나 이미 수령됨
wallet_->AddGold(mail->receiverId, mail->attachedGold);
mail->state = MailState::Claimed;
return true;
}
private:
MailStore* store_;
Wallet* wallet_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.