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의 제약을 우회할 수 있으며, 게임 서버를 최적화할 수 있다.
동기 코드는 블로킹으로 스레드를 낭비한다. 1000개 요청 처리에 1000개 스레드가 필요하다. async/await는 스레드 없이 대기하므로 10개 스레드로 10만 개 요청을 처리한다. 이는 I/O 바운드 작업에서 100배 효율 향상을 의미한다.
// 게임 서버: 플레이어 데이터 로드
// 동기: 스레드가 블로킹
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;
// 프레임 드롭 없음!
}
// 원본 코드
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) { }
}
핵심 개념:
// await의 실제 동작
TaskAwaiter awaiter = task.GetAwaiter();
if (awaiter.IsCompleted) {
// 이미 완료: 동기 실행
var result = awaiter.GetResult();
} else {
// 미완료: 콜백 등록 후 반환
awaiter.OnCompleted(() => {
// Continuation: 나중에 실행
var result = awaiter.GetResult();
});
return; // 현재 스레드 해제
}
// 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: 차이 거의 없음
// 게임 서버: 플레이어 데이터 캐시
// 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(); // 재사용 가능 (할당 발생)
// 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); // 간결!
}
LINQ는 데이터 질의를 퍼스트 클래스 언어 기능으로 만든다. 컬렉션, 데이터베이스, XML을 동일한 구문으로 질의한다. 지연 실행은 메모리 효율을 극대화하고, Expression Tree는 코드를 데이터로 변환해 최적화를 가능하게 한다.
// 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은 불변
// 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와 유사한 변환
// 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배 이상!
// 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); // 네트워크 부하 분산
}
델리게이트는 C#의 함수형 프로그래밍 기반이다. 이벤트는 느슨한 결합을 가능하게 하고, Action/Func는 고차 함수를 지원한다. 람다와 클로저는 간결한 코드를 만들지만, 힙 할당과 GC 압력을 유발할 수 있다.
// 델리게이트 선언
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만 호출
// 람다 표현식
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;
// 클로저 = 힙 할당 + 간접 호출
// 성능 비용이 있음!
// 게임 이벤트 시스템
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");
}
};
}
}
// 문제: 이벤트 메모리 누수
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) { }
}
리플렉션은 런타임에 타입을 조사하고 조작한다. DI 컨테이너, 직렬화, ORM은 모두 리플렉션 기반이다. 하지만 리플렉션은 느리다 (100배). Source Generator는 컴파일 타임 메타프로그래밍으로 런타임 비용을 제거한다.
// 타입 정보 얻기
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배 느림!)
// 커스텀 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);
// 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
// 문제: 리플렉션은 느림
// 해결: 컴파일 타임 코드 생성
// 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)));
}
}
}
}
// 장점:
// - 컴파일 타임 생성 (런타임 비용 없음)
// - 타입 안전
// - 리플렉션 불필요
C#의 타입 시스템은 계속 진화한다. Nullable 참조 타입은 null 참조 예외를 컴파일 타임에 방지한다. Record는 불변 데이터를 간결하게 표현한다. 패턴 매칭은 타입 검사와 데이터 추출을 통합한다.
// 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);
}
// 전통적인 불변 클래스
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);
// 타입 패턴
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"
};
// 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"];
제네릭은 타입 안전한 재사용 코드를 만든다. 공변성/반공변성은 상속 계층에서 제네릭을 유연하게 사용한다. 제약 조건은 컴파일 타임 안전성을 보장한다. 타입 추론은 코드를 간결하게 한다.
// 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" });
// 문제: 불변성 (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");
// 제네릭 메서드
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; // 공변/반공변
C#은 IL(중간 언어)로 컴파일되고 JIT(Just-In-Time)으로 네이티브 코드가 된다. IL을 이해하면 성능 최적화와 동작 원리를 파악할 수 있다. ILSpy/dnSpy는 디컴파일로 구현을 확인한다.
// 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
}
}
// 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 = 타입 체크 + 값 복사
// 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 최적화 비활성화
}
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 지양
}
}