빠른 비교
// IEnumerable — 지연 실행, 열거할 때 계산
IEnumerable<Player> query = players
.Where(p => p.Score >= 1000)
.OrderByDescending(p => p.Score);
// List — 지금 계산하고 결과를 메모리에 고정
List<Player> snapshot = query.ToList();
// snapshot은 이후 players가 변해도 영향 없음
players.Clear();
Console.WriteLine(snapshot.Count); // 원래 결과 유지갈리는 기준
IEnumerable<T> — 반복 가능한 흐름
IEnumerable<T> 는 "지금 다 준비된 컬렉션"이 아니라 "열거 가능한 흐름" 입니다. foreach나 ToList() 같은 열거 트리거가 호출될 때 비로소 계산됩니다.
IEnumerable<int> lazy = Enumerable.Range(1, 1_000_000)
.Where(n => n % 2 == 0);
// 아직 아무것도 계산되지 않음
// 첫 번째 순회 — 계산 시작
foreach (int n in lazy.Take(5))
Console.Write(n); // 2, 4, 6, 8, 10
// 두 번째 순회 — 처음부터 다시 계산
int count = lazy.Count(); // ⚠️ 전체 재계산IEnumerable<T>를 여러 번 순회하면 매번 처음부터 재계산됩니다. 이것이 가장 흔한 성능 버그입니다.
materialization 기준 — 언제 ToList()를 쓸까
ToList() / ToArray()는 그 시점의 결과를 전부 계산해 메모리에 고정(materialize) 합니다.
// ✅ materialization이 필요한 경우
// 1. 결과를 여러 번 재사용
var top = players.Where(p => p.IsActive).ToList();
Console.WriteLine(top.Count); // 한 번 계산
Report(top); // 재사용 — 재계산 없음
// 2. 원본 컬렉션이 변경될 수 있음
var snapshot = db.Users.Where(u => u.IsActive).ToList();
db.Users.Clear(); // snapshot은 영향 없음
// 3. 지연 실행이 부작용을 일으킬 수 있음
var results = GetExpensiveItems().ToList(); // DB 호출 1번만
// ❌ 불필요한 materialization
// 한 번만 순회하고 끝나면 ToList() 불필요
foreach (var p in players.Where(p => p.IsActive)) // ToList() 없이도 OK
Process(p);yield return — 직접 지연 시퀀스 만들기
yield return 은 반복자 메서드를 만드는 키워드입니다. 요소를 하나씩 생성하고 호출자가 다음을 요청할 때까지 실행을 멈춥니다.
// yield return — 지연 생성
IEnumerable<int> EvenNumbers(int max)
{
for (int i = 0; i <= max; i += 2)
yield return i; // 하나 반환 후 대기
}
// 처음 3개만 필요하면 나머지는 생성되지 않음
foreach (int n in EvenNumbers(1_000_000).Take(3))
Console.Write(n); // 0, 2, 4 — 나머지는 계산 안 됨선택 기준
| 선택 | 의미 | 언제 사용 |
|---|---|---|
IEnumerable<T> | 지연 실행, 열거 시 계산 | 한 번 순회, 파이프라인 |
List<T> via ToList() | 즉시 계산, 스냅샷 | 재사용, 원본 변경 가능, 비용 큰 쿼리 |
ToArray() | 즉시 계산, 배열 | 크기 고정, 외부 API 전달 |
yield return | 지연 생성 | 무한 시퀀스, 계산 비용 분산 |
주의할 점
IEnumerable<T>를 여러 번 순회하면 매번 재계산됩니다. LINQ 체인 안에 DB 쿼리나 HTTP 호출이 있다면 특히 위험합니다. Count(), Any(), ToList()를 따로 호출하면 쿼리가 그 횟수만큼 실행됩니다.
반대로 모든 곳에 ToList()를 추가하는 습관도 문제입니다. 큰 컬렉션을 조기에 메모리에 올리면 불필요한 메모리 사용과 초기 처리 비용이 생깁니다. "이 결과가 두 번 이상 쓰이는가?"를 기준으로 결정하세요.
참고 링크
2 sources