상세 컨텐츠

본문 제목

1.주간 C# 서버 챌린지: 스레드 안전(Thread-Safe) 아이템 루팅 시스템 구현

개발/C# 서버 챌린지

by Jiung. 2025. 9. 6. 01:05

본문

주간 C# 서버 챌린지: 고성능, 스레드 안전 아이템 루팅 시스템 구현

안녕하세요! 매주 여러분의 성장을 돕기 위한 새로운 챌린지를 제공해 드릴 멘토입니다. 오늘은 그 첫 번째 시간으로, 실제 MMO 게임 서버 환경에서 마주할 수 있는 흥미로운 문제를 준비했습니다.

 

MMORPG 서버의 아이템 드랍 지연, C#으로 해결해 보세요. .NET 8의 Immutable 컬렉션과 비동기 Task를 활용하여 경합 상태(Race Condition) 없는 고성능, 스레드 안전(Thread-Safe) 루팅 시스템을 구현하는 1주차 챌린지입니다. 샘플 코드를 통해 실력을 업그레이드하세요.

 


문제: 고성능, 스레드 안전(Thread-Safe) 아이템 루팅 시스템 구현

상황 시나리오:

당신은 인기 MMORPG '아르카디아의 그림자' 서버 팀의 시니어 개발자입니다. 최근 대규모 레이드 업데이트 이후, 여러 플레이어가 동시에 보스 몬스터를 처치하고 아이템을 획득하는 과정에서 서버 지연 및 아이템 분배 오류가 발생하고 있다는 보고가 들어왔습니다. 원인 분석 결과, 기존 루팅 시스템이 다수의 동시 요청을 효율적으로 처리하지 못하고 있으며, 여러 스레드가 공유 자원(아이템 테이블)에 접근할 때 경합 상태(Race Condition)가 발생하는 것으로 확인되었습니다.

당신의 임무는 이 문제를 해결하기 위해, 현대적인 C# 기술을 사용하여 빠르고 안정적이며 확장 가능한 새로운 아이템 루팅 시스템을 설계하고 구현하는 것입니다.


C# 12 / .NET 8 기반 스레드 안전 솔루션 코드

다음은 이 문제를 해결하기 위한 샘플 코드입니다. 최신 C# 기능과 비동기 처리, 그리고 스레드 안전성을 중점적으로 다루었습니다.

// System.Collections.Immutable를 사용하기 위해 NuGet 패키지 참조가 필요할 수 있습니다.
// dotnet add package System.Collections.Immutable
using System.Collections.Immutable;
using System.Security.Cryptography;

/// <summary>
/// 개별 아이템의 데이터를 정의하는 레코드 구조체입니다.
/// 불변성(Immutability)을 보장하여 스레드 간에 안전하게 공유할 수 있습니다.
/// </summary>
public record struct Item(int Id, string Name, double DropChance);

/// <summary>
/// 몬스터가 드랍할 수 있는 아이템 목록과 규칙을 관리하는 클래스입니다.
/// 이 클래스의 인스턴스는 불변(Immutable)으로 설계되어 초기화 후에는 변경되지 않습니다.
/// </summary>
public sealed class LootTable
{
    // 아이템 목록을 ImmutableArray로 선언하여, 한 번 생성되면 절대로 변경되지 않음을 보장합니다.
    // 이는 다중 스레드 환경에서 '읽기' 작업 시 락(lock) 없이도 안전하게 접근할 수 있게 해줍니다.
    public ImmutableArray<Item> Items { get; }

    // LootTable 생성자입니다. 아이템 목록을 받아 내부의 불변 컬렉션에 저장합니다.
    public LootTable(IEnumerable<Item> items)
    {
        // 제공된 아이템 목록으로부터 불변 배열(ImmutableArray)을 생성합니다.
        // ToImmutableArray()는 방어적 복사(defensive copy)를 수행하므로, 원본 컬렉션이 변경되어도 이 인스턴스에는 영향을 주지 않습니다.
        Items = items.ToImmutableArray();
    }
}

/// <summary>
/// 아이템 드랍 로직을 처리하는 핵심 서비스 클래스입니다.
/// 스레드로부터 안전하게 설계되어 여러 요청을 동시에 처리할 수 있습니다.
/// </summary>
public sealed class LootGenerator
{
    // 특정 몬스터 ID에 해당하는 LootTable을 매핑하는 불변 사전(ImmutableDictionary)입니다.
    // 서버 시작 시 한 번 초기화되며, 런타임 중에는 읽기 전용으로만 사용되어 스레드 안전성을 확보합니다.
    private readonly ImmutableDictionary<int, LootTable> _lootTables;

    // LootGenerator 생성자입니다. 몬스터 ID와 LootTable의 매핑 정보를 받습니다.
    public LootGenerator(IReadOnlyDictionary<int, LootTable> lootTables)
    {
        // 제공된 사전으로부터 불변 사전(ImmutableDictionary)을 생성합니다.
        _lootTables = lootTables.ToImmutableDictionary();
    }

    /// <summary>
    /// 지정된 몬스터에 대한 아이템 드랍을 비동기적으로 계산합니다.
    /// 스레드 풀의 작업 스레드에서 실행되도록 설계되어, 메인 스레드의 부하를 줄여줍니다.
    /// </summary>
    /// <param name="monsterId">아이템을 드랍할 몬스터의 ID입니다.</param>
    /// <param name="cancellationToken">비동기 작업 취소를 위한 토큰입니다.</param>
    /// <returns>드랍된 아이템 목록을 포함하는 Task<List<Item>>을 반환합니다.</returns>
    public Task<List<Item>> GenerateLootAsync(int monsterId, CancellationToken cancellationToken = default)
    {
        // Task.Run을 사용하여 CPU 집약적인 루팅 계산 로직을 백그라운드 스레드로 오프로드합니다.
        // 이는 게임 서버의 메인 루프가 블로킹되는 것을 방지하여 서버 전체의 반응성을 유지합니다.
        return Task.Run(() =>
        {
            // CancellationToken을 확인하여 작업 취소 요청이 있었는지 주기적으로 체크합니다.
            cancellationToken.ThrowIfCancellationRequested();

            // 몬스터 ID로 LootTable을 찾습니다. 없으면 예외를 발생시키는 대신 빈 리스트를 반환하는 것이 더 안정적일 수 있습니다.
            if (!_lootTables.TryGetValue(monsterId, out var lootTable))
            {
                // 해당 몬스터에 대한 LootTable이 없으면 빈 리스트를 즉시 반환합니다.
                return new List<Item>();
            }

            var droppedItems = new List<Item>();
            foreach (var item in lootTable.Items)
            {
                // CancellationToken을 루프 내에서도 확인하여 더 빠른 응답성을 제공합니다.
                cancellationToken.ThrowIfCancellationRequested();

                // 암호학적으로 안전한 난수 생성기를 사용하여 각 아이템의 드랍 여부를 결정합니다.
                // RandomNumberGenerator.GetDouble()은 0.0에서 1.0 사이의 균일 분포된 난수를 생성합니다.
                if (RandomNumberGenerator.GetDouble() < item.DropChance)
                {
                    droppedItems.Add(item);
                }
            }

            return droppedItems;
        }, cancellationToken);
    }
}

실제 게임 서버 환경에서의 활용 시나리오

LootGenerator는 게임 서버의 다양한 지점에서 활용될 수 있습니다:

  1. 몬스터 사망 처리 로직: 몬스터의 OnDeath 이벤트 핸들러 내에서 LootGenerator.GenerateLootAsync()를 호출합니다. 생성된 아이템 목록은 보스 몬스터를 공격한 플레이어들에게 공정하게 분배되거나, 가장 많은 피해를 준 파티/플레이어에게 귀속됩니다.
  2. 보물 상자 열기: 플레이어가 월드에 배치된 보물 상자와 상호작용할 때, 이 시스템을 호출하여 상자 안의 아이템을 동적으로 생성할 수 있습니다.
  3. 퀘스트 보상 생성: 퀘스트 완료 시, 정해진 보상 외에 추가적인 랜덤 보상을 지급할 때 LootGenerator를 사용하여 결과의 예측 불가능성을 더할 수 있습니다.

Immutable 컬렉션을 사용한 이유는 명확합니다. 게임 서버 데이터(아이템 정보, 몬스터 스펙 등)는 보통 서버 시작 시 로드되어 런타임 중에는 거의 변경되지 않습니다. 이런 '한 번 쓰고, 여러 번 읽는(Write-Once, Read-Many)' 패턴의 데이터를 Immutable 컬렉션으로 관리하면, lock과 같은 동기화 메커니즘 없이도 여러 스레드가 안전하게 데이터에 접근할 수 있어 경합을 원천적으로 차단하고 성능을 크게 향상시킬 수 있습니다.


왜 Immutable 컬렉션을 사용해야 하는가? (성능과 안전성)

  • 불변성(Immutability)의 이해: record structSystem.Collections.Immutable을 사용하여 스레드 안전한 데이터 구조를 설계하는 방법을 학습합니다.
  • 비동기 프로그래밍: Task.Run을 사용하여 CPU 집약적 작업을 백그라운드 스레드로 오프로드하고, 서버의 응답성을 유지하는 방법을 익힙니다.
  • 스레드 안전성: lock을 사용하지 않고도 다중 스레드 환경에서 안전하게 동작하는 코드를 작성하는 원리를 이해합니다.
  • 암호학적 난수 생성: System.Random 대신 System.Security.Cryptography.RandomNumberGenerator를 사용하여 보안 및 예측 불가능성이 중요한 시스템에서 더 높은 품질의 난수를 생성하는 방법을 배웁니다.

이번 챌린지의 핵심 학습 목표 4가지

  • 성능: 대량의 루팅 요청을 동시에 처리할 때의 지연 시간과 처리량. lock 경합이 없는가?
  • 스레드 안전성: 여러 스레드에서 동시에 GenerateLootAsync를 호출해도 데이터가 깨지거나 예외가 발생하지 않는가?
  • 유지보수성: 코드의 가독성이 높고, 새로운 아이템이나 몬스터를 추가하는 것이 용이한가? 로직의 각 부분이 명확하게 분리되어 있는가?
  • 확장성: 향후 더 복잡한 루팅 규칙(예: 특정 직업 전용 아이템, 특정 조건 만족 시 드랍률 증가)을 추가하기 용이한 구조인가?

보너스 챌린지: 드랍 가중치 그룹 시스템 확장

핵심 요구사항을 완료하셨다면, 다음 목표에 도전해 보세요.

과제: '드랍 가중치 그룹' 시스템을 추가하여 LootGenerator를 확장해 보세요.

요구사항:

  1. 하나의 LootTable이 여러 개의 '드랍 그룹'을 가질 수 있도록 수정합니다.
  2. 각 그룹에는 여러 아이템이 포함될 수 있으며, 그룹별로 "이 그룹에서 단 하나의 아이템만 드랍" 또는 "이 그룹에서 최대 N개의 아이템 드랍"과 같은 규칙을 설정할 수 있어야 합니다.
  3. 예를 들어, '전설 무기 그룹'에서는 단 하나의 무기만 1% 확률로 드랍되고, '소모품 그룹'에서는 최대 3개의 포션이 각각의 드랍률에 따라 드랍될 수 있습니다.
  4. 이 새로운 규칙을 반영하여 GenerateLootAsync 로직을 수정하고, 기존의 스레드 안전성과 성능을 유지해야 합니다.

관련글 더보기