7. ThreadLocal 버퍼 누수와 레지스트리 동시성

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 패킷 직렬화 경로다. 직렬화할 때마다 임시 버퍼가 필요한데,
// 매번 할당/해제하면 비싸므로 "스레드별 재사용 버퍼"를 두기로 했다.
//  - 각 스레드가 자기 전용 직렬화 버퍼(SerializeBuffer)를 ThreadLocal 로 보유.
//  - 동시에, 운영툴이 "현재 살아있는 버퍼들의 총 메모리"를 보고 싶어 해서,
//    버퍼가 생성될 때 전역 레지스트리에 자기 자신을 등록하도록 했다.
//
// 운영 중 증상:
//  - 단기 작업용 스레드를 많이 생성/종료하는 워크로드(예: 매치메이킹 잡,
//    임시 IO 스레드 풀 확장/축소)에서 시간이 갈수록 메모리가 단조 증가한다.
//  - 레지스트리에 등록된 버퍼 개수가 "현재 스레드 수"보다 훨씬 많아진다.
//  - GetTotalBytes() 가 실제보다 점점 과대하게 나온다.
//  - 가끔 레지스트리 집계 중 예외(컬렉션 변경/인덱스 오류)가 난다.
//
// 요구사항
// ----------------------------------------------------------------------------
//  - 각 스레드는 자기 전용 직렬화 버퍼를 재사용한다(할당 비용 절감).
//  - 스레드가 끝나면 그 버퍼의 메모리는 회수되고 레지스트리에서도 빠져야 한다.
//  - GetTotalBytes() 는 현재 실제로 살아있는 버퍼 총량을 반영해야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
//  1) 스레드를 반복 생성/종료하면 메모리/레지스트리가 어떻게 되는가? 왜?
//  2) (A)(B)(C) 각 지점의 결함을 설명하라.
//  3) 스레드 종료 시 정리되도록, 레지스트리 동시성까지 포함해 수정하라.
// ============================================================================

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

namespace SerializeBuffers
{
    // 살아있는 버퍼들을 추적하는 전역 레지스트리(싱글턴)
    public sealed class BufferRegistry
    {
        public static readonly BufferRegistry Instance = new BufferRegistry();

        // (C) 락 없이 일반 List 에 추가
        private readonly List<SerializeBuffer> _buffers = new();

        // (B) 등록만 있고, 해제(Deregister)는 어디에도 없다
        public void Register(SerializeBuffer b)
        {
            _buffers.Add(b);
        }

        public long GetTotalBytes()
        {
            long total = 0;
            foreach (var b in _buffers)
                total += b.Capacity;
            return total;
        }
    }

    public sealed class SerializeBuffer
    {
        private readonly byte[] _data;

        public SerializeBuffer()
        {
            _data = new byte[64 * 1024]; // 64KB 작업 버퍼
            BufferRegistry.Instance.Register(this); // 생성 시 등록
        }

        public int Capacity => _data.Length;
        public byte[] Data => _data;
    }

    public static class Serializer
    {
        // (A) 스레드별 재사용 버퍼. ThreadLocal 인데 Dispose 도, 해제도 없다.
        private static readonly ThreadLocal<SerializeBuffer> _tls =
            new ThreadLocal<SerializeBuffer>(() => new SerializeBuffer());

        // 직렬화 핫패스(개념용)
        public static void SerializePacket(int payloadByte)
        {
            var b = _tls.Value;
            b.Data[0] = (byte)payloadByte;
            // ... 실제 직렬화 생략 ...
        }
    }

    // 데모용 구동 코드
    public static class Demo
    {
        public static void Run()
        {
            // 단기 스레드를 반복 생성/종료하는 워크로드 흉내
            for (int round = 0; round < 5; round++)
            {
                var ts = new Thread[8];
                for (int i = 0; i < 8; i++)
                {
                    ts[i] = new Thread(() =>
                    {
                        for (int k = 0; k < 100; k++)
                            Serializer.SerializePacket(k);
                    });
                    ts[i].Start();
                }
                foreach (var t in ts) t.Join();

                Console.WriteLine($"round {round}: registry total = {BufferRegistry.Instance.GetTotalBytes()} bytes");
            }
            // 기대: 매 라운드 스레드가 끝나면 버퍼도 회수되어 total 이 0 근처로 돌아와야 함
            Console.WriteLine("done");
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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