안녕하세요! 매주 여러분의 성장을 돕기 위한 새로운 챌린지를 제공해 드릴 멘토입니다. 오늘은 그 첫 번째 시간으로, 실제 MMO 게임 서버 환경에서 마주할 수 있는 흥미로운 문제를 준비했습니다.
MMORPG 서버의 아이템 드랍 지연, C#으로 해결해 보세요. .NET 8의 Immutable 컬렉션과 비동기 Task를 활용하여 경합 상태(Race Condition) 없는 고성능, 스레드 안전(Thread-Safe) 루팅 시스템을 구현하는 1주차 챌린지입니다. 샘플 코드를 통해 실력을 업그레이드하세요.
상황 시나리오:
당신은 인기 MMORPG '아르카디아의 그림자' 서버 팀의 시니어 개발자입니다. 최근 대규모 레이드 업데이트 이후, 여러 플레이어가 동시에 보스 몬스터를 처치하고 아이템을 획득하는 과정에서 서버 지연 및 아이템 분배 오류가 발생하고 있다는 보고가 들어왔습니다. 원인 분석 결과, 기존 루팅 시스템이 다수의 동시 요청을 효율적으로 처리하지 못하고 있으며, 여러 스레드가 공유 자원(아이템 테이블)에 접근할 때 경합 상태(Race Condition)가 발생하는 것으로 확인되었습니다.
당신의 임무는 이 문제를 해결하기 위해, 현대적인 C# 기술을 사용하여 빠르고 안정적이며 확장 가능한 새로운 아이템 루팅 시스템을 설계하고 구현하는 것입니다.
다음은 이 문제를 해결하기 위한 샘플 코드입니다. 최신 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
는 게임 서버의 다양한 지점에서 활용될 수 있습니다:
OnDeath
이벤트 핸들러 내에서 LootGenerator.GenerateLootAsync()
를 호출합니다. 생성된 아이템 목록은 보스 몬스터를 공격한 플레이어들에게 공정하게 분배되거나, 가장 많은 피해를 준 파티/플레이어에게 귀속됩니다.LootGenerator
를 사용하여 결과의 예측 불가능성을 더할 수 있습니다.Immutable
컬렉션을 사용한 이유는 명확합니다. 게임 서버 데이터(아이템 정보, 몬스터 스펙 등)는 보통 서버 시작 시 로드되어 런타임 중에는 거의 변경되지 않습니다. 이런 '한 번 쓰고, 여러 번 읽는(Write-Once, Read-Many)' 패턴의 데이터를 Immutable
컬렉션으로 관리하면, lock
과 같은 동기화 메커니즘 없이도 여러 스레드가 안전하게 데이터에 접근할 수 있어 경합을 원천적으로 차단하고 성능을 크게 향상시킬 수 있습니다.
record struct
와 System.Collections.Immutable
을 사용하여 스레드 안전한 데이터 구조를 설계하는 방법을 학습합니다.Task.Run
을 사용하여 CPU 집약적 작업을 백그라운드 스레드로 오프로드하고, 서버의 응답성을 유지하는 방법을 익힙니다.lock
을 사용하지 않고도 다중 스레드 환경에서 안전하게 동작하는 코드를 작성하는 원리를 이해합니다.System.Random
대신 System.Security.Cryptography.RandomNumberGenerator
를 사용하여 보안 및 예측 불가능성이 중요한 시스템에서 더 높은 품질의 난수를 생성하는 방법을 배웁니다.lock
경합이 없는가?GenerateLootAsync
를 호출해도 데이터가 깨지거나 예외가 발생하지 않는가?핵심 요구사항을 완료하셨다면, 다음 목표에 도전해 보세요.
과제: '드랍 가중치 그룹' 시스템을 추가하여 LootGenerator
를 확장해 보세요.
요구사항:
LootTable
이 여러 개의 '드랍 그룹'을 가질 수 있도록 수정합니다.GenerateLootAsync
로직을 수정하고, 기존의 스레드 안전성과 성능을 유지해야 합니다.2.주간 C# 서버 챌린지: 스레드 안전한 경매장 구현 (0) | 2025.09.08 |
---|