27. 세션 타이머/지연 콜백의 수명 관리 결함 (C#)

난이도 상 해설 보기 →

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

결함 코드 · C#
// ─────────────────────────────────────────────────────────────
// 시나리오: 각 접속 세션에 주기 타이머를 건다.
//   - 유휴 점검 타이머: 일정 주기로 마지막 활동 시각을 보고 핑을 보내거나
//     유휴 종료를 결정한다(콜백이 세션의 소켓/상태를 참조한다).
//   - 세션은 정상 종료(로그아웃)·강제 종료(중복 로그인/제재)·연결 끊김 등
//     여러 경로로 종료되며, 종료되면 SessionManager에서 제거된다.
//   - 타이머 콜백은 ThreadPool 스레드에서 발화하므로 종료 처리와 병행될 수 있다.
//
// 요구사항:
//   - 세션이 종료되면 그 세션에 걸린 타이머도 더 이상 발화하지 않아야 한다.
//   - 종료된 세션의 소켓/상태에 접근하거나, 종료된 세션이 회수되지 못하고
//     계속 쌓이는 일이 없어야 한다.
//
// 과제: (A)(B) 지점을 검토하라. 세션이 Close된 뒤 타이머가 어떻게 동작하는지,
//        그로 인해 무엇이 잘못되는지(두 가지 이상) 설명하고 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
using System;
using System.Net.Sockets;
using System.Threading;

namespace Net
{
    public sealed class Session
    {
        private readonly Socket _socket;
        private readonly Timer _idleTimer;
        public long PlayerId { get; }
        public DateTime LastActivity;
        private volatile bool _closed;

        public Session(Socket socket, long playerId)
        {
            _socket = socket;
            PlayerId = playerId;
            LastActivity = DateTime.UtcNow;
            // (A) 유휴 점검 타이머 — 5초마다 OnIdleCheck 발화(콜백이 this를 참조)
            _idleTimer = new Timer(OnIdleCheck, null, 5000, 5000);
        }

        public void Touch() => LastActivity = DateTime.UtcNow;

        private void OnIdleCheck(object? state)
        {
            if ((DateTime.UtcNow - LastActivity).TotalSeconds > 30)
            {
                // 유휴로 판단 → 핑 전송(응답 없으면 다음 주기에 종료)
                var ping = new byte[] { 0x00 };
                _socket.Send(ping);
            }
        }

        public void Close()
        {
            _closed = true;
            _socket.Close();
            // (B) 세션 종료 처리: 매니저에서 제거
            SessionManager.Remove(PlayerId);
        }
    }

    public static class SessionManager
    {
        private static readonly System.Collections.Concurrent.ConcurrentDictionary<long, Session> _map = new();
        public static void Add(Session s) => _map[s.PlayerId] = s;
        public static void Remove(long id) => _map.TryRemove(id, out _);
        public static int Count => _map.Count;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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