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#
내 답안 · 자동 저장

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