34. 로그인 대기열 동접 상한의 check-then-act와 이탈 처리 (C#)

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 로그인 대기열 동접 상한 제어 (Admission Control)
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-클라):
//   - 서버는 동시 접속자 상한(MaxConcurrent)을 두고, 그 이상은 로그인을
//     즉시 거부하는 대신 대기열(Queue)에 순서대로 넣는다.
//   - 접속 슬롯이 하나 비면(누군가 로그아웃/끊김) 대기열 맨 앞 클라이언트를
//     차례로 입장시킨다.
//   - 클라이언트 로그인 요청, 로그인 완료 처리, 로그아웃/끊김 처리는 각각
//     별도의 네트워크 워커 스레드에서 동시에 호출될 수 있다.
//   - 대기 중인 클라이언트도 언제든 연결을 끊을 수 있다(대기 포기, 앱 종료 등).
//
// 요구사항:
//   - 실제 접속 중인 인원(입장 처리된 세션 수)은 MaxConcurrent 를 절대
//     넘어서는 안 된다.
//   - 상한 미만으로 여유가 생기면 대기열에 남은 클라이언트가 결국 입장할 수
//     있어야 한다(대기 중 이탈이 반영되지 않아 여유가 있는데도 계속 거부되면
//     안 된다).
//
// 과제: (A)(B)(C) 지점을 검토하라. 동시 요청이 몰릴 때, 그리고 대기 중
//       클라이언트가 이탈할 때 각각 어떤 문제가 생기는지 밝히고 수정하라.

using System.Collections.Generic;

public sealed class LoginGate
{
    private readonly int _maxConcurrent;
    private int _currentCount;
    private readonly Queue<long> _waitQueue = new Queue<long>();
    private readonly HashSet<long> _waiting = new HashSet<long>();

    public LoginGate(int maxConcurrent)
    {
        _maxConcurrent = maxConcurrent;
    }

    // 클라이언트 로그인 요청 — 네트워크 워커 스레드에서 호출.
    public bool TryEnter(long connId)
    {
        // (A) 검사와 증가가 분리되어 있어 여러 스레드가 동시에 호출하면
        //     둘 다 통과할 수 있다.
        if (_currentCount < _maxConcurrent)
        {
            _currentCount++;
            return true; // 즉시 입장
        }

        // 상한 초과 — 대기열에 등록
        _waitQueue.Enqueue(connId);
        _waiting.Add(connId);
        return false;
    }

    // 접속 중이던 클라이언트가 로그아웃/끊김 — 네트워크 워커 스레드에서 호출.
    public void OnLeave(long connId)
    {
        // (B) 이 connId 가 "접속 중"이었는지 "대기 중"이었는지 구분하지 않고
        //     항상 _currentCount 를 감소시킨다.
        _currentCount--;

        PromoteNext();
    }

    // 대기 중이던 클라이언트가 연결을 끊음(대기 포기) — 네트워크 워커 스레드에서 호출.
    public void OnCancelWaiting(long connId)
    {
        // (C) _waitQueue(Queue<long>) 에서는 특정 원소만 제거할 수 없으므로
        //     _waiting 에서만 제거 표시하고, 실제 큐 정리는 PromoteNext 가
        //     꺼낼 때에야 이루어진다.
        _waiting.Remove(connId);
    }

    private void PromoteNext()
    {
        while (_currentCount < _maxConcurrent && _waitQueue.Count > 0)
        {
            var next = _waitQueue.Dequeue();
            if (!_waiting.Contains(next))
                continue; // 이미 이탈한 대상 — 건너뜀

            _waiting.Remove(next);
            _currentCount++;
            // 실제로는 여기서 next 에게 입장 허가 통지를 보낸다.
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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