18. 매칭 수락(레디 체크) 대기 중 취소/타임아웃 경합
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 매칭 수락(레디 체크) 대기 중 취소/타임아웃 경합
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-클라):
// - 매치메이커가 N명을 묶어 "매치 후보" 를 만들면, 각 플레이어에게 "수락하시겠습니까?"
// 팝업(레디 체크)을 띄운다.
// - 모두가 제한 시간(예: 10초) 안에 Accept 하면 매치가 성사되어 게임 서버로 보낸다.
// - 한 명이라도 Decline 하거나, 시간 안에 수락하지 않으면(타임아웃) 매치를 취소하고
// 수락했던 인원은 다시 큐로 돌려보낸다.
// - Accept/Decline 요청, 타임아웃 타이머는 서로 다른 스레드에서 동시에 처리된다.
//
// 요구사항:
// - 매치는 "전원 수락" 일 때 정확히 한 번만 성사돼야 한다(중복 성사 금지).
// - 타임아웃/취소가 일어났으면 그 후 도착한 Accept 로 매치가 성사돼선 안 된다.
// - 취소된 매치의 플레이어는 정확히 한 번 큐로 복귀해야 한다(이중 복귀/유실 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 중복 성사·유령 매치·이중 복귀가
// 발생하는지(동시 인터리빙 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
using System.Threading;
public class MatchProposal
{
public string MatchId;
public List<long> Players; // 후보 플레이어
public HashSet<long> Accepted = new();
public bool Cancelled = false;
public Timer TimeoutTimer; // 제한 시간 타이머
}
public class ReadyCheckManager
{
private readonly Dictionary<string, MatchProposal> _matches = new();
private readonly Action<MatchProposal> _startGame; // 게임 서버로 전송
private readonly Action<long> _requeue; // 큐로 복귀
public ReadyCheckManager(Action<MatchProposal> startGame, Action<long> requeue)
{
_startGame = startGame; _requeue = requeue;
}
public void Propose(MatchProposal m, int timeoutMs)
{
_matches[m.MatchId] = m;
// 제한 시간 후 OnTimeout 호출
m.TimeoutTimer = new Timer(_ => OnTimeout(m.MatchId), null, timeoutMs, Timeout.Infinite);
}
public void OnAccept(string matchId, long playerId)
{
if (!_matches.TryGetValue(matchId, out var m)) return;
// (A) 수락 등록 후 전원 수락 여부 검사
m.Accepted.Add(playerId);
if (m.Accepted.Count == m.Players.Count)
{
// (B) 전원 수락 → 게임 시작
_startGame(m);
_matches.Remove(matchId);
}
}
public void OnDecline(string matchId, long playerId)
{
if (!_matches.TryGetValue(matchId, out var m)) return;
Cancel(m);
}
private void OnTimeout(string matchId)
{
if (!_matches.TryGetValue(matchId, out var m)) return;
Cancel(m);
}
private void Cancel(MatchProposal m)
{
m.Cancelled = true;
// (C) 수락했던 인원을 큐로 복귀
foreach (var p in m.Accepted)
_requeue(p);
_matches.Remove(m.MatchId);
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 매칭 수락(레디 체크) 대기 중 취소/타임아웃 경합
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-클라):
// - 매치메이커가 N명을 묶어 "매치 후보" 를 만들면 각 플레이어에게 레디 체크 팝업을
// 띄운다. 모두가 제한 시간 안에 Accept 하면 매치 성사 → 게임 서버로 보낸다.
// - 한 명이라도 Decline 하거나 타임아웃이면 매치 취소, 수락 인원은 큐로 복귀.
// - Accept/Decline 요청과 타임아웃 콜백은 서로 다른 스레드에서 동시에 처리된다.
//
// 요구사항:
// - 매치는 "전원 수락" 일 때 정확히 한 번만 성사돼야 한다(중복 성사 금지).
// - 타임아웃/취소 후 도착한 Accept 로 매치가 성사돼선 안 된다.
// - 취소된 매치 인원은 정확히 한 번 큐로 복귀해야 한다(이중 복귀/유실 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 중복 성사·유령 매치·이중 복귀·UB가
// 발생하는지(동시 인터리빙 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <functional>
struct MatchProposal {
std::string matchId;
std::vector<std::int64_t> players;
std::unordered_set<std::int64_t> accepted;
bool cancelled = false;
};
class ReadyCheckManager {
public:
ReadyCheckManager(std::function<void(const MatchProposal&)> startGame,
std::function<void(std::int64_t)> requeue)
: startGame_(std::move(startGame)), requeue_(std::move(requeue)) {}
void Propose(MatchProposal m, int timeoutMs) {
matches_[m.matchId] = std::move(m);
// 제한 시간 후 OnTimeout(matchId) 가 타이머 스레드에서 호출된다고 가정
ScheduleAfter(timeoutMs, [this, id = matches_[m.matchId].matchId]() { OnTimeout(id); });
}
void OnAccept(const std::string& matchId, std::int64_t playerId) {
auto it = matches_.find(matchId);
if (it == matches_.end()) return;
MatchProposal& m = it->second;
// (A) 수락 등록 후 전원 수락 여부 검사
m.accepted.insert(playerId);
if (m.accepted.size() == m.players.size()) {
// (B) 전원 수락 → 게임 시작
startGame_(m);
matches_.erase(it);
}
}
void OnDecline(const std::string& matchId, std::int64_t) { Cancel(matchId); }
private:
void OnTimeout(const std::string& matchId) { Cancel(matchId); }
void Cancel(const std::string& matchId) {
auto it = matches_.find(matchId);
if (it == matches_.end()) return;
MatchProposal& m = it->second;
m.cancelled = true;
// (C) 수락했던 인원을 큐로 복귀
for (auto p : m.accepted)
requeue_(p);
matches_.erase(it);
}
void ScheduleAfter(int /*ms*/, std::function<void()> /*cb*/) { /* 타이머 등록(생략) */ }
std::function<void(const MatchProposal&)> startGame_;
std::function<void(std::int64_t)> requeue_;
std::unordered_map<std::string, MatchProposal> matches_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.