1. Zone 통계 수집기의 데이터 레이스 (lost update / torn read)

난이도 하 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// MMORPG 필드 서버의 "구역(Zone)" 통계 수집기다.
// 한 Zone 안에는 수백 개의 몬스터 AI가 돌아가고, 각 AI는 자신을 처리하는
// 워커 스레드에서 동작한다. 워커 스레드들은 전투가 일어날 때마다
// ZoneMetrics 에 "가한 피해량(damage)"과 "처치 수(kill)"를 누적한다.
// 운영툴은 1초마다 별도 스레드에서 Snapshot() 을 읽어 대시보드에 표시한다.
//
// 요구사항
// ----------------------------------------------------------------------------
//  - 여러 워커 스레드가 동시에 AddDamage / AddKill 을 호출한다.
//  - 운영 스레드는 동시에 Snapshot() 으로 현재 누적값을 읽는다.
//  - 통계는 "유실 없이" 정확히 누적되어야 한다 (전투 로그와 대사해 검증함).
//  - 핫패스이므로 가능하면 가볍게(락 최소화) 유지하고 싶다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
//  1) 멀티스레드 환경에서 어떤 결과 오류/이상이 발생하는가? 왜 발생하는가?
//  2) (A)(B)(C) 각 지점에서 무엇이 문제인지 설명하라.
//  3) 정확성을 보장하면서 핫패스 오버헤드를 최소화하도록 수정하라.
// ============================================================================

using System;
using System.Threading;

namespace ZoneStats
{
    public sealed class ZoneMetrics
    {
        // 누적 통계
        private long _totalDamage;
        private int _killCount;

        // 최근에 가장 큰 단일 피해를 기록(크리티컬 추적용)
        private long _maxSingleHit;

        // 통계가 한 번이라도 갱신되면 true. 운영툴이 "데이터 들어오기 시작했는지" 폴링.
        private bool _hasData;

        public void AddDamage(long amount)
        {
            // (A) 다수의 워커 스레드가 동시에 진입한다.
            _totalDamage += amount;

            // (B) 단일 최대 피해 갱신
            if (amount > _maxSingleHit)
                _maxSingleHit = amount;

            _hasData = true;
        }

        public void AddKill()
        {
            _killCount++;
        }

        // 운영 스레드가 1초마다 호출
        public (long damage, int kills, long maxHit) Snapshot()
        {
            // (C) 락 없이 세 필드를 따로따로 읽어 묶어서 반환
            return (_totalDamage, _killCount, _maxSingleHit);
        }

        public bool HasData => _hasData;
    }

    // 데모용 구동 코드
    public static class Demo
    {
        public static void Run()
        {
            var metrics = new ZoneMetrics();
            const int workers = 8;
            const int hitsPerWorker = 100_000;

            var threads = new Thread[workers];
            for (int t = 0; t < workers; t++)
            {
                threads[t] = new Thread(() =>
                {
                    var rnd = new Random();
                    for (int i = 0; i < hitsPerWorker; i++)
                    {
                        metrics.AddDamage(rnd.Next(1, 1000));
                        if (i % 50 == 0) metrics.AddKill();
                    }
                });
                threads[t].Start();
            }

            // 운영 스레드 흉내
            var monitor = new Thread(() =>
            {
                while (!metrics.HasData) Thread.Sleep(1);
                for (int i = 0; i < 20; i++)
                {
                    var s = metrics.Snapshot();
                    Console.WriteLine($"dmg={s.damage} kills={s.kills} max={s.maxHit}");
                    Thread.Sleep(10);
                }
            });
            monitor.Start();

            foreach (var th in threads) th.Join();
            monitor.Join();

            var final = metrics.Snapshot();
            // 기대값: workers * hitsPerWorker 번의 AddDamage, kill = workers * (hitsPerWorker/50)
            Console.WriteLine($"[FINAL] damage={final.damage}, kills={final.kills}");
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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