8. ReaderWriterLockSlim 쓰기 기아 + 재진입 + 예외 누수

난이도 상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMO 서버의 전역 세션 레지스트리(SessionRegistry)다.
//  - 수십 개의 로직/네트워크 스레드가 "세션 조회"를 초당 수만 번 한다(읽기).
//  - 가끔(접속/해제) 세션을 추가/제거한다(쓰기). 읽기가 압도적으로 많다.
//  - "읽기 다중/쓰기 단독"이 맞다고 판단해 ReaderWriterLockSlim 을 도입.
//
// 운영 중 증상:
//  - 평시엔 빠른데, 부하가 높아 읽기 트래픽이 포화되면 "접속/해제 처리가
//    수 초씩 지연"되거나 사실상 멈춘다. 새 플레이어가 한참 로비에 묶인다.
//  - 동시에, 어떤 코드경로에서 같은 스레드가 락을 두 번 잡다 예외가 난다.
//  - 또 다른 경로에선 예외 발생 시 "락이 안 풀려" 이후 모든 접근이 영구 블록.
//
// 요구사항
// ----------------------------------------------------------------------------
//  - 읽기는 동시 다중 허용, 쓰기는 단독.
//  - 읽기 폭주 중에도 쓰기(접속/해제)가 "합리적 시간 안에" 처리돼야 한다.
//  - 예외가 나도 락은 반드시 해제돼야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
//  1) 쓰기가 왜 굶주리는가(starvation)? 어떤 정책 문제인가?
//  2) (A)(B)(C) 각 지점의 결함을 설명하라.
//  3) 기아·재진입·예외누수를 모두 해결하도록 수정하라.
// ============================================================================

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

namespace SessionReg
{
    public sealed class Session
    {
        public int Id;
        public string Name = "";
    }

    public sealed class SessionRegistry
    {
        // (A) 락 정책: 기본 생성자
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
        private readonly Dictionary<int, Session> _sessions = new();

        public Session Find(int id)
        {
            _lock.EnterReadLock();
            // (B) 읽는 동안 예외가 나면?
            _sessions.TryGetValue(id, out var s);
            // 검증/후처리에서 예외가 날 수 있는 호출
            Validate(s);
            _lock.ExitReadLock();
            return s;
        }

        public void Add(Session s)
        {
            _lock.EnterWriteLock();
            _sessions[s.Id] = s;
            _lock.ExitWriteLock();
        }

        public void Remove(int id)
        {
            _lock.EnterWriteLock();
            _sessions.Remove(id);
            _lock.ExitWriteLock();
        }

        // 어떤 세션이 특정 조건이면, 같은 호출 흐름 안에서 추가 조회를 한다
        public bool FindAndCheckParty(int id)
        {
            _lock.EnterReadLock();
            _sessions.TryGetValue(id, out var s);
            bool inParty = false;
            if (s != null)
            {
                // (C) 읽기 락을 잡은 채로 같은 객체의 다른 조회 메서드를 또 호출
                //     -> 그 메서드도 EnterReadLock 을 시도한다
                inParty = HasPartyMember(s.Id);
            }
            _lock.ExitReadLock();
            return inParty;
        }

        private bool HasPartyMember(int id)
        {
            _lock.EnterReadLock();   // (C) 재진입 시도
            bool exists = _sessions.ContainsKey(id);
            _lock.ExitReadLock();
            return exists;
        }

        private void Validate(Session s)
        {
            // 디버그 검증 등에서 가끔 throw
            if (s != null && s.Id < 0)
                throw new InvalidOperationException("bad session id");
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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