C# LINQ 내부 동작 원리
LINQ란 무엇인가
섹션 제목: “LINQ란 무엇인가”LINQ(Language Integrated Query)는 C# 3.0에서 도입된 기능으로, 컬렉션·데이터베이스·XML 등 다양한 데이터 소스를 일관된 문법으로 조회·변환합니다. 단순한 편의 문법이 아니라 IEnumerable<T>와 익스텐션 메서드, 람다 표현식, 표현식 트리를 조합한 정교한 시스템입니다.
쿼리 표현식 vs 메서드 체인
섹션 제목: “쿼리 표현식 vs 메서드 체인”LINQ는 두 가지 문법으로 동일한 연산을 표현합니다.
// 쿼리 표현식 (Query Expression)var result1 = from n in numbers where n > 3 select n * 2;
// 메서드 체인 (Method Chain)var result2 = numbers .Where(n => n > 3) .Select(n => n * 2);컴파일러는 쿼리 표현식을 메서드 체인으로 변환합니다. 두 형태는 완전히 동일한 IL 코드를 생성합니다.
지연 실행 (Deferred Execution)
섹션 제목: “지연 실행 (Deferred Execution)”LINQ의 가장 중요한 특성은 지연 실행입니다. 쿼리를 정의하는 시점에는 실제 연산이 일어나지 않고, 결과를 소비할 때(열거할 때) 비로소 실행됩니다.
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// 이 시점에는 아무 연산도 일어나지 않음var query = numbers.Where(n => { Console.WriteLine($"Checking {n}"); return n > 3;});
Console.WriteLine("쿼리 정의 완료");
// foreach를 만나는 순간 실제 실행foreach (var n in query){ Console.WriteLine($"Result: {n}");}출력:
쿼리 정의 완료Checking 1Checking 2Checking 3Checking 4Result: 4Checking 5Result: 5즉시 실행 연산자
섹션 제목: “즉시 실행 연산자”ToList(), ToArray(), Count(), First(), Sum() 등은 즉시 실행을 강제합니다.
// 즉시 실행: 이 시점에 전체 파이프라인 실행var list = numbers.Where(n => n > 3).ToList();IEnumerable 파이프라인
섹션 제목: “IEnumerable 파이프라인”LINQ 연산자는 IEnumerable<T>를 반환하는 익스텐션 메서드입니다. 각 연산자는 이전 시퀀스를 감싸는 래퍼 이터레이터를 반환합니다.
numbers → WhereIterator → SelectIterator → 소비자(foreach/ToList)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; // 상태 머신 기반 이터레이터 }}yield return은 컴파일러가 상태 머신 클래스로 변환합니다. 각 MoveNext() 호출마다 다음 요소를 생성합니다.
LINQ to Objects vs LINQ to SQL
섹션 제목: “LINQ to Objects vs LINQ to SQL”LINQ to Objects
섹션 제목: “LINQ to Objects”IEnumerable<T> 기반으로 동작하며 메모리 내 컬렉션을 처리합니다. 람다 표현식이 Func<T, bool> 델리게이트로 컴파일됩니다.
// 람다 → Func<int, bool> 델리게이트로 컴파일var result = numbers.Where(n => n > 3);LINQ to SQL / EF Core
섹션 제목: “LINQ to SQL / EF Core”IQueryable<T> 기반으로 동작합니다. 람다 표현식이 표현식 트리(Expression Tree) 로 컴파일되어 SQL로 변환됩니다.
// 람다 → Expression<Func<User, bool>> 표현식 트리로 컴파일IQueryable<User> query = db.Users.Where(u => u.Age > 20);// SQL: SELECT * FROM Users WHERE Age > 20IEnumerable<T>와 IQueryable<T>의 혼동은 N+1 문제를 일으킵니다.
// 위험: AsEnumerable()이 모든 데이터를 메모리로 로드 후 필터링var bad = db.Users.AsEnumerable().Where(u => u.Age > 20);
// 올바름: DB에서 WHERE 절로 필터링var good = db.Users.Where(u => u.Age > 20);표현식 트리 (Expression Tree)
섹션 제목: “표현식 트리 (Expression Tree)”표현식 트리는 코드를 데이터 구조로 표현합니다. LINQ to SQL, EF Core, 동적 쿼리 빌더가 이를 분석해 SQL을 생성합니다.
using System.Linq.Expressions;
// Expression<Func<int, bool>>: 표현식 트리Expression<Func<int, bool>> expr = n => n > 3;
// 트리 구조 분석var binary = (BinaryExpression)expr.Body;Console.WriteLine(binary.NodeType); // GreaterThanConsole.WriteLine(binary.Left); // nConsole.WriteLine(binary.Right); // 3
// 컴파일하여 델리게이트로 실행var func = expr.Compile();Console.WriteLine(func(5)); // True주요 LINQ 연산자 내부 구조
섹션 제목: “주요 LINQ 연산자 내부 구조”SelectMany — 평탄화
섹션 제목: “SelectMany — 평탄화”var words = new[] { "Hello World", "Foo Bar" };
// 각 문자열을 단어로 분리 후 평탄화var allWords = words.SelectMany(s => s.Split(' '));// 결과: ["Hello", "World", "Foo", "Bar"]GroupBy
섹션 제목: “GroupBy”var people = new[]{ new { Name = "Alice", Dept = "HR" }, new { Name = "Bob", Dept = "IT" }, new { Name = "Carol", Dept = "HR" },};
var byDept = people.GroupBy(p => p.Dept);// 내부적으로 Lookup<TKey, TElement> 딕셔너리 구조 생성foreach (var group in byDept){ Console.WriteLine($"{group.Key}: {string.Join(", ", group.Select(p => p.Name))}");}Join vs GroupJoin
섹션 제목: “Join vs GroupJoin”// Join: SQL INNER JOINvar joined = orders.Join( customers, o => o.CustomerId, c => c.Id, (o, c) => new { o.Amount, c.Name });
// GroupJoin: SQL LEFT OUTER JOINvar grouped = customers.GroupJoin( orders, c => c.Id, o => o.CustomerId, (c, orders) => new { c.Name, Orders = orders });성능 고려사항
섹션 제목: “성능 고려사항”1. 다중 열거 방지
섹션 제목: “1. 다중 열거 방지”// 나쁜 예: query를 두 번 열거하면 파이프라인이 두 번 실행됨var query = numbers.Where(ExpensiveFilter);var count = query.Count(); // 첫 번째 실행var list = query.ToList(); // 두 번째 실행
// 좋은 예: 한 번만 열거var list = numbers.Where(ExpensiveFilter).ToList();var count = list.Count;2. 조기 필터링
섹션 제목: “2. 조기 필터링”// 나쁜 예: Select 후 Where → 불필요한 객체 생성var bad = items.Select(i => new ExpensiveDTO(i)).Where(d => d.IsValid);
// 좋은 예: Where 먼저 적용var good = items.Where(i => i.IsValid).Select(i => new ExpensiveDTO(i));3. LINQ vs 전통 루프
섹션 제목: “3. LINQ vs 전통 루프”LINQ는 가독성이 뛰어나지만 각 연산자마다 델리게이트 호출 오버헤드가 있습니다. 성능 크리티컬한 경로에서는 Span<T>, for 루프, PLINQ를 고려합니다.
// PLINQ: 병렬 LINQvar result = numbers.AsParallel() .Where(n => ExpensiveFilter(n)) .ToList();커스텀 LINQ 연산자 작성
섹션 제목: “커스텀 LINQ 연산자 작성”IEnumerable<T> 익스텐션 메서드로 자신만의 LINQ 연산자를 정의할 수 있습니다.
public static class LinqExtensions{ // 배치 처리: 시퀀스를 size 크기 청크로 분할 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; }}
// 사용var chunks = Enumerable.Range(1, 10).Batch(3);// [[1,2,3], [4,5,6], [7,8,9], [10]]| 개념 | 핵심 |
|---|---|
| 지연 실행 | 쿼리 정의 ≠ 실행. 열거할 때 실행됨 |
| IEnumerable | 메모리 내 처리. 람다 → 델리게이트 |
| IQueryable | DB 처리. 람다 → 표현식 트리 → SQL |
| yield return | 상태 머신 기반 지연 이터레이터 |
| 즉시 실행 | ToList/ToArray/Count/First 등 |
LINQ는 단순 편의 기능이 아닌, C# 타입 시스템 전반을 활용한 강력한 추상화입니다. 지연 실행과 표현식 트리의 동작을 이해하면 성능 문제를 예방하고 더 표현력 있는 코드를 작성할 수 있습니다.