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++
// ============================================================================
// [코드리뷰 문제] C++ - 로그인 대기열 동접 상한 제어 (Admission Control)
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-클라):
// - 서버는 동시 접속자 상한(maxConcurrent)을 두고, 그 이상은 로그인을
// 즉시 거부하는 대신 대기열에 순서대로 넣는다.
// - 접속 슬롯이 하나 비면(누군가 로그아웃/끊김) 대기열 맨 앞 클라이언트를
// 차례로 입장시킨다.
// - 대기열에는 접속 세션 객체에 대한 포인터를 보관한다.
// - 클라이언트 로그인 요청, 로그인 완료 처리, 로그아웃/끊김 처리는 각각
// 별도의 네트워크 워커 스레드에서 동시에 호출될 수 있다.
// - 대기 중인 클라이언트도 언제든 연결을 끊을 수 있고, 연결이 끊기면
// 상위 레이어가 해당 세션 객체를 정리(해제)한다.
//
// 요구사항:
// - 실제 접속 중인 인원(입장 처리된 세션 수)은 maxConcurrent 를 절대
// 넘어서는 안 된다.
// - 대기 중 이탈한 클라이언트의 세션에 이후 어떤 코드도 접근해서는 안 된다.
//
// 과제: (A)(B) 지점을 검토하라. 동시 요청이 몰릴 때, 그리고 대기 중
// 클라이언트가 이탈할 때 각각 어떤 문제가 생기는지 밝히고 수정하라.
#include <deque>
#include <mutex>
#include <atomic>
struct Session {
long long connId;
bool admitted = false;
};
class LoginGate {
int maxConcurrent_;
std::atomic<int> currentCount_{0};
std::mutex queueMx_;
std::deque<Session*> waitQueue_; // 대기 중인 세션에 대한 원시 포인터
public:
explicit LoginGate(int maxConcurrent) : maxConcurrent_(maxConcurrent) {}
// 클라이언트 로그인 요청 — 네트워크 워커 스레드에서 호출.
bool tryEnter(Session* session)
{
// (A) 읽기와 증가가 분리되어 있어(atomic 이라도 compare-and-swap 이 아니라
// load 후 별도로 증가) 여러 스레드가 동시에 통과할 수 있다.
if (currentCount_.load() < maxConcurrent_)
{
currentCount_.fetch_add(1);
session->admitted = true;
return true;
}
std::lock_guard<std::mutex> lk(queueMx_);
waitQueue_.push_back(session);
return false;
}
// 대기 중이던 클라이언트가 연결을 끊음 — 상위 레이어가 세션을 정리하기
// "직전"에 이 함수로 대기열에서 빠졌음을 알린다고 가정.
// 세션 객체 자체는 이 함수가 반환된 직후 다른 스레드에서 delete 될 수 있다.
void onCancelWaiting(Session* /*session*/)
{
// (B) 대기열(waitQueue_)에서 실제로 해당 포인터를 제거하지 않는다.
// 다음 자리가 비어 promoteNext 가 큐를 스캔할 때까지 포인터가
// 그대로 남아 있다.
}
// 접속 중이던 클라이언트가 로그아웃/끊김.
void onLeave(Session* /*session*/)
{
currentCount_.fetch_sub(1);
promoteNext();
}
private:
void promoteNext()
{
std::lock_guard<std::mutex> lk(queueMx_);
while (currentCount_.load() < maxConcurrent_ && !waitQueue_.empty())
{
Session* next = waitQueue_.front();
waitQueue_.pop_front();
// 이미 이탈해 해제됐을 수 있는 포인터를 그대로 역참조한다.
next->admitted = true;
currentCount_.fetch_add(1);
// 실제로는 여기서 next 에게 입장 허가 통지를 보낸다.
}
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.