3. C# 중복 접속 / 동시 로그인 처리 (기존 세션 킥)

난이도 상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 중복 접속 / 동시 로그인 처리 (기존 세션 킥)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 한 계정(accountId)당 동시에 하나의 세션만 허용한다.
//   - 같은 계정으로 새 로그인이 들어오면 기존 세션을 강제 종료(킥)하고
//     새 세션을 등록한다("나중 로그인 우선" 정책).
//   - 로그인 요청은 여러 IO 스레드에서 동시에 들어온다. 같은 계정으로
//     두 디바이스가 거의 동시에 접속할 수 있다(멀티 디바이스, 재접속 폭주).
//
// 요구사항:
//   - 어떤 race 에서도 계정당 최종적으로 정확히 하나의 활성 세션만 남아야 한다.
//   - "둘 다 살아남음"(중복) 도, "둘 다 끊김"(서비스 거부) 도 발생하면 안 된다.
//   - 킥당한 세션에는 KickNotice 를 보내고 깨끗이 정리해야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public class Session
{
    public long AccountId;
    public string SessionKey = Guid.NewGuid().ToString();
    public volatile bool Alive = true;

    public Task SendKickNoticeAsync() => Task.CompletedTask; // 킥 통지 전송(생략)
    public void Disconnect()
    {
        Alive = false;
        // 소켓 종료 등 ...
    }
}

public class LoginManager
{
    // accountId -> 현재 활성 세션
    private readonly Dictionary<long, Session> _online = new Dictionary<long, Session>();
    private readonly object _lock = new object();

    // 여러 IO 스레드에서 동시 호출됨
    public async Task<Session> LoginAsync(long accountId)
    {
        // 1) DB 인증 (네트워크 I/O, 수십 ms 소요)
        await AuthenticateAsync(accountId);

        var newSession = new Session { AccountId = accountId };

        // 2) 기존 세션이 있으면 킥
        Session old = null;
        lock (_lock)
        {
            _online.TryGetValue(accountId, out old);   // (A)
        }

        if (old != null)
        {
            await old.SendKickNoticeAsync();   // (B) 락 밖에서 킥 통지 + await
            old.Disconnect();
        }

        // 3) 새 세션 등록
        lock (_lock)
        {
            _online[accountId] = newSession;   // (C)
        }

        return newSession;
    }

    // 세션이 스스로 끊길 때(클라가 정상 종료) 호출
    public void OnSessionClosed(Session s)
    {
        lock (_lock)
        {
            _online.Remove(s.AccountId);   // (D)
        }
    }

    private Task AuthenticateAsync(long accountId) => Task.Delay(20);
}
내 리뷰 · C#
내 답안 · 자동 저장

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