31. 여러 이벤트 경로에서 트리거되는 연결 정리의 중복 실행 (이중 정리)

난이도 중 해설 보기 →

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

결함 코드 · C#
// [시나리오] 클라이언트 세션 연결 정리(cleanup) (서버-클라)
// 세션은 다음 세 가지 서로 다른 이벤트 경로로 종료될 수 있다.
//   1) 소켓 수신/송신 중 오류 발생 (에러 콜백)
//   2) heartbeat 타임아웃 감시 타이머가 만료 감지 (타임아웃 콜백)
//   3) 클라이언트가 정상적으로 로그아웃 패킷을 보냄 (명시적 종료 요청)
// 이 세 경로는 서로 다른 스레드(수신 스레드, 타이머 스레드, 워커 스레드)에서
// 독립적으로 트리거될 수 있고, 셋 다 같은 세션의 "연결 정리" 루틴을 호출한다.
//
// [요구사항]
//   - 한 세션의 연결 정리(리소스 해제, DB 저장, 다른 시스템에 알림)는 정확히
//     한 번만 수행되어야 한다.
//   - 여러 경로가 거의 동시에 트리거되어도 위 요구사항이 지켜져야 한다.
//
// [과제] SessionManager.HandleError, HandleTimeout, HandleLogout 과 공통으로 호출되는
//        Cleanup 을 검토하고, 여러 경로가 동시에 트리거될 때 위 요구사항이 어떻게
//        어긋날 수 있는지 원인과 수정안을 제시하라. 표시된 (A)(B) 지점에 주목하라.

using System;
using System.Collections.Generic;

class PlayerSession
{
    public int SessionId;
    public bool IsClosed;
}

class SessionManager
{
    private readonly Dictionary<int, PlayerSession> _sessions = new();

    private void SaveToDb(PlayerSession s) { /* 최종 상태 DB 저장 */ }
    private void NotifyOtherSystems(PlayerSession s) { /* 길드/파티 등에 퇴장 통지 */ }
    private void ReleaseResources(PlayerSession s) { /* 버퍼/소켓 핸들 반납 */ }

    // 소켓 에러 콜백 스레드에서 호출
    public void HandleError(int sessionId)
    {
        if (_sessions.TryGetValue(sessionId, out var s))
            Cleanup(s);
    }

    // heartbeat 타이머 스레드에서 호출
    public void HandleTimeout(int sessionId)
    {
        if (_sessions.TryGetValue(sessionId, out var s))
            Cleanup(s);
    }

    // 로그아웃 패킷을 처리하는 워커 스레드에서 호출
    public void HandleLogout(int sessionId)
    {
        if (_sessions.TryGetValue(sessionId, out var s))
            Cleanup(s);
    }

    private void Cleanup(PlayerSession s)
    {
        // (A)
        if (s.IsClosed)
            return;

        SaveToDb(s);
        NotifyOtherSystems(s);
        ReleaseResources(s);

        // (B)
        s.IsClosed = true;
        _sessions.Remove(s.SessionId);
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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