3. 틱 루프의 GC 압박, LOH, 풀 이중 반납

난이도 상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 실시간 슈팅 게임 서버의 틱 루프(tick loop)다. 서버는 60Hz로 돈다.
// 매 틱마다:
//  - 접속한 플레이어들의 입력 패킷을 처리하고
//  - 각 룸의 상태 변화를 "스냅샷 메시지"로 만들어 브로드캐스트 큐에 넣는다.
//    (룸 전체 엔티티 상태를 직렬화한 풀(full) 스냅샷이라 페이로드가 크다.)
// 메시지 객체는 GC 부담을 줄이려고 ObjectPool 로 재활용하도록 설계했다.
//
// 운영 중 증상: 가동 몇 분 뒤부터 "틱 시간"이 들쭉날쭉(스파이크)하고,
// GC Gen2 / LOH 컬렉션이 자주 뜬다는 제보가 있다. 풀을 썼는데도 그렇다.
//
// 요구사항
// ----------------------------------------------------------------------------
//  - 틱 핫패스에서 힙 할당을 최소화한다(GC 압박 회피).
//  - 메시지 객체는 풀에서 빌려 쓰고 처리 후 반납한다.
//  - 처리량이 높아야 하며 GC 일시정지로 인한 틱 스파이크가 없어야 한다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
//  1) GC 압박/틱 스파이크의 원인이 되는 할당들을 모두 지적하라.
//  2) (A)(B)(C)(D) 각 지점의 결함을 설명하라.
//  3) 풀 사용 버그(반납/고갈)와 대용량 버퍼 문제를 수정하라.
// ============================================================================

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;

namespace TickLoop
{
    // 브로드캐스트할 스냅샷 메시지
    public sealed class EventMessage
    {
        public int RoomId;
        public int EventType;
        public byte[] Payload = new byte[96 * 1024]; // (D) 고정 페이로드 버퍼
        public int PayloadLen;

        public void Reset()
        {
            RoomId = 0;
            EventType = 0;
            PayloadLen = 0;
        }
    }

    public sealed class MessagePool
    {
        private readonly ConcurrentBag<EventMessage> _bag = new();

        public EventMessage Rent()
        {
            if (_bag.TryTake(out var m))
                return m;
            return new EventMessage(); // 비면 새로 생성
        }

        public void Return(EventMessage m)
        {
            m.Reset();
            _bag.Add(m);
        }
    }

    public sealed class GameServer
    {
        private readonly MessagePool _pool = new();
        private readonly ConcurrentQueue<EventMessage> _broadcastQueue = new();
        private readonly List<int> _activeRooms = new() { 1, 2, 3, 4, 5 };

        // 매 틱 호출 (60Hz)
        public void Tick(int tickNo)
        {
            foreach (var roomId in _activeRooms)
            {
                // (A) 매 틱, 매 룸마다 로그 문자열을 만든다
                string log = "tick " + tickNo + " room " + roomId + " processing";
                LogVerbose(log);

                var msg = _pool.Rent();
                msg.RoomId = roomId;
                msg.EventType = 7;
                msg.PayloadLen = FillPayload(msg.Payload, roomId, tickNo);

                _broadcastQueue.Enqueue(msg);

                // (B) 통계 수집: object[] 에 담아 집계기로 넘김
                RecordStat("room_event", roomId, tickNo);
            }

            // 브로드캐스트 처리
            DrainBroadcast();
        }

        private void DrainBroadcast()
        {
            while (_broadcastQueue.TryDequeue(out var msg))
            {
                Broadcast(msg);
                _pool.Return(msg);
                // (C)
                _pool.Return(msg);
            }
        }

        private int FillPayload(byte[] buf, int roomId, int tickNo)
        {
            // 대충 몇 바이트만 채운다
            buf[0] = (byte)roomId;
            buf[1] = (byte)tickNo;
            return 2;
        }

        private void Broadcast(EventMessage msg)
        {
            // 실제로는 소켓 전송. 여기선 생략.
        }

        // 통계: 키/값을 object 박스로 받아 집계
        private static readonly Dictionary<string, long> s_stats = new();
        private void RecordStat(string name, params object[] args)
        {
            // (B 계속)
            lock (s_stats)
            {
                long sum = 0;
                foreach (var a in args) sum += Convert.ToInt64(a);
                s_stats.TryGetValue(name, out var prev);
                s_stats[name] = prev + sum;
            }
        }

        private void LogVerbose(string s)
        {
            // 운영에선 보통 꺼져 있지만 호출 자체는 매 틱 일어남
            if (VerboseEnabled)
                Console.WriteLine(s);
        }

        public bool VerboseEnabled = false;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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