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++
// ============================================================================
// [코드리뷰 문제] C++ - 만료(기간제) 아이템 자동 삭제와 사용이 동시에 일어나는 상황
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
// - 기간제 아이템(7일 부스터, 이벤트 코스튬, 30분 물약 등)은 만료 시각이 지나면
// 백그라운드 스윕(sweep) 워커가 주기적으로 인벤토리에서 자동 삭제한다.
// - 동시에 플레이어는 그 아이템을 "사용(use)" 할 수 있다. 사용 핸들러는
// 존재 확인 -> 만료 확인 -> 효과 적용 -> 수량 차감(0이면 슬롯 제거) 순으로 처리한다.
// - 스윕 워커와 사용 요청(더블클릭 시 동시 사용)이 서로 다른 스레드에서 같은 플레이어의
// 같은 아이템에 동시에 닿을 수 있다.
//
// 요구사항:
// - 만료된 아이템은 효과가 적용되면 안 된다(만료 후 사용 금지).
// - 한 번의 사용은 효과/차감을 정확히 한 번만(이중 적용·이중 차감·음수 수량 금지).
// - 스윕 삭제와 사용이 같은 아이템에 동시에 닿아도 자료구조가 손상되거나 무효 참조를
// 건드리지 않아야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 만료 후 사용·효과 이중 적용·수량 손상·
// 순회 중 삭제·참조 수명 문제가 동시 인터리빙으로 발생하는지 설명하고, 수정안과 더 나은
// 설계를 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
#include <functional>
struct ItemInstance {
int64_t uid;
int itemId;
int count;
int64_t expireAtMs; // 0 = 무기한
};
class TimedItemService {
public:
TimedItemService(std::function<int64_t()> nowMs, std::function<void(int64_t,int)> applyEffect)
: nowMs_(std::move(nowMs)), applyEffect_(std::move(applyEffect)) {}
// 사용 (네트워크 스레드/워커에서 호출, 더블클릭 시 동시 진입 가능)
bool useItem(int64_t playerId, int64_t uid) {
auto pit = inv_.find(playerId);
if (pit == inv_.end()) return false;
auto& bag = pit->second;
auto iit = bag.find(uid);
if (iit == bag.end()) return false;
ItemInstance& item = iit->second; // 맵 노드 참조를 들고 진행
// (A) 이 흐름(검사 → 효과 → 차감)을 검토하라
if (item.expireAtMs != 0 && item.expireAtMs <= nowMs_())
return false;
applyEffect_(playerId, item.itemId); // 먼저 효과
item.count--; // 그 다음 차감
if (item.count <= 0)
bag.erase(iit); // (이 지점 이후 위 참조 사용을 검토하라)
return true;
}
// 백그라운드 스윕: 만료 아이템 자동 삭제 (타이머 스레드 주기 호출)
void sweepExpired(int64_t playerId) {
auto pit = inv_.find(playerId);
if (pit == inv_.end()) return;
auto& bag = pit->second;
int64_t now = nowMs_();
// (B) 이 순회/삭제 패턴과 사용 경로의 관계를 검토하라
for (auto& kv : bag) {
if (kv.second.expireAtMs != 0 && kv.second.expireAtMs <= now)
bag.erase(kv.first);
}
}
// 아이템 지급(획득/구매/우편 수령 등)
void addItem(int64_t playerId, const ItemInstance& item) {
// (C) 이 동시 접근 경로를 검토하라
inv_[playerId][item.uid] = item;
}
private:
std::unordered_map<int64_t, std::unordered_map<int64_t, ItemInstance>> inv_;
std::function<int64_t()> nowMs_;
std::function<void(int64_t,int)> applyEffect_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.