C# 내부 메커니즘: async/await에서 LINQ까지

REIN·5일 전
0

게임 개발 CS

목록 보기
5/20

들어가며

C#은 표현력성능 사이의 균형이다. Unity 클라이언트는 60fps(16.67ms/프레임)를 유지해야 하고, 게임 서버는 수만 명의 동시 접속자를 처리해야 한다. C#은 이 양극단에서 모두 사용된다. async/await는 게임 서버의 I/O를 논블로킹으로 처리하고, LINQ는 게임 오브젝트 쿼리를 간결하게 만든다. 리플렉션은 Component 시스템의 기반이고, Span은 제로 할당 최적화를 가능하게 한다.

C#의 진화는 세 방향으로 진행되었다. 언어 기능의 확장(제네릭 → LINQ → async/await → 패턴 매칭)은 표현력을 높였다. 타입 시스템의 강화(nullable 참조 타입, record, init-only)는 안전성을 보장한다. 성능 최적화(ref struct, Span, ValueTask)는 제로 비용 추상화를 추구한다. 결과적으로 C#은 "높은 수준의 추상화와 낮은 수준의 제어를 동시에" 제공하는 드문 언어가 되었다.

게임 개발에서도 C#은 세 계층으로 사용된다. Unity 클라이언트는 Mono/IL2CPP 위에서 동작하며 GC 스파이크를 피해야 하기에 성능 최적화가 핵심이다. 게임 서버는 .NET Core 위에서 동작하며 언어 기능을 활용해 동시성을 처리한다. 게임 툴은 WPF/WinForms로 만들어지며 타입 안정성으로 생산성을 높인다. 각 계층마다 C#의 다른 측면이 중요하다.

핵심은 컴파일러가 코드를 변환한다는 것이다. async/await는 상태 머신으로, LINQ는 Iterator로, 람다는 클로저 클래스로 변환된다. 이 변환을 이해하면 성능 특성을 예측할 수 있고, Unity의 제약을 우회할 수 있으며, 게임 서버를 최적화할 수 있다.


1. 비동기 프로그래밍의 본질

왜 이것이 중요한가?

동기 코드는 블로킹으로 스레드를 낭비한다. 1000개 요청 처리에 1000개 스레드가 필요하다. async/await는 스레드 없이 대기하므로 10개 스레드로 10만 개 요청을 처리한다. 이는 I/O 바운드 작업에서 100배 효율 향상을 의미한다.

1.1 동기 vs 비동기의 근본적 차이

// 게임 서버: 플레이어 데이터 로드

// 동기: 스레드가 블로킹
PlayerData SyncLoadPlayer(int playerId) {
    var dbConnection = GetConnection();
    var result = dbConnection.Query("SELECT * FROM Players WHERE Id = @id", 
        new { id = playerId }); // DB I/O 대기 중 블로킹!
    return ParsePlayerData(result);
}

// 호출 시:
// Thread-1: [실행] → [DB 대기(블로킹 50ms)] → [실행]
// 50ms 동안 스레드가 낭비됨

// 비동기: 스레드 반환
async Task<PlayerData> AsyncLoadPlayer(int playerId) {
    var dbConnection = GetConnection();
    var result = await dbConnection.QueryAsync(
        "SELECT * FROM Players WHERE Id = @id", 
        new { id = playerId }); // 스레드 반환!
    return ParsePlayerData(result);
}

// 호출 시:
// Thread-1: [실행] → await (스레드 풀 반환) → [DB 완료 후 다른 스레드에서 재개]
// 대기 중 Thread-1은 다른 플레이어 요청 처리 가능

게임 서버 스레드 효율성:

// 동기: 1000명 동시 접속 = 1000개 스레드
for (int i = 0; i < 1000; i++) {
    Task.Run(() => {
        var player = SyncLoadPlayer(i);
        ProcessLogin(player);
        // 각 스레드가 DB I/O에서 블로킹
    });
}
// 메모리: 1000 * 1MB = 1GB
// 컨텍스트 스위칭: 매우 높음
// 처리량: 낮음

// 비동기: 1000명 동시 접속 = 10개 스레드
var tasks = new List<Task>();
for (int i = 0; i < 1000; i++) {
    int playerId = i;
    tasks.Add(Task.Run(async () => {
        var player = await AsyncLoadPlayer(playerId);
        ProcessLogin(player);
    }));
}
await Task.WhenAll(tasks);
// 메모리: 10 * 1MB = 10MB (100배 절약)
// 처리량: 10배 이상 향상

Unity 클라이언트: 에셋 로딩

// 동기: 메인 스레드 블로킹 (프레임 드롭!)
void LoadAssetSync() {
    Texture texture = Resources.Load<Texture>("HugeTexture"); // 50ms 블로킹
    material.mainTexture = texture;
    // 50ms = 3프레임 손실! (60fps 기준)
}

// 비동기: 메인 스레드 논블로킹
async Task LoadAssetAsync() {
    var request = Resources.LoadAsync<Texture>("HugeTexture");
    await request; // 메인 스레드 반환, 다른 Update 계속 실행
    material.mainTexture = request.asset as Texture;
    // 프레임 드롭 없음!
}

1.2 async/await의 내부 메커니즘

// 원본 코드
async Task<int> CalculateAsync() {
    Console.WriteLine("Start");
    await Task.Delay(1000);
    Console.WriteLine("Middle");
    await Task.Delay(1000);
    Console.WriteLine("End");
    return 42;
}

// 컴파일러가 생성하는 코드 (개념적)
Task<int> CalculateAsync() {
    var stateMachine = new CalculateAsyncStateMachine();
    stateMachine.builder = AsyncTaskMethodBuilder<int>.Create();
    stateMachine.state = -1;
    stateMachine.builder.Start(ref stateMachine);
    return stateMachine.builder.Task;
}

// 상태 머신
struct CalculateAsyncStateMachine : IAsyncStateMachine {
    public int state;
    public AsyncTaskMethodBuilder<int> builder;
    private TaskAwaiter awaiter1;
    private TaskAwaiter awaiter2;
    
    public void MoveNext() {
        int result = 0;
        try {
            switch (state) {
                case -1: // 초기 상태
                    Console.WriteLine("Start");
                    awaiter1 = Task.Delay(1000).GetAwaiter();
                    if (!awaiter1.IsCompleted) {
                        state = 0;
                        builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this);
                        return; // 스레드 반환!
                    }
                    goto case 0;
                    
                case 0: // 첫 await 완료 후
                    awaiter1.GetResult();
                    Console.WriteLine("Middle");
                    awaiter2 = Task.Delay(1000).GetAwaiter();
                    if (!awaiter2.IsCompleted) {
                        state = 1;
                        builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
                        return;
                    }
                    goto case 1;
                    
                case 1: // 두 번째 await 완료 후
                    awaiter2.GetResult();
                    Console.WriteLine("End");
                    result = 42;
                    state = -2;
                    builder.SetResult(result);
                    return;
            }
        } catch (Exception ex) {
            state = -2;
            builder.SetException(ex);
        }
    }
    
    public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}

핵심 개념:

  1. 상태 머신: 각 await는 상태 전환점
  2. Continuation: await 후 코드는 콜백으로 등록
  3. SynchronizationContext: 완료 후 어느 스레드에서 재개할지 결정
// await의 실제 동작
TaskAwaiter awaiter = task.GetAwaiter();
if (awaiter.IsCompleted) {
    // 이미 완료: 동기 실행
    var result = awaiter.GetResult();
} else {
    // 미완료: 콜백 등록 후 반환
    awaiter.OnCompleted(() => {
        // Continuation: 나중에 실행
        var result = awaiter.GetResult();
    });
    return; // 현재 스레드 해제
}

1.3 ConfigureAwait의 중요성

// UI 스레드 (WPF/WinForms)
async void Button_Click(object sender, EventArgs e) {
    // UI 스레드에서 시작
    label.Text = "Loading...";
    
    // await: 기본적으로 원래 컨텍스트로 복귀
    string data = await DownloadAsync("http://api.com");
    
    // UI 스레드에서 재개 (SynchronizationContext 덕분)
    label.Text = data; // UI 접근 가능
}

// 문제: 불필요한 컨텍스트 복귀
async Task<string> DownloadAsync(string url) {
    using var client = new HttpClient();
    
    // 기본: UI 스레드로 복귀
    string result = await client.GetStringAsync(url);
    
    // result를 가공 (UI 필요 없음)
    return result.ToUpper();
}

// 해결: ConfigureAwait(false)
async Task<string> DownloadOptimizedAsync(string url) {
    using var client = new HttpClient();
    
    // 스레드 풀에서 계속 실행
    string result = await client.GetStringAsync(url)
        .ConfigureAwait(false);
    
    // 스레드 풀 스레드에서 실행 (빠름)
    return result.ToUpper();
}

// 원칙:
// - 라이브러리 코드: 항상 ConfigureAwait(false)
// - UI 코드: ConfigureAwait(false) 사용 금지 (UI 접근 필요)

성능 차이:

// ASP.NET Core (SynchronizationContext 없음)
// ConfigureAwait는 불필요하지만 습관으로 사용

// 성능 벤치마크
async Task WithoutConfigureAwait() {
    for (int i = 0; i < 1000; i++) {
        await Task.Delay(1); // 기본
    }
}

async Task WithConfigureAwait() {
    for (int i = 0; i < 1000; i++) {
        await Task.Delay(1).ConfigureAwait(false);
    }
}

// 결과 (UI 앱):
// WithoutConfigureAwait: 3000ms (UI 스레드 대기열)
// WithConfigureAwait: 1500ms (스레드 풀 직접 실행)

// ASP.NET Core: 차이 거의 없음

1.4 ValueTask와 성능 최적화

// 게임 서버: 플레이어 데이터 캐시

// Task: 항상 힙 할당
async Task<PlayerData> GetPlayerAsync(int playerId) {
    if (cache.TryGetValue(playerId, out var cached)) {
        return cached; // Task<PlayerData> 할당!
    }
    return await LoadFromDatabaseAsync(playerId);
}

// ValueTask: 캐시 히트 시 할당 없음
async ValueTask<PlayerData> GetPlayerOptimizedAsync(int playerId) {
    if (cache.TryGetValue(playerId, out var cached)) {
        return cached; // 할당 없음!
    }
    return await LoadFromDatabaseAsync(playerId);
}

// 성능 비교 (90% 캐시 히트율)
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1_000_000; i++) {
    await GetPlayerAsync(i % 100); // 90% 캐시
}
Console.WriteLine($"Task: {sw.ElapsedMilliseconds}ms");
// 출력: ~500ms + GC

sw.Restart();
for (int i = 0; i < 1_000_000; i++) {
    await GetPlayerOptimizedAsync(i % 100);
}
Console.WriteLine($"ValueTask: {sw.ElapsedMilliseconds}ms");
// 출력: ~50ms, GC 거의 없음

// 10배 빠름!

Unity: 에셋 캐시

class AssetCache {
    private Dictionary<string, Texture> cache = new();
    
    // ValueTask로 캐시 최적화
    public async ValueTask<Texture> LoadTextureAsync(string path) {
        if (cache.TryGetValue(path, out var texture)) {
            return texture; // 동기 반환, 할당 없음
        }
        
        var request = Resources.LoadAsync<Texture>(path);
        await request;
        
        texture = request.asset as Texture;
        cache[path] = texture;
        return texture;
    }
}

// 사용
var assetCache = new AssetCache();

// 첫 로드: 비동기
var tex1 = await assetCache.LoadTextureAsync("Hero"); // 할당

// 재사용: 동기 + 할당 없음
var tex2 = await assetCache.LoadTextureAsync("Hero"); // 할당 없음!

ValueTask 규칙:

// 올바른 사용
async ValueTask<int> CorrectUsage() {
    ValueTask<int> vt = GetValueOptimizedAsync();
    return await vt; // 한 번만 await
}

// 잘못된 사용
async Task IncorrectUsage() {
    ValueTask<int> vt = GetValueOptimizedAsync();
    int result1 = await vt;
    int result2 = await vt; // 두 번 await: Undefined Behavior!
}

// ValueTask는 일회용
// Task로 변환 가능
ValueTask<int> vt = GetValueOptimizedAsync();
Task<int> task = vt.AsTask(); // 재사용 가능 (할당 발생)

1.5 취소와 타임아웃

// CancellationToken
async Task LongRunningOperationAsync(CancellationToken ct) {
    for (int i = 0; i < 100; i++) {
        ct.ThrowIfCancellationRequested(); // 취소 체크
        
        await Task.Delay(100, ct); // 취소 가능한 대기
        ProcessChunk(i);
    }
}

// 사용
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5)); // 5초 후 취소

try {
    await LongRunningOperationAsync(cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Operation cancelled");
}

// 타임아웃
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
    await DownloadAsync(url, cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Timeout");
}

// 여러 토큰 결합
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    cts1.Token, cts2.Token
);
// 둘 중 하나라도 취소되면 linkedCts도 취소

비동기 타임아웃 패턴:

// Task.WhenAny를 이용한 타임아웃
async Task<string> DownloadWithTimeoutAsync(string url, int seconds) {
    var downloadTask = DownloadAsync(url);
    var timeoutTask = Task.Delay(TimeSpan.FromSeconds(seconds));
    
    var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
    
    if (completedTask == timeoutTask) {
        throw new TimeoutException();
    }
    
    return await downloadTask;
}

// C# 10+: WaitAsync
async Task<string> DownloadWithTimeoutModernAsync(string url, int seconds) {
    using var cts = new CancellationTokenSource();
    var timeout = TimeSpan.FromSeconds(seconds);
    
    return await DownloadAsync(url, cts.Token)
        .WaitAsync(timeout); // 간결!
}

2. LINQ: 언어 통합 질의

왜 이것이 중요한가?

LINQ는 데이터 질의를 퍼스트 클래스 언어 기능으로 만든다. 컬렉션, 데이터베이스, XML을 동일한 구문으로 질의한다. 지연 실행은 메모리 효율을 극대화하고, Expression Tree는 코드를 데이터로 변환해 최적화를 가능하게 한다.

2.1 LINQ의 구조와 지연 실행

// Unity/게임 서버: 적 필터링

List<Enemy> enemies = GetAllEnemies();

// 질의 구문 (Query Syntax)
var query = from enemy in enemies
            where enemy.Health > 0 && enemy.Distance < 10f
            orderby enemy.Threat descending
            select enemy;

// 메서드 구문 (Method Syntax) - 동일
var query = enemies
    .Where(e => e.Health > 0 && e.Distance < 10f)
    .OrderByDescending(e => e.Threat)
    .Select(e => e);

// 핵심: 지연 실행 (Deferred Execution)
var aliveEnemies = enemies.Where(e => {
    Console.WriteLine($"Checking {e.Name}");
    return e.Health > 0;
});

Console.WriteLine("Query created");
// 출력: Query created (필터링 안 됨!)

foreach (var enemy in aliveEnemies) {
    Console.WriteLine($"Found: {enemy.Name}");
}
// 출력:
// Checking Goblin1
// Found: Goblin1
// Checking Goblin2 (Health=0, 필터링됨)
// Checking Orc1
// Found: Orc1

// 실행 시점: foreach 호출 시!

게임에서의 지연 실행 함정:

// 위험: Update에서 매 프레임 재평가
List<Enemy> allEnemies = GetAllEnemies();
var nearbyEnemies = allEnemies.Where(e => e.Distance < 10f);

void Update() {
    // 매 프레임 필터링 재실행!
    int count = nearbyEnemies.Count();      // 평가 1
    Enemy closest = nearbyEnemies.First();  // 평가 2
    Enemy farthest = nearbyEnemies.Last();  // 평가 3
    
    // 60fps에서 3번 * 60 = 180번/초 재평가!
}

// 해결: 캐싱
List<Enemy> cachedNearby;

void UpdateEnemies() {
    // 적 위치 변경 시에만 재평가
    cachedNearby = allEnemies
        .Where(e => e.Distance < 10f)
        .ToList();
}

void Update() {
    // 캐시된 리스트 사용 (평가 없음)
    int count = cachedNearby.Count;
    Enemy closest = cachedNearby.First();
    Enemy farthest = cachedNearby.Last();
}

게임 서버: 플레이어 쿼리

// 대규모 플레이어 관리
Dictionary<int, Player> players = GetAllPlayers();

// 지연 실행으로 메모리 절약
var activePlayers = players.Values
    .Where(p => p.IsOnline && p.LastActivity > DateTime.Now.AddMinutes(-5));

// 필요할 때만 평가
void BroadcastMessage(string message) {
    // 이 시점에 필터링 실행
    foreach (var player in activePlayers) {
        player.SendMessage(message);
    }
}

// 즉시 실행이 필요한 경우
List<Player> snapshot = activePlayers.ToList(); // 현재 시점 캡처
// 이후 players가 변경되어도 snapshot은 불변

2.2 LINQ의 내부 구현

// Where의 실제 구현 (개념적)
public static IEnumerable<T> Where<T>(
    this IEnumerable<T> source,
    Func<T, bool> predicate)
{
    foreach (var item in source) {
        if (predicate(item)) {
            yield return item; // Iterator 패턴
        }
    }
}

// Select의 실제 구현
public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TResult> selector)
{
    foreach (var item in source) {
        yield return selector(item);
    }
}

// 체이닝의 비밀
var result = numbers
    .Where(n => n > 2)    // Iterator 1
    .Select(n => n * 2)   // Iterator 2
    .ToList();            // 실행

// 실제로는:
// Iterator2(Iterator1(numbers))
// 중첩된 열거자!

yield return의 마법:

// 원본 코드
IEnumerable<int> GetNumbers() {
    yield return 1;
    yield return 2;
    yield return 3;
}

// 컴파일러가 생성하는 코드 (개념적)
class GetNumbersIterator : IEnumerable<int>, IEnumerator<int> {
    private int state = 0;
    private int current;
    
    public int Current => current;
    object IEnumerator.Current => current;
    
    public bool MoveNext() {
        switch (state) {
            case 0:
                current = 1;
                state = 1;
                return true;
            case 1:
                current = 2;
                state = 2;
                return true;
            case 2:
                current = 3;
                state = 3;
                return true;
            case 3:
                return false;
        }
        return false;
    }
    
    public IEnumerator<int> GetEnumerator() => this;
    IEnumerator IEnumerable.GetEnumerator() => this;
    public void Reset() => throw new NotSupportedException();
    public void Dispose() { }
}

// yield는 상태 머신!
// async/await와 유사한 변환

2.3 LINQ to Objects vs LINQ to SQL

// LINQ to Objects: 메모리 내 컬렉션
var query = users
    .Where(u => u.Age > 18)
    .OrderBy(u => u.Name)
    .Select(u => new { u.Name, u.Age });

// 실행: C# 코드로 필터링

// LINQ to SQL / Entity Framework
var query = dbContext.Users
    .Where(u => u.Age > 18)
    .OrderBy(u => u.Name)
    .Select(u => new { u.Name, u.Age });

// 실행: SQL로 변환!
// SELECT Name, Age
// FROM Users
// WHERE Age > 18
// ORDER BY Name

// 차이: Expression Tree

Expression Tree의 비밀:

// 델리게이트 (코드)
Func<int, bool> predicate = n => n > 2;
// 컴파일됨, 실행만 가능

// Expression Tree (데이터)
Expression<Func<int, bool>> expression = n => n > 2;
// 트리 구조로 저장, 분석 가능

// Expression Tree 탐색
Console.WriteLine(expression.NodeType); // Lambda
var lambda = (LambdaExpression)expression;
Console.WriteLine(lambda.Parameters[0].Name); // n
var body = (BinaryExpression)lambda.Body;
Console.WriteLine(body.NodeType); // GreaterThan
Console.WriteLine(body.Left); // n
Console.WriteLine(body.Right); // 2

// 트리 구조:
//       Lambda
//         |
//    GreaterThan (>)
//      /      \
//   Param(n)  Const(2)

// Entity Framework는 이 트리를 SQL로 변환

IQueryable vs IEnumerable:

// IEnumerable: 메모리에서 실행
IEnumerable<User> query1 = dbContext.Users
    .AsEnumerable()
    .Where(u => u.Age > 18)
    .OrderBy(u => u.Name);

// SQL:
// SELECT * FROM Users
// C#에서 필터링/정렬 (느림!)

// IQueryable: 데이터베이스에서 실행
IQueryable<User> query2 = dbContext.Users
    .Where(u => u.Age > 18)
    .OrderBy(u => u.Name);

// SQL:
// SELECT * FROM Users WHERE Age > 18 ORDER BY Name
// DB에서 필터링/정렬 (빠름!)

// 성능 차이: 100배 이상!

2.4 고급 LINQ 패턴

// 1. GroupBy: 적 타입별 통계
var enemyStats = enemies
    .GroupBy(e => e.Type)
    .Select(g => new {
        Type = g.Key,
        Count = g.Count(),
        TotalHealth = g.Sum(e => e.Health),
        AverageDamage = g.Average(e => e.Damage)
    });

// 게임 서버: 플레이어 레벨별 그룹화
var playersByLevel = players
    .GroupBy(p => p.Level / 10 * 10) // 10단위 그룹
    .Select(g => new {
        LevelRange = $"{g.Key}-{g.Key + 9}",
        PlayerCount = g.Count(),
        AveragePlayTime = g.Average(p => p.TotalPlayTime.TotalHours)
    });

// 2. Join: 플레이어와 길드
var playerGuilds = players
    .Join(
        guilds,
        p => p.GuildId,
        g => g.Id,
        (p, g) => new { p.Name, GuildName = g.Name }
    );

// 3. SelectMany: 모든 인벤토리 아이템
var allItems = players
    .SelectMany(p => p.Inventory.Items)
    .Where(i => i.Rarity == ItemRarity.Legendary);

// Unity: 모든 자식 컴포넌트
var allRenderers = gameObjects
    .SelectMany(go => go.GetComponentsInChildren<Renderer>());

// 4. DistinctBy: 유니크한 아이템 종류 (C# 9+)
var uniqueItemTypes = inventory.Items
    .DistinctBy(i => i.ItemId)
    .Select(i => i.Name);

// 5. Aggregate: 데미지 계산
var totalDamage = damageEvents.Aggregate(0f, (total, evt) => {
    float multiplier = evt.IsCritical ? 2.0f : 1.0f;
    return total + evt.BaseDamage * multiplier;
});

// 콤보 문자열 생성
var comboString = skills.Aggregate(
    new StringBuilder(),
    (sb, skill) => sb.Append(skill.Name).Append(" -> "),
    sb => sb.ToString().TrimEnd(' ', '-', '>')
);

// 6. Zip: 데미지와 타겟 매칭
var damages = new[] { 10, 20, 30 };
var targets = new[] { enemy1, enemy2, enemy3 };
var attacks = damages.Zip(targets, (dmg, target) => new { 
    Target = target, 
    Damage = dmg 
});

foreach (var attack in attacks) {
    attack.Target.TakeDamage(attack.Damage);
}

게임 최적화: LINQ vs 반복문

// LINQ: 간결하지만 느림
var aliveEnemies = enemies
    .Where(e => e.Health > 0)
    .OrderBy(e => e.Distance)
    .Take(5)
    .ToList();
// - Iterator 할당
// - 정렬 오버헤드
// - 시간: ~1ms (1000개 적 기준)

// 최적화: 수동 반복 (핫 패스)
aliveEnemyList.Clear();
for (int i = 0; i < enemies.Count && aliveEnemyList.Count < 5; i++) {
    if (enemies[i].Health > 0) {
        aliveEnemyList.Add(enemies[i]);
    }
}
// 정렬 생략, 할당 없음
// 시간: ~0.1ms (10배 빠름)

// 원칙:
// - 초기화/툴: LINQ 사용 (생산성)
// - Update/FixedUpdate: 반복문 사용 (성능)
// - 게임 서버: LINQ OK (프레임 제약 없음)

커스텀 LINQ 연산자:

// ForEachAsync: 병렬 에셋 로딩
public static async Task ForEachAsync<T>(
    this IEnumerable<T> source,
    int maxConcurrency,
    Func<T, Task> action)
{
    var semaphore = new SemaphoreSlim(maxConcurrency);
    var tasks = source.Select(async item => {
        await semaphore.WaitAsync();
        try {
            await action(item);
        } finally {
            semaphore.Release();
        }
    });
    await Task.WhenAll(tasks);
}

// 사용: 10개씩 동시 로딩
await assetPaths.ForEachAsync(10, async path => {
    var asset = await LoadAssetAsync(path);
    RegisterAsset(asset);
});

// Batch: 패킷 배치 전송
public static IEnumerable<IEnumerable<T>> Batch<T>(
    this IEnumerable<T> source,
    int size)
{
    var batch = new List<T>(size);
    foreach (var item in source) {
        batch.Add(item);
        if (batch.Count == size) {
            yield return batch;
            batch = new List<T>(size);
        }
    }
    if (batch.Count > 0) {
        yield return batch;
    }
}

// 사용: 100개씩 패킷 전송
var packets = GeneratePackets();
foreach (var batch in packets.Batch(100)) {
    SendBatch(batch);
    await Task.Delay(10); // 네트워크 부하 분산
}

3. 델리게이트와 이벤트 시스템

왜 이것이 중요한가?

델리게이트는 C#의 함수형 프로그래밍 기반이다. 이벤트는 느슨한 결합을 가능하게 하고, Action/Func는 고차 함수를 지원한다. 람다와 클로저는 간결한 코드를 만들지만, 힙 할당과 GC 압력을 유발할 수 있다.

3.1 델리게이트의 본질

// 델리게이트 선언
delegate int BinaryOperation(int a, int b);

// 사용
int Add(int a, int b) => a + b;
int Multiply(int a, int b) => a * b;

BinaryOperation op = Add;
Console.WriteLine(op(5, 3)); // 8

op = Multiply;
Console.WriteLine(op(5, 3)); // 15

// 델리게이트는 타입 안전한 함수 포인터

델리게이트의 내부 구조:

// 델리게이트는 클래스
public delegate int BinaryOperation(int a, int b);

// 컴파일러가 생성하는 코드 (개념적)
public sealed class BinaryOperation : MulticastDelegate {
    public BinaryOperation(object target, IntPtr method) { }
    
    public int Invoke(int a, int b) { }
    
    public IAsyncResult BeginInvoke(int a, int b, AsyncCallback callback, object state) { }
    
    public int EndInvoke(IAsyncResult result) { }
}

// MulticastDelegate 구조
class MulticastDelegate {
    private object _target;        // 인스턴스 메서드의 this
    private IntPtr _methodPtr;     // 메서드 포인터
    private MulticastDelegate _prev; // 체인 (멀티캐스트)
}

멀티캐스트 델리게이트:

void Method1() => Console.WriteLine("Method1");
void Method2() => Console.WriteLine("Method2");
void Method3() => Console.WriteLine("Method3");

Action action = Method1;
action += Method2; // 체인에 추가
action += Method3;

action(); // 모두 호출
// 출력:
// Method1
// Method2
// Method3

// 내부 구조:
// [Method1] -> [Method2] -> [Method3]

// 제거
action -= Method2;
action(); // Method1, Method3만 호출

3.2 람다와 클로저

// 람다 표현식
Func<int, int> square = x => x * x;
Func<int, int, int> add = (x, y) => x + y;
Action<string> print = s => Console.WriteLine(s);

// 클로저: 외부 변수 캡처
int factor = 10;
Func<int, int> multiply = x => x * factor;

Console.WriteLine(multiply(5)); // 50

factor = 20;
Console.WriteLine(multiply(5)); // 100 (변수 참조!)

// 클로저의 함정
var functions = new List<Func<int>>();
for (int i = 0; i < 5; i++) {
    functions.Add(() => i); // 클로저!
}

foreach (var func in functions) {
    Console.WriteLine(func());
}
// 출력: 5, 5, 5, 5, 5 (모두 같은 i 참조)

// 해결: 지역 변수로 복사
for (int i = 0; i < 5; i++) {
    int local = i;
    functions.Add(() => local);
}
// 출력: 0, 1, 2, 3, 4

클로저 컴파일:

// 원본 코드
int factor = 10;
Func<int, int> multiply = x => x * factor;

// 컴파일러가 생성하는 코드 (개념적)
class DisplayClass {
    public int factor; // 캡처된 변수
    
    public int MultiplyMethod(int x) {
        return x * this.factor;
    }
}

var displayClass = new DisplayClass();
displayClass.factor = 10;
Func<int, int> multiply = displayClass.MultiplyMethod;

// 클로저 = 힙 할당 + 간접 호출
// 성능 비용이 있음!

3.3 이벤트 패턴

// 게임 이벤트 시스템
class Player {
    // 델리게이트 정의
    public delegate void HealthChangedHandler(object sender, HealthChangedEventArgs e);
    
    // 이벤트 선언
    public event HealthChangedHandler HealthChanged;
    
    private int health = 100;
    
    public int Health {
        get => health;
        set {
            if (health != value) {
                var oldHealth = health;
                health = Math.Clamp(value, 0, 100);
                OnHealthChanged(new HealthChangedEventArgs(oldHealth, health));
            }
        }
    }
    
    // 이벤트 발생
    protected virtual void OnHealthChanged(HealthChangedEventArgs e) {
        HealthChanged?.Invoke(this, e);
    }
}

// EventHandler<T> 사용 (권장)
class Enemy {
    public event EventHandler<DamageEventArgs> Damaged;
    public event EventHandler Died;
    
    public void TakeDamage(int damage) {
        Health -= damage;
        Damaged?.Invoke(this, new DamageEventArgs(damage));
        
        if (Health <= 0) {
            Died?.Invoke(this, EventArgs.Empty);
        }
    }
}

// 사용: UI 업데이트
var player = new Player();
player.HealthChanged += (sender, e) => {
    UpdateHealthBar(e.NewHealth);
    
    if (e.NewHealth < 20) {
        PlayLowHealthWarning();
    }
};

// 사용: 적 처치 보상
var enemy = new Enemy();
enemy.Died += (sender, e) => {
    player.AddExperience(100);
    DropLoot(enemy.Position);
};

커스텀 이벤트 인자:

// 데미지 이벤트
class DamageEventArgs : EventArgs {
    public int Damage { get; }
    public DamageType Type { get; }
    public bool IsCritical { get; }
    public Vector3 HitPosition { get; }
    
    public DamageEventArgs(int damage, DamageType type, bool isCritical, Vector3 hitPos) {
        Damage = damage;
        Type = type;
        IsCritical = isCritical;
        HitPosition = hitPos;
    }
}

enum DamageType { Physical, Magical, True }

// 전투 시스템
class CombatSystem {
    public event EventHandler<DamageEventArgs> DamageDealt;
    
    public void Attack(Character attacker, Character target) {
        int damage = CalculateDamage(attacker, target);
        bool isCrit = Random.value < attacker.CritChance;
        
        if (isCrit) {
            damage *= 2;
        }
        
        target.Health -= damage;
        
        DamageDealt?.Invoke(this, new DamageEventArgs(
            damage, 
            attacker.DamageType, 
            isCrit, 
            target.Position
        ));
    }
}

// 구독: 데미지 텍스트 표시
combatSystem.DamageDealt += (sender, e) => {
    ShowDamageText(e.Damage, e.HitPosition, e.IsCritical);
    
    if (e.Type == DamageType.Magical) {
        PlayMagicalHitEffect(e.HitPosition);
    }
};

게임 이벤트 버스 패턴:

// 전역 이벤트 시스템
class GameEventBus {
    private static GameEventBus instance;
    public static GameEventBus Instance => instance ??= new GameEventBus();
    
    public event EventHandler<EnemyKilledEventArgs> EnemyKilled;
    public event EventHandler<LevelUpEventArgs> PlayerLeveledUp;
    public event EventHandler<ItemCollectedEventArgs> ItemCollected;
    
    public void RaiseEnemyKilled(Enemy enemy) {
        EnemyKilled?.Invoke(this, new EnemyKilledEventArgs(enemy));
    }
    
    public void RaisePlayerLeveledUp(int newLevel) {
        PlayerLeveledUp?.Invoke(this, new LevelUpEventArgs(newLevel));
    }
}

// 구독: 퀘스트 시스템
class QuestSystem {
    void Start() {
        GameEventBus.Instance.EnemyKilled += OnEnemyKilled;
        GameEventBus.Instance.ItemCollected += OnItemCollected;
    }
    
    void OnEnemyKilled(object sender, EnemyKilledEventArgs e) {
        // 퀘스트 진행도 업데이트
        foreach (var quest in activeQuests) {
            if (quest.TargetEnemy == e.Enemy.Type) {
                quest.Progress++;
                CheckQuestCompletion(quest);
            }
        }
    }
    
    void OnItemCollected(object sender, ItemCollectedEventArgs e) {
        // 수집 퀘스트 업데이트
    }
}

// 구독: 업적 시스템
class AchievementSystem {
    private int enemiesKilled = 0;
    
    void Start() {
        GameEventBus.Instance.EnemyKilled += (sender, e) => {
            enemiesKilled++;
            
            if (enemiesKilled >= 100) {
                UnlockAchievement("Monster Slayer");
            }
        };
    }
}

3.4 약한 이벤트 패턴

// 문제: 이벤트 메모리 누수
class Publisher {
    public event EventHandler DataChanged;
}

class Subscriber {
    public Subscriber(Publisher publisher) {
        publisher.DataChanged += OnDataChanged;
    }
    
    private void OnDataChanged(object sender, EventArgs e) { }
    
    // 문제: 구독 해제 안 하면 Publisher가 Subscriber 참조
    // Subscriber가 더 이상 필요 없어도 GC 불가
}

// 해결 1: IDisposable
class ProperSubscriber : IDisposable {
    private Publisher publisher;
    
    public ProperSubscriber(Publisher pub) {
        publisher = pub;
        publisher.DataChanged += OnDataChanged;
    }
    
    private void OnDataChanged(object sender, EventArgs e) { }
    
    public void Dispose() {
        publisher.DataChanged -= OnDataChanged;
    }
}

// 해결 2: WeakEventManager (WPF)
class WeakSubscriber {
    public WeakSubscriber(Publisher publisher) {
        WeakEventManager<Publisher, EventArgs>
            .AddHandler(publisher, nameof(Publisher.DataChanged), OnDataChanged);
    }
    
    private void OnDataChanged(object sender, EventArgs e) { }
}

4. 리플렉션과 메타프로그래밍

왜 이것이 중요한가?

리플렉션은 런타임에 타입을 조사하고 조작한다. DI 컨테이너, 직렬화, ORM은 모두 리플렉션 기반이다. 하지만 리플렉션은 느리다 (100배). Source Generator는 컴파일 타임 메타프로그래밍으로 런타임 비용을 제거한다.

4.1 리플렉션의 기초

// 타입 정보 얻기
Type type = typeof(string);
Console.WriteLine(type.FullName); // System.String
Console.WriteLine(type.IsClass);  // True
Console.WriteLine(type.IsValueType); // False

// 런타임 타입
object obj = "Hello";
Type runtimeType = obj.GetType();
Console.WriteLine(runtimeType.Name); // String

// 멤버 탐색
var members = type.GetMembers();
foreach (var member in members) {
    Console.WriteLine($"{member.MemberType}: {member.Name}");
}
// Method: Length
// Method: Substring
// Property: Length
// ...

동적 인스턴스 생성:

// 일반적인 방법
var list = new List<int>();

// 리플렉션
Type listType = typeof(List<int>);
object instance = Activator.CreateInstance(listType);

// 제네릭 타입
Type genericType = typeof(List<>);
Type concreteType = genericType.MakeGenericType(typeof(int));
object list2 = Activator.CreateInstance(concreteType);

// 생성자 파라미터
Type type = typeof(Person);
object person = Activator.CreateInstance(type, "John", 30);

// 또는
var constructor = type.GetConstructor(new[] { typeof(string), typeof(int) });
object person2 = constructor.Invoke(new object[] { "Jane", 25 });

메서드 호출:

class Calculator {
    public int Add(int a, int b) => a + b;
    public static int Multiply(int a, int b) => a * b;
}

// 인스턴스 메서드
var calc = new Calculator();
Type type = typeof(Calculator);
MethodInfo addMethod = type.GetMethod("Add");
object result = addMethod.Invoke(calc, new object[] { 5, 3 });
Console.WriteLine(result); // 8

// 정적 메서드
MethodInfo multiplyMethod = type.GetMethod("Multiply");
object result2 = multiplyMethod.Invoke(null, new object[] { 5, 3 });
Console.WriteLine(result2); // 15

// 성능 비교
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1_000_000; i++) {
    calc.Add(5, 3); // 직접 호출
}
Console.WriteLine($"Direct: {sw.ElapsedMilliseconds}ms");
// 출력: ~5ms

sw.Restart();
for (int i = 0; i < 1_000_000; i++) {
    addMethod.Invoke(calc, new object[] { 5, 3 }); // 리플렉션
}
Console.WriteLine($"Reflection: {sw.ElapsedMilliseconds}ms");
// 출력: ~500ms (100배 느림!)

4.2 Attribute와 메타데이터

// 커스텀 Attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
class TableAttribute : Attribute {
    public string Name { get; }
    public TableAttribute(string name) => Name = name;
}

[AttributeUsage(AttributeTargets.Property)]
class ColumnAttribute : Attribute {
    public string Name { get; }
    public bool IsPrimaryKey { get; set; }
    
    public ColumnAttribute(string name) => Name = name;
}

// 사용
[Table("Users")]
class User {
    [Column("user_id", IsPrimaryKey = true)]
    public int Id { get; set; }
    
    [Column("user_name")]
    public string Name { get; set; }
    
    [Column("user_age")]
    public int Age { get; set; }
}

// Attribute 읽기
Type type = typeof(User);
var tableAttr = type.GetCustomAttribute<TableAttribute>();
Console.WriteLine($"Table: {tableAttr.Name}"); // Users

foreach (var prop in type.GetProperties()) {
    var columnAttr = prop.GetCustomAttribute<ColumnAttribute>();
    if (columnAttr != null) {
        Console.WriteLine($"{prop.Name} -> {columnAttr.Name}");
        if (columnAttr.IsPrimaryKey) {
            Console.WriteLine("  (Primary Key)");
        }
    }
}

간단한 ORM / Component 시스템:

// Unity 스타일 Component 시스템 구현
class GameObject {
    private Dictionary<Type, Component> components = new();
    
    public T AddComponent<T>() where T : Component, new() {
        Type type = typeof(T);
        if (components.ContainsKey(type)) {
            throw new InvalidOperationException($"Component {type.Name} already exists");
        }
        
        T component = new T();
        component.GameObject = this;
        components[type] = component;
        
        // 리플렉션으로 Start 메서드 호출
        MethodInfo startMethod = type.GetMethod("Start", 
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        startMethod?.Invoke(component, null);
        
        return component;
    }
    
    public T GetComponent<T>() where T : Component {
        Type type = typeof(T);
        return components.TryGetValue(type, out var component) 
            ? component as T 
            : null;
    }
}

abstract class Component {
    public GameObject GameObject { get; internal set; }
    protected virtual void Start() { }
    protected virtual void Update() { }
}

// 사용
class HealthComponent : Component {
    public int Health = 100;
    
    protected override void Start() {
        Console.WriteLine("HealthComponent initialized");
    }
}

var gameObject = new GameObject();
var health = gameObject.AddComponent<HealthComponent>();
// 출력: HealthComponent initialized

// 데이터 직렬화 시스템
class SaveSystem {
    public static string Serialize(object obj) {
        Type type = obj.GetType();
        var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
        
        var data = new Dictionary<string, object>();
        foreach (var prop in properties) {
            // [NonSerialized] 속성 체크
            if (prop.GetCustomAttribute<NonSerializedAttribute>() != null) {
                continue;
            }
            
            data[prop.Name] = prop.GetValue(obj);
        }
        
        return JsonSerializer.Serialize(data);
    }
    
    public static T Deserialize<T>(string json) where T : new() {
        var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
        T obj = new T();
        Type type = typeof(T);
        
        foreach (var kvp in data) {
            PropertyInfo prop = type.GetProperty(kvp.Key);
            if (prop != null && prop.CanWrite) {
                object value = JsonSerializer.Deserialize(kvp.Value.GetRawText(), prop.PropertyType);
                prop.SetValue(obj, value);
            }
        }
        
        return obj;
    }
}

// 사용
class PlayerData {
    public string Name { get; set; }
    public int Level { get; set; }
    public float[] Position { get; set; }
    
    [NonSerialized]
    public GameObject GameObject; // 직렬화 제외
}

var player = new PlayerData { 
    Name = "Hero", 
    Level = 10, 
    Position = new[] { 1.5f, 2.3f, 0.0f } 
};

string json = SaveSystem.Serialize(player);
// {"Name":"Hero","Level":10,"Position":[1.5,2.3,0.0]}

PlayerData loaded = SaveSystem.Deserialize<PlayerData>(json);

4.3 Expression Tree 조작

// Expression Tree 생성
Expression<Func<int, int, int>> expr = (a, b) => a + b;

// 수동 생성
ParameterExpression paramA = Expression.Parameter(typeof(int), "a");
ParameterExpression paramB = Expression.Parameter(typeof(int), "b");
BinaryExpression body = Expression.Add(paramA, paramB);
Expression<Func<int, int, int>> manualExpr = 
    Expression.Lambda<Func<int, int, int>>(body, paramA, paramB);

// 컴파일 및 실행
Func<int, int, int> compiled = manualExpr.Compile();
Console.WriteLine(compiled(5, 3)); // 8

// Expression Tree 변환 (방문자 패턴)
class ParameterReplacer : ExpressionVisitor {
    private readonly ParameterExpression oldParam;
    private readonly ParameterExpression newParam;
    
    public ParameterReplacer(ParameterExpression oldParam, ParameterExpression newParam) {
        this.oldParam = oldParam;
        this.newParam = newParam;
    }
    
    protected override Expression VisitParameter(ParameterExpression node) {
        return node == oldParam ? newParam : base.VisitParameter(node);
    }
}

// 파라미터 이름 변경
Expression<Func<int, int>> original = x => x * 2;
ParameterExpression newParam = Expression.Parameter(typeof(int), "y");
var replacer = new ParameterReplacer(original.Parameters[0], newParam);
Expression<Func<int, int>> modified = (Expression<Func<int, int>>)replacer.Visit(original);
// 원본: x => x * 2
// 변경: y => y * 2

4.4 Source Generator (C# 9+)

// 문제: 리플렉션은 느림
// 해결: 컴파일 타임 코드 생성

// Source Generator (별도 프로젝트)
[Generator]
public class AutoNotifyGenerator : ISourceGenerator {
    public void Initialize(GeneratorInitializationContext context) { }
    
    public void Execute(GeneratorExecutionContext context) {
        // [AutoNotify] 속성이 있는 클래스 찾기
        var compilation = context.Compilation;
        
        foreach (var syntaxTree in compilation.SyntaxTrees) {
            var model = compilation.GetSemanticModel(syntaxTree);
            var classes = syntaxTree.GetRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .Where(c => HasAutoNotifyAttribute(c, model));
            
            foreach (var classDecl in classes) {
                // INotifyPropertyChanged 구현 생성
                string generatedCode = GenerateNotifyCode(classDecl);
                context.AddSource($"{classDecl.Identifier}.g.cs", generatedCode);
            }
        }
    }
}

// 사용자 코드
[AutoNotify]
partial class Person {
    private string name;
    private int age;
}

// 생성되는 코드
partial class Person : INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;
    
    public string Name {
        get => name;
        set {
            if (name != value) {
                name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
    
    public int Age {
        get => age;
        set {
            if (age != value) {
                age = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Age)));
            }
        }
    }
}

// 장점:
// - 컴파일 타임 생성 (런타임 비용 없음)
// - 타입 안전
// - 리플렉션 불필요

5. 타입 시스템의 진화

왜 이것이 중요한가?

C#의 타입 시스템은 계속 진화한다. Nullable 참조 타입은 null 참조 예외를 컴파일 타임에 방지한다. Record는 불변 데이터를 간결하게 표현한다. 패턴 매칭은 타입 검사와 데이터 추출을 통합한다.

5.1 Nullable 참조 타입 (C# 8+)

// C# 8 이전: 모든 참조 타입이 nullable
string name = null; // OK

// C# 8+: Nullable 명시
#nullable enable

string name = null;  // 경고: null을 nullable이 아닌 타입에 할당
string? nullableName = null; // OK

void PrintLength(string text) {
    Console.WriteLine(text.Length); // text는 null이 아님을 보장
}

void PrintLengthSafe(string? text) {
    if (text != null) {
        Console.WriteLine(text.Length); // null 체크 후 안전
    }
    // Console.WriteLine(text.Length); // 경고: text가 null일 수 있음
}

// Null 병합 연산자
string? nullableText = GetText();
string text = nullableText ?? "default";

// Null 조건 연산자
int? length = nullableText?.Length;

// Null 허용 연산자 (!)
string text2 = nullableText!; // 확신: null이 아님 (주의!)

Nullable 어노테이션:

// [NotNull] - 반환값이 null이 아님
string GetName([NotNull] string? input) {
    if (input == null) {
        return "Unknown";
    }
    return input;
}

// [MaybeNull] - 반환값이 null일 수 있음
[return: MaybeNull]
T GetValueOrDefault<T>(T? value) where T : class {
    return value ?? default(T);
}

// [NotNullWhen] - 조건부 null 체크
bool TryGetValue(string key, [NotNullWhen(true)] out string? value) {
    if (cache.ContainsKey(key)) {
        value = cache[key];
        return true;
    }
    value = null;
    return false;
}

// 사용
if (TryGetValue("key", out string? result)) {
    // result는 여기서 null이 아님
    Console.WriteLine(result.Length);
}

5.2 Record 타입 (C# 9+)

// 전통적인 불변 클래스
class PersonClass {
    public string Name { get; init; }
    public int Age { get; init; }
    
    public PersonClass(string name, int age) {
        Name = name;
        Age = age;
    }
    
    public override bool Equals(object? obj) {
        return obj is PersonClass other &&
               Name == other.Name &&
               Age == other.Age;
    }
    
    public override int GetHashCode() {
        return HashCode.Combine(Name, Age);
    }
    
    public override string ToString() {
        return $"PersonClass {{ Name = {Name}, Age = {Age} }}";
    }
}

// Record: 간결!
record PersonRecord(string Name, int Age);

// 자동 생성:
// - 위치 생성자
// - 값 기반 equality
// - GetHashCode
// - ToString
// - Deconstruct

// 사용
var person = new PersonRecord("John", 30);
Console.WriteLine(person); // PersonRecord { Name = John, Age = 30 }

var (name, age) = person; // Deconstruction

// with 표현식 (비파괴적 변경)
var older = person with { Age = 31 };
Console.WriteLine(person.Age); // 30 (원본 불변)
Console.WriteLine(older.Age);  // 31

// Equality
var person2 = new PersonRecord("John", 30);
Console.WriteLine(person == person2); // True (값 비교)

Record Struct (C# 10+):

// 값 타입 record
record struct Point(int X, int Y);

// 변경 가능한 record struct
record struct MutablePoint(int X, int Y) {
    public int X { get; set; } = X;
    public int Y { get; set; } = Y;
}

// readonly record struct
readonly record struct ImmutablePoint(int X, int Y);

5.3 패턴 매칭

// 타입 패턴
object obj = "Hello";

if (obj is string s) {
    Console.WriteLine($"String: {s}");
}

// switch 표현식 (C# 8+)
string GetShapeDescription(object shape) => shape switch {
    Circle c => $"Circle with radius {c.Radius}",
    Rectangle r => $"Rectangle {r.Width}x{r.Height}",
    Triangle t => $"Triangle",
    _ => "Unknown shape"
};

// 속성 패턴
string GetDiscount(Customer customer) => customer switch {
    { Age: >= 65 } => "Senior discount",
    { Orders: > 10 } => "Loyal customer discount",
    { IsStudent: true } => "Student discount",
    _ => "No discount"
};

// 위치 패턴 (C# 10+)
string GetQuadrant(Point point) => point switch {
    (0, 0) => "Origin",
    (> 0, > 0) => "Quadrant 1",
    (< 0, > 0) => "Quadrant 2",
    (< 0, < 0) => "Quadrant 3",
    (> 0, < 0) => "Quadrant 4",
    _ => "On axis"
};

// 리스트 패턴 (C# 11+)
string CheckArray(int[] numbers) => numbers switch {
    [] => "Empty",
    [1] => "Single element: 1",
    [1, 2] => "1 and 2",
    [var first, .., var last] => $"First: {first}, Last: {last}",
    _ => "Other"
};

고급 패턴:

// and, or, not 패턴
bool IsValidAge(int age) => age is >= 0 and < 120;
bool IsWeekend(DayOfWeek day) => day is DayOfWeek.Saturday or DayOfWeek.Sunday;
bool IsNotNull(object? obj) => obj is not null;

// 재귀 패턴
record Address(string Street, string City);
record Person(string Name, Address Address);

bool IsFromSeoul(Person person) => person is { Address: { City: "Seoul" } };

// when 절
string GetTemperatureDescription(int temp) => temp switch {
    < 0 => "Freezing",
    >= 0 and < 10 => "Cold",
    >= 10 and < 20 when IsRaining() => "Cool and rainy",
    >= 10 and < 20 => "Mild",
    >= 20 and < 30 => "Warm",
    >= 30 => "Hot",
    _ => "Unknown"
};

5.4 현대 C# 기능들

// Target-typed new (C# 9+)
List<int> numbers = new(); // new List<int>()
Dictionary<string, int> dict = new(); // new Dictionary<string, int>()

// Init-only properties (C# 9+)
class Person {
    public string Name { get; init; }
    public int Age { get; init; }
}

var person = new Person { Name = "John", Age = 30 };
// person.Name = "Jane"; // 컴파일 에러!

// Top-level statements (C# 9+)
// Program.cs:
Console.WriteLine("Hello, World!");
// 클래스, Main 메서드 불필요

// Global using (C# 10+)
// GlobalUsings.cs:
global using System;
global using System.Linq;
global using System.Collections.Generic;

// File-scoped namespace (C# 10+)
namespace MyApp.Models;

class User { } // 중괄호 제거

// Required members (C# 11+)
class Person {
    public required string Name { get; init; }
    public required int Age { get; init; }
}

var person = new Person(); // 컴파일 에러!
var validPerson = new Person { Name = "John", Age = 30 }; // OK

// Raw string literals (C# 11+)
string json = """
{
    "name": "John",
    "age": 30
}
""";

// List patterns (C# 11+)
int[] numbers = { 1, 2, 3 };
if (numbers is [1, 2, 3]) {
    Console.WriteLine("Matches");
}

// Primary constructors (C# 12+)
class Person(string name, int age) {
    public string Name { get; } = name;
    public int Age { get; } = age;
}

// Collection expressions (C# 12+)
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];

6. 고급 제네릭과 공변성/반공변성

왜 이것이 중요한가?

제네릭은 타입 안전한 재사용 코드를 만든다. 공변성/반공변성은 상속 계층에서 제네릭을 유연하게 사용한다. 제약 조건은 컴파일 타임 안전성을 보장한다. 타입 추론은 코드를 간결하게 한다.

6.1 제네릭 제약 조건

// where T : class - 참조 타입
class Repository<T> where T : class {
    private List<T> items = new();
    
    public void Add(T item) {
        if (item == null) throw new ArgumentNullException();
        items.Add(item);
    }
}

// where T : struct - 값 타입
struct Point<T> where T : struct {
    public T X { get; set; }
    public T Y { get; set; }
}

// where T : new() - 기본 생성자
class Factory<T> where T : new() {
    public T Create() => new T();
}

// where T : BaseClass - 특정 클래스 상속
class EntityRepository<T> where T : Entity {
    public void Save(T entity) {
        entity.Id = GenerateId();
        // ...
    }
}

// where T : IComparable - 인터페이스 구현
class Sorter<T> where T : IComparable<T> {
    public void Sort(List<T> items) {
        items.Sort((a, b) => a.CompareTo(b));
    }
}

// 다중 제약
class Cache<TKey, TValue>
    where TKey : IEquatable<TKey>
    where TValue : class, new() {
}

실전 예제:

// 제네릭 저장소
interface IEntity {
    int Id { get; set; }
}

class GenericRepository<T> where T : IEntity, new() {
    private readonly List<T> storage = new();
    
    public void Add(T entity) {
        entity.Id = storage.Count + 1;
        storage.Add(entity);
    }
    
    public T GetById(int id) {
        return storage.FirstOrDefault(e => e.Id == id) ?? new T();
    }
    
    public IEnumerable<T> GetAll() => storage;
}

// 사용
class User : IEntity {
    public int Id { get; set; }
    public string Name { get; set; }
}

var userRepo = new GenericRepository<User>();
userRepo.Add(new User { Name = "John" });

6.2 공변성과 반공변성

// 문제: 불변성 (Invariance)
List<string> strings = new List<string>();
// List<object> objects = strings; // 컴파일 에러!

// 이유: List<T>는 불변
// 만약 가능하다면:
// objects.Add(123); // int를 추가
// string s = strings[0]; // 런타임 에러!

// 공변성 (Covariance): out T
// 반환값만 사용 (읽기 전용)
interface IReadOnlyList<out T> {
    T this[int index] { get; }
    int Count { get; }
}

IReadOnlyList<string> strings2 = new List<string> { "a", "b" };
IReadOnlyList<object> objects2 = strings2; // OK! (공변성)

// 안전한 이유: 읽기만 가능
object obj = objects2[0]; // string을 object로 읽기 (안전)

// 반공변성 (Contravariance): in T
// 파라미터만 사용 (쓰기 전용)
interface IComparer<in T> {
    int Compare(T x, T y);
}

IComparer<object> objectComparer = /* ... */;
IComparer<string> stringComparer = objectComparer; // OK! (반공변성)

// 안전한 이유: 쓰기만 가능
stringComparer.Compare("a", "b"); // string을 object로 비교 (안전)

실전 예제:

// 공변성: IEnumerable<out T>
IEnumerable<string> strings = new[] { "a", "b", "c" };
IEnumerable<object> objects = strings; // 공변성

foreach (object obj in objects) {
    Console.WriteLine(obj); // 안전
}

// 반공변성: Action<in T>
Action<object> actionObject = obj => Console.WriteLine(obj);
Action<string> actionString = actionObject; // 반공변성

actionString("Hello"); // string을 object로 처리 (안전)

// Func<in T, out TResult>: 입력은 반공변, 출력은 공변
Func<object, string> funcObjectToString = obj => obj.ToString();
Func<string, object> funcStringToObject = funcObjectToString; // 둘 다 적용

object result = funcStringToObject("input");

6.3 고급 제네릭 패턴

// 제네릭 메서드
T Max<T>(T a, T b) where T : IComparable<T> {
    return a.CompareTo(b) > 0 ? a : b;
}

int maxInt = Max(5, 10);
string maxString = Max("apple", "banana");

// 타입 추론
var result = Max(5, 10); // T가 int로 추론됨

// 제네릭 확장 메서드
static class EnumerableExtensions {
    public static T MaxBy<T, TKey>(
        this IEnumerable<T> source,
        Func<T, TKey> keySelector)
        where TKey : IComparable<TKey> {
        
        T maxItem = default;
        TKey maxKey = default;
        bool first = true;
        
        foreach (var item in source) {
            var key = keySelector(item);
            if (first || key.CompareTo(maxKey) > 0) {
                maxItem = item;
                maxKey = key;
                first = false;
            }
        }
        
        return maxItem;
    }
}

// 사용
var oldestPerson = people.MaxBy(p => p.Age);

// 제네릭 델리게이트
delegate TResult Converter<in T, out TResult>(T input);

Converter<string, int> stringToInt = s => int.Parse(s);
Converter<object, object> objectToObject = stringToInt; // 공변/반공변

7. C#과 IL: 컴파일의 이해

왜 이것이 중요한가?

C#은 IL(중간 언어)로 컴파일되고 JIT(Just-In-Time)으로 네이티브 코드가 된다. IL을 이해하면 성능 최적화와 동작 원리를 파악할 수 있다. ILSpy/dnSpy는 디컴파일로 구현을 확인한다.

7.1 C#에서 IL까지

// C# 코드
int Add(int a, int b) {
    return a + b;
}

// IL 코드 (ildasm 출력)
.method private hidebysig instance int32 Add(int32 a, int32 b) cil managed
{
    .maxstack 2
    ldarg.1      // a를 스택에 로드
    ldarg.2      // b를 스택에 로드
    add          // 더하기
    ret          // 반환
}

// JIT → 네이티브 코드 (x64)
mov eax, ecx   // a를 eax에
add eax, edx   // b를 더하기
ret            // 반환

복잡한 예제:

// C# 코드
class Counter {
    private int count = 0;
    
    public void Increment() {
        count++;
    }
    
    public int GetCount() => count;
}

// IL 코드
.class private auto ansi beforefieldinit Counter extends [System.Runtime]System.Object
{
    .field private int32 count
    
    .method public hidebysig instance void Increment() cil managed
    {
        .maxstack 3
        ldarg.0           // this
        ldarg.0           // this
        ldfld int32 Counter::count  // count 로드
        ldc.i4.1          // 1 로드
        add               // count + 1
        stfld int32 Counter::count  // count 저장
        ret
    }
    
    .method public hidebysig instance int32 GetCount() cil managed
    {
        .maxstack 1
        ldarg.0           // this
        ldfld int32 Counter::count  // count 로드
        ret
    }
}

7.2 Boxing/Unboxing의 IL

// C# 코드
int value = 42;
object boxed = value;        // Boxing
int unboxed = (int)boxed;    // Unboxing

// IL 코드
// Boxing
ldc.i4.s 42          // 42를 스택에
stloc.0              // value에 저장

ldloc.0              // value 로드
box [System.Runtime]System.Int32  // Boxing (힙 할당!)
stloc.1              // boxed에 저장

// Unboxing
ldloc.1              // boxed 로드
unbox.any [System.Runtime]System.Int32  // Unboxing
stloc.2              // unboxed에 저장

// Boxing = 힙 할당 + 값 복사
// Unboxing = 타입 체크 + 값 복사

7.3 JIT 컴파일과 최적화

// JIT는 메서드를 처음 호출할 때 컴파일
void FirstCall() {
    // IL → 네이티브 코드 변환 (느림)
    ProcessData();
}

void SecondCall() {
    // 이미 컴파일됨 (빠름)
    ProcessData();
}

// 인라이닝 최적화
int Add(int a, int b) => a + b;

void Calculate() {
    int result = Add(5, 3);
    // JIT가 인라이닝:
    // int result = 5 + 3;
    // 메서드 호출 비용 제거!
}

// 루프 최적화
void ProcessArray(int[] array) {
    for (int i = 0; i < array.Length; i++) {
        array[i] *= 2;
    }
    // JIT가 범위 체크 제거 (안전하다고 판단 시)
}

Tiered Compilation (C# 7.1+):

// Tier 0: 빠른 JIT (최적화 없음)
// - 시작 시간 단축
// - 기본 코드 생성

// Tier 1: 최적화된 JIT
// - 메서드가 자주 호출되면 재컴파일
// - 인라이닝, 루프 풀기 등 최적화

void HotPath() {
    // 처음: Tier 0 (빠른 시작)
    // 자주 호출됨
    // → Tier 1로 재컴파일 (최적화됨)
}

// 확인
[MethodImpl(MethodImplOptions.NoOptimization)]
void NoOptimization() {
    // JIT 최적화 비활성화
}

7.4 Unity와 IL2CPP

Mono vs IL2CPP:

// Mono: JIT 컴파일
// - 런타임에 IL → 네이티브 변환
// - 빠른 빌드
// - 큰 바이너리
// - iOS 제약 (JIT 금지)

// IL2CPP: AOT 컴파일
// - 빌드 타임에 IL → C++ → 네이티브
// - 느린 빌드 (5-10분)
// - 작은 바이너리
// - 모든 플랫폼 지원
// - 성능 향상 (10-30%)

IL2CPP 제약 사항:

// 1. 리플렉션 제약
// Mono: 모든 리플렉션 가능
Type type = Type.GetType("MyNamespace.MyClass");
object instance = Activator.CreateInstance(type);

// IL2CPP: 코드 스트리핑으로 타입 정보 제거됨
// 해결: link.xml로 보존
/*
<linker>
    <assembly fullname="Assembly-CSharp">
        <type fullname="MyNamespace.MyClass" preserve="all"/>
    </assembly>
</linker>
*/

// 2. 제네릭 제약
// Mono: 런타임 제네릭 생성 가능
void GenericMethod<T>() where T : new() {
    T instance = new T(); // OK
}

// IL2CPP: AOT 컴파일 시 사용된 타입만 생성
// 미리 사용하지 않은 타입은 런타임 에러!
GenericMethod<KnownClass>(); // OK (빌드에 포함)
GenericMethod<UnknownClass>(); // 런타임 에러!

// 해결: 명시적 사용
void EnsureGenericTypes() {
    GenericMethod<PossibleClass1>();
    GenericMethod<PossibleClass2>();
    // 빌드에 포함됨
}

// 3. Delegate.BeginInvoke 미지원
Action action = () => DoSomething();
// action.BeginInvoke(null, null); // IL2CPP 에러!

// 해결: Task 사용
Task.Run(() => DoSomething());

Unity 성능 고려사항:

// 1. Update 루프 최적화
// 나쁜 예
void Update() {
    // 매 프레임 할당
    var enemies = FindObjectsOfType<Enemy>(); // 느림!
    var position = transform.position; // 네이티브 호출
    
    foreach (var enemy in enemies) {
        float distance = Vector3.Distance(position, enemy.transform.position);
    }
}

// 좋은 예
private Enemy[] cachedEnemies;
private Vector3 cachedPosition;

void Start() {
    cachedEnemies = FindObjectsOfType<Enemy>(); // 한 번만
}

void Update() {
    cachedPosition = transform.position; // 한 번만 호출
    
    for (int i = 0; i < cachedEnemies.Length; i++) {
        var enemy = cachedEnemies[i];
        if (enemy == null) continue; // null 체크
        
        Vector3 diff = cachedPosition - enemy.transform.position;
        float distanceSq = diff.sqrMagnitude; // Sqrt 생략
    }
}

// 2. Boxing 방지
// 나쁜 예
Debug.Log("Position: " + transform.position); // Boxing!
// Vector3는 struct → ToString() → Boxing

// 좋은 예
Debug.Log($"Position: {transform.position.x}, {transform.position.y}, {transform.position.z}");
// 또는 조건부 컴파일
#if UNITY_EDITOR
Debug.Log($"Position: {transform.position}");
#endif

// 3. 코루틴 vs async/await
// 코루틴: Unity 전용, 할당 있음
IEnumerator LoadAssetCoroutine() {
    var request = Resources.LoadAsync<Texture>("Texture");
    yield return request; // Iterator 할당
    texture = request.asset as Texture;
}

// async/await: 표준 C#, 더 적은 할당
async Task LoadAssetAsync() {
    var request = Resources.LoadAsync<Texture>("Texture");
    await request; // 상태 머신, 재사용 가능
    texture = request.asset as Texture;
}

// Unity 2023+: await 완전 지원
// 이전 버전: UniTask 라이브러리 추천

게임 서버 vs Unity 클라이언트:

// 게임 서버 (.NET Core/6+)
// - 모든 C# 기능 사용 가능
// - 최신 라이브러리
// - GC 튜닝 가능
// - async/await 최적화

class GameServer {
    async Task HandlePlayerAsync(Player player) {
        // 모든 기능 사용 가능
        var data = await LoadPlayerDataAsync(player.Id);
        await ProcessGameLogicAsync(data);
    }
}

// Unity 클라이언트
// - IL2CPP 제약
// - GC 스파이크 주의
// - 프레임 예산 (16.67ms)
// - 네이티브 상호작용

class GameClient : MonoBehaviour {
    async void Start() {
        // Unity 메인 스레드에서 실행
        var texture = await LoadTextureAsync();
        material.mainTexture = texture;
    }
    
    void Update() {
        // 16.67ms 예산
        // 할당 최소화
        // LINQ 지양
    }
}

참고 자료

필수 도서

  • C# in Depth by Jon Skeet
  • CLR via C# by Jeffrey Richter
  • Concurrency in C# Cookbook by Stephen Cleary

공식 문서

도구

블로그

profile
RL Researcher, Video Game Developer

0개의 댓글