IEnumerable<T>
1. 개요
1. 개요
IEnumerable<T>는 .NET 프레임워크의 System.Collections.Generic 네임스페이스에 정의된 제네릭 인터페이스이다. 이 인터페이스는 컬렉션을 단순하게 반복할 수 있도록 지원하는 역할을 하며, 컬렉션의 요소들을 순차적으로 접근할 수 있는 표준화된 방법을 제공한다.
가장 일반적인 사용법은 foreach 문을 이용한 컬렉션 순회이다. IEnumerable<T>를 구현하는 모든 객체는 foreach 루프를 통해 안전하고 간편하게 요소를 열거할 수 있다. 또한 LINQ 쿼리의 기본적인 대상이 되며, LINQ의 다양한 확장 메서드(Where, Select, OrderBy 등)는 대부분 IEnumerable<T>를 반환하거나 이를 기반으로 동작한다.
주요 메서드로는 컬렉션의 요소를 순회하는 데 사용되는 열거자를 반환하는 GetEnumerator() 메서드가 있다. 이 인터페이스는 컬렉션의 데이터 소스를 나타내는 가장 기본적인 수준의 추상화로, 데이터의 저장 방식이나 구조를 노출하지 않고 순회 기능만을 정의한다는 특징을 가진다. 따라서 배열, 리스트, 사전과 같은 내장 컬렉션 타입부터 사용자 정의 컬렉션에 이르기까지 광범위하게 구현되어 활용된다.
2. 정의와 특징
2. 정의와 특징
2.1. 인터페이스로서의 역할
2.1. 인터페이스로서의 역할
IEnumerable<T>는 .NET Framework의 System.Collections.Generic 네임스페이스에 정의된 제네릭 인터페이스이다. 이 인터페이스의 핵심 역할은 특정 컬렉션에 포함된 요소들을 순차적으로 접근할 수 있는 표준화된 방법을 제공하는 것이다. 구체적으로, IEnumerable<T>를 구현하는 객체는 GetEnumerator() 메서드를 통해 열거자(IEnumerator<T>)를 반환하며, 이 열거자는 컬렉션의 현재 위치를 추적하고 다음 요소로 이동하는 기능을 제공한다.
이러한 인터페이스의 역할 덕분에 C#의 foreach 문은 IEnumerable<T>를 구현하는 모든 컬렉션을 일관된 방식으로 순회할 수 있다. foreach 루프는 내부적으로 GetEnumerator()를 호출하여 열거자를 얻고, MoveNext()와 Current 속성을 사용하여 컬렉션의 끝까지 요소를 차례대로 읽어낸다. 이는 배열, List<T>, Dictionary<TKey, TValue> 등 서로 내부 구조가 다른 다양한 컬렉션 타입에 대해 동일한 순회 구문을 사용할 수 있게 하는 다형성의 대표적인 예시이다.
또한, IEnumerable<T>는 LINQ 기술의 근간을 이룬다. LINQ의 수많은 확장 메서드들은 IEnumerable<T>를 확장하여 정의되며, 필터링, 정렬, 그룹화, 변환 등의 복잡한 데이터 처리 작업을 선언적이고 간결한 쿼리 형태로 수행할 수 있게 해준다. 따라서 IEnumerable<T>를 구현한다는 것은 단순한 순회 기능뿐만 아니라, 강력한 LINQ 쿼리의 대상이 될 수 있는 자격을 부여받는 것을 의미한다.
결국 IEnumerable<T> 인터페이스는 .NET의 컬렉션 처리 패러다임에서 가장 기본적이고 광범위하게 사용되는 계약이다. 이는 컬렉션의 구체적인 구현 세부사항을 감추고, '순회 가능하다'는 최소한의 공통 기능을 노출함으로써 높은 수준의 추상화와 코드 재사용성을 가능하게 한다.
2.2. 지연 실행(Lazy Evaluation)
2.2. 지연 실행(Lazy Evaluation)
IEnumerable<T>의 핵심 특징 중 하나는 지연 실행(Lazy Evaluation)을 지원한다는 점이다. 이는 LINQ 쿼리나 yield return을 통해 생성된 시퀀스가 실제로 값을 요청받기 전까지 실행을 미루는 방식을 의미한다. 쿼리가 정의되는 시점에는 데이터를 가져오지 않고, foreach 루프를 돌리거나 ToList(), ToArray() 같은 메서드로 구체화할 때 비로소 계산이 수행된다.
이러한 지연 실행 방식은 성능과 메모리 효율성에 큰 장점을 제공한다. 예를 들어, 방대한 데이터 소스에서 조건에 맞는 첫 몇 개의 항목만 필요로 할 때, 전체 컬렉션을 메모리에 올리지 않고 필요한 시점에 필요한 만큼만 처리할 수 있다. 또한 여러 LINQ 연산자(예: Where, Select)를 체이닝해도 각 단계가 즉시 실행되지 않고 하나의 실행 계획으로 구성되어 최종 열거 시 한 번에 효율적으로 평가된다.
하지만 지연 실행은 주의를 요하는 측면도 있다. 데이터 소스가 데이터베이스나 파일 시스템 같은 외부 자원일 경우, 열거가 이루어질 때마다 쿼리가 재실행되거나 연결이 반복적으로 발생할 수 있다. 이는 다중 열거 문제를 일으켜 성능 저하의 원인이 될 수 있으며, 데이터 소스의 상태가 변하면 동일한 쿼리를 두 번 실행해도 서로 다른 결과를 반환할 수 있다. 따라서 외부 데이터를 다루거나 결과를 재사용해야 할 때는 ToList() 등을 통해 즉시 평가하여 결과를 메모리에 고정시키는 것이 일반적이다.
3. 주요 메서드
3. 주요 메서드
3.1. LINQ 확장 메서드
3.1. LINQ 확장 메서드
IEnumerable<T>는 LINQ의 기반이 되는 인터페이스로, LINQ의 강력한 쿼리 기능은 대부분 IEnumerable<T>를 위한 확장 메서드 형태로 제공된다. 이러한 확장 메서드들은 System.Linq 네임스페이스에 정의되어 있으며, IEnumerable<T>를 구현하는 모든 컬렉션에서 사용할 수 있다.
주요 LINQ 확장 메서드는 크게 필터링, 정렬, 그룹화, 집계, 변환 등의 카테고리로 나눌 수 있다. 대표적인 메서드로는 조건에 맞는 요소를 필터링하는 Where, 특정 기준으로 정렬하는 OrderBy와 OrderByDescending, 요소를 그룹으로 묶는 GroupBy, 컬렉션의 요소 수를 세는 Count, 모든 요소의 합을 구하는 Sum, 컬렉션을 다른 형태로 변환하는 Select 등이 있다. 이러한 메서드들은 체이닝을 통해 연결하여 복잡한 쿼리를 간결하게 표현할 수 있게 해준다.
메서드 카테고리 | 대표 메서드 | 설명 |
|---|---|---|
필터링 |
| 조건을 만족하는 요소만 선택 |
정렬 |
| 특정 키를 기준으로 오름차순 정렬 |
그룹화 |
| 지정된 키에 따라 요소를 그룹화 |
집계 |
| 요소의 개수, 합계, 평균 등을 계산 |
변환 |
| 각 요소를 새로운 형태로 변환 |
요소 접근 |
| 첫 번째, 마지막, 특정 위치의 요소 반환 |
이러한 LINQ 확장 메서드들은 지연 실행 모델을 따르는 경우가 많다. 즉, 메서드를 호출하는 시점에는 실제 연산이 실행되지 않고, 결과가 필요한 시점(예: foreach 루프나 ToList 메서드 호출 시)에 쿼리가 평가된다. 이는 불필요한 계산을 줄이고 성능을 최적화하는 데 도움을 준다.
3.2. 열거(Enumeration) 관련 메서드
3.2. 열거(Enumeration) 관련 메서드
IEnumerable<T> 인터페이스는 컬렉션의 요소를 순차적으로 접근하기 위한 기본적인 메커니즘을 제공한다. 이 인터페이스의 핵심은 GetEnumerator 메서드로, 이 메서드는 컬렉션의 요소들을 순회할 수 있는 열거자 객체를 반환한다. 반환된 IEnumerator<T> 객체는 Current 속성과 MoveNext, Reset 메서드를 통해 컬렉션 내의 각 요소에 접근하고 다음 위치로 이동하는 기능을 담당한다. C#의 foreach 반복문은 내부적으로 이 GetEnumerator 메서드를 호출하여 반환된 열거자를 사용해 컬렉션을 순회한다.
IEnumerable<T>를 직접 구현하거나 확장할 때는 GetEnumerator 메서드 외에도 IEnumerable(제네릭이 아닌 버전)의 GetEnumerator 메서드를 명시적으로 구현해야 하는 경우가 있다. 이는 이전 버전과의 호환성을 유지하기 위함이다. 또한, 반복자 패턴을 쉽게 구현할 수 있도록 C#은 yield return과 yield break 키워드를 제공한다. 개발자는 복잡한 IEnumerator<T> 클래스를 직접 작성하지 않고도 이 키워드를 사용해 간결하게 열거 로직을 정의할 수 있다.
IEnumerable<T> 인터페이스 자체는 단순히 열거 기능만을 정의하지만, LINQ의 다양한 확장 메서드들이 이 인터페이스를 기반으로 동작한다. 예를 들어, Where, Select, OrderBy 같은 메서드들은 IEnumerable<T>를 반환하며, 이는 지연 실행 모델의 기초가 된다. 컬렉션의 실제 순회와 데이터 변환은 GetEnumerator를 호출하여 열거가 시작되는 시점, 즉 foreach 루프나 ToList 같은 메서드가 호출될 때 비로소 이루어진다.
4. 사용 예시
4. 사용 예시
4.1. 컬렉션 순회
4.1. 컬렉션 순회
IEnumerable<T> 인터페이스의 가장 기본적이고 일반적인 사용법은 컬렉션의 요소들을 순차적으로 접근하는 것이다. C#의 foreach 문은 내부적으로 IEnumerable<T>.GetEnumerator 메서드를 호출하여 반환된 열거자를 사용해 컬렉션을 순회한다. 이 방식을 통해 사용자는 컬렉션의 내부 구조(예: 배열, 연결 리스트, 사전)를 알 필요 없이 일관된 방법으로 모든 요소에 접근할 수 있다.
IEnumerable<T>를 구현하는 모든 객체는 foreach 루프에 사용될 수 있다. 이는 닷넷 프레임워크의 대부분의 표준 컬렉션 클래스들(예: List<T>, Dictionary<TKey, TValue>, HashSet<T>)이 해당 인터페이스를 구현하기 때문이다. 사용자는 컬렉션의 정확한 타입 대신 IEnumerable<T> 타입으로 참조하여 더 유연하고 일반화된 코드를 작성할 수 있다.
순회 과정은 GetEnumerator 메서드가 반환한 열거자 객체의 MoveNext 메서드와 Current 속성을 통해 진행된다. foreach 루프는 매 반복마다 MoveNext를 호출하여 다음 요소가 있는지 확인하고, 있다면 Current 속성을 통해 현재 요소 값을 가져온다. 이 메커니즘은 컬렉션의 종류에 관계없이 동일하게 작동한다.
이러한 표준화된 순회 방식은 알고리즘과 데이터 구조를 분리시켜 준다. 즉, 데이터를 저장하는 방법과 데이터를 처리하는 로직을 독립적으로 개발할 수 있게 한다. 이는 소프트웨어 설계의 유연성과 코드 재사용성을 크게 향상시키는 핵심 원리 중 하나이다.
4.2. LINQ 쿼리
4.2. LINQ 쿼리
IEnumerable<T>는 LINQ 쿼리의 기반이 되는 핵심 인터페이스이다. LINQ 쿼리 식이나 메서드 구문을 사용하면, 데이터 소스가 IEnumerable<T>를 구현하고 있기 때문에 필터링, 정렬, 그룹화, 변환 등의 다양한 작업을 선언적으로 수행할 수 있다. 이는 데이터베이스 쿼리와 유사한 방식으로 메모리 내 컬렉션을 처리할 수 있게 해준다.
LINQ 쿼리는 IEnumerable<T>를 반환하며, 이는 지연 실행의 특성을 가진다. 즉, 쿼리를 정의하는 시점에는 실제 데이터 처리가 이루어지지 않고, 결과를 실제로 필요로 하는 시점(예: foreach 루프나 ToList() 메서드 호출 시)에 실행된다. 이를 통해 불필요한 계산을 줄이고 성능을 최적화할 수 있다. 예를 들어, Where()나 Select() 같은 연산자는 호출 즉시 실행되지 않고, 열거가 시작될 때 비로소 실행 흐름이 구성된다.
LINQ 쿼리는 IEnumerable<T>를 통해 배열, 리스트, 사전 등 다양한 .NET 표준 컬렉션뿐만 아니라, 사용자가 정의한 커스텀 컬렉션에도 동일한 방식으로 적용할 수 있다. 이는 일관된 쿼리 경험을 제공하며, 데이터 소스의 종류에 상관없이 통일된 문법으로 데이터를 가공할 수 있게 한다. 결과적으로 코드의 가독성과 유지보수성이 크게 향상된다.
5. IEnumerable<T>와 다른 인터페이스
5. IEnumerable<T>와 다른 인터페이스
5.1. ICollection<T>, IList<T>와의 관계
5.1. ICollection<T>, IList<T>와의 관계
IEnumerable<T>는 컬렉션을 순회하는 가장 기본적인 기능을 정의하는 인터페이스이다. 이는 컬렉션 계층 구조의 최상위에 위치하며, ICollection<T>와 IList<T> 인터페이스는 모두 IEnumerable<T>를 상속받는다. 즉, 모든 ICollection<T>와 IList<T>는 IEnumerable<T>의 기능을 포함하고 있으므로, foreach 문이나 LINQ 쿼리를 사용하여 순회할 수 있다.
ICollection<T> 인터페이스는 IEnumerable<T>의 기본적인 열거 기능에 더해, 컬렉션의 요소 수를 확인하거나(Count), 요소를 추가/제거(Add, Remove, Clear)하는 등 컬렉션을 수정하는 기능을 추가로 정의한다. List<T>나 HashSet<T>와 같은 대부분의 일반적인 제네릭 컬렉션은 ICollection<T>를 구현한다. 반면, IList<T>는 ICollection<T>를 다시 상속받아 인덱스를 통한 요소 접근(예: list[0]) 및 삽입/삭제 기능을 제공하며, 배열이나 List<T>가 이를 구현하는 대표적인 예이다.
따라서, 특정 메서드가 IEnumerable<T> 타입의 매개변수를 요구한다면, ICollection<T>나 IList<T>를 구현하는 모든 객체를 전달할 수 있다. 이는 다형성의 장점을 보여준다. 그러나 반대로, IEnumerable<T> 참조를 통해 제공되는 기능은 순회만 가능하며, 요소의 개수를 미리 알 수 없거나(Count 속성 없음), 컬렉션을 직접 수정할 수 없다는 제약이 따른다.
5.2. IQueryable<T>와의 차이점
5.2. IQueryable<T>와의 차이점
IEnumerable<T>는 메모리 내에 존재하는 컬렉션을 순회하는 데 사용되는 반면, IQueryable<T>는 주로 데이터베이스나 원격 서비스와 같은 외부 데이터 소스를 대상으로 하는 쿼리를 표현하기 위해 설계된 인터페이스이다. 이 둘의 가장 큰 차이는 쿼리가 실행되는 위치와 시점에 있다. IEnumerable<T>를 사용하는 LINQ to Objects 쿼리는 모든 데이터가 로컬 메모리에 로드된 후 클라이언트 측(.NET 런타임)에서 실행된다. 반면, IQueryable<T>를 사용하는 LINQ to SQL이나 Entity Framework 쿼리는 식 트리로 변환되어 서버(예: SQL Server)로 전달되고, 최종적으로는 SQL 문으로 변환되어 데이터베이스 서버에서 실행된다. 이는 네트워크를 통해 전송되는 데이터 양을 최소화하고 서버의 처리 능력을 활용하여 성능을 최적화하는 데 핵심적이다.
IEnumerable<T>의 확장 메서드는 대리자(Func<T, bool> 등)를 인수로 받는 반면, IQueryable<T>의 확장 메서드는 식 트리(Expression<Func<T, bool>> 등)를 인수로 받는다. 이 식 트리는 쿼리의 논리적 구조를 런타임에 검사할 수 있는 데이터 형태로 보관하며, 이를 통해 쿼리 공급자는 이를 특정 데이터 소스(예: 관계형 데이터베이스)의 네이티브 쿼리 언어로 번역할 수 있다. 결과적으로, IQueryable<T> 쿼리는 실제로 열거(예: foreach 루프 또는 ToList() 호출)가 발생하기 전까지 실행되지 않는 지연 실행의 특성을 가지며, 이는 IEnumerable<T>의 지연 실행과 유사해 보이지만 쿼리 변환 및 실행이 클라이언트가 아닌 서버 측에서 이루어진다는 점에서 근본적으로 다르다.
따라서, 개발자는 데이터 소스의 위치와 특성에 따라 적절한 인터페이스를 선택해야 한다. 메모리 내 컬렉션(예: List<T>, 배열)을 쿼리할 때는 IEnumerable<T>를 사용하는 것이 일반적이다. 반면, Entity Framework Core를 통해 관계형 데이터베이스를 조회할 때 반환되는 DbSet<T>는 IQueryable<T>를 구현하므로, 필터링(Where), 정렬(OrderBy) 등의 작업을 클라이언트 코드에 추가하면 이는 서버 측 SQL 쿼리의 WHERE, ORDER BY 절로 변환되어 효율적으로 실행된다. IQueryable<T>를 IEnumerable<T>로 변환(예: AsEnumerable() 호출)하면 이후의 모든 쿼리 연산은 클라이언트 측 메모리에서 실행되므로 주의가 필요하다.
6. 구현 방법
6. 구현 방법
6.1. yield return을 이용한 구현
6.1. yield return을 이용한 구현
yield return 키워드는 IEnumerable<T> 인터페이스를 구현하는 가장 간편하고 효율적인 방법을 제공한다. 이 키워드를 사용하면 명시적으로 상태 머신을 구성하지 않고도 컬렉션의 요소를 하나씩 반환하는 반복자 메서드를 쉽게 작성할 수 있다. 컴파일러는 yield return이 사용된 메서드를 자동으로 상태 머신을 구현하는 클래스로 변환하여, 메서드의 실행 상태(예: 현재 반환 위치)를 보존하고 관리한다.
yield return을 이용한 구현은 일반적으로 반복자 메서드라는 특별한 형태로 이루어진다. 이 메서드는 반환 타입으로 IEnumerable<T>를 지정하고, 메서드 본문 내에서 yield return 문을 사용해 시퀀스의 각 요소를 순차적으로 생산한다. yield break 문은 시퀀스의 생성을 중단하는 데 사용된다. 이 방식의 핵심 장점은 모든 요소를 한 번에 메모리에 담을 필요 없이 필요할 때마다(on-demand) 요소를 생성하여 반환하는 지연 실행이 가능하다는 점이다.
구현 예시로, 특정 범위의 숫자나 조건을 만족하는 데이터를 생성하는 시퀀스를 만들 때 유용하다. 예를 들어, 1부터 N까지의 숫자를 생성하거나, 파일에서 한 줄씩 읽어 반환하는 로직을 yield return을 사용해 간결하게 표현할 수 있다. 이는 대용량 데이터를 처리하거나 무한 시퀀스를 모델링할 때 메모리 효율성을 크게 높여준다.
yield return으로 생성된 IEnumerable<T> 시퀀스는 foreach 루프나 LINQ 연산자로 소비될 수 있다. 단, 이 방식으로 구현된 반복자는 기본적으로 읽기 전용이며, Reset 메서드가 제대로 구현되지 않는다는 점을 주의해야 한다. 또한, 생성된 상태 머신은 시퀀스를 열거하는 동안 특정 리소스(예: 파일 핸들)를 사용한다면, IDisposable 인터페이스를 통해 적절한 정리가 이루어지도록 설계되어 있다.
6.2. 커스텀 컬렉션 클래스 구현
6.2. 커스텀 컬렉션 클래스 구현
커스텀 컬렉션 클래스를 구현할 때는 IEnumerable<T> 인터페이스를 직접 구현하여 해당 클래스의 인스턴스가 foreach 문이나 LINQ 쿼리에서 사용될 수 있도록 한다. 일반적으로는 GetEnumerator 메서드를 구현하는 것이 핵심이다. 이 메서드는 컬렉션의 요소들을 순차적으로 접근할 수 있는 열거자를 반환해야 한다.
커스텀 클래스에서 IEnumerator<T>를 구현하는 한 가지 방법은 별도의 중첩 클래스를 정의하는 것이다. 이 중첩 클래스는 컬렉션의 현재 위치를 추적하고 Current 속성과 MoveNext 메서드, Reset 메서드를 구현한다. 이 방식은 비교적 많은 코드를 작성해야 하지만, 순회 로직을 완전히 제어할 수 있다는 장점이 있다.
보다 간편한 방법은 C#의 yield return 구문을 사용하는 것이다. GetEnumerator 메서드 내에서 yield return을 사용하면 컴파일러가 자동으로 적절한 상태 머신을 생성해 열거자를 만들어준다. 이 방법은 코드를 매우 간결하게 작성할 수 있게 해주며, 대부분의 커스텀 시나리오에 적합하다.
커스텀 컬렉션 클래스를 구현할 때는 ICollection<T>이나 IList<T> 같은 더 풍부한 기능의 인터페이스를 함께 구현할지 고려해야 한다. 단순한 순회만 필요하다면 IEnumerable<T>만으로 충분하지만, 컬렉션에 요소를 추가하거나 삭제하는 기능, 인덱스로의 접근 기능 등이 필요하다면 해당 인터페이스들을 추가로 구현한다.
7. 주의사항
7. 주의사항
7.1. 다중 열거 문제
7.1. 다중 열거 문제
IEnumerable<T>를 사용할 때 주의해야 할 점 중 하나는 다중 열거 문제이다. 이는 동일한 IEnumerable<T> 시퀀스를 여러 번 열거할 때 발생하는 성능 저하나 예상치 못한 동작을 의미한다.
IEnumerable<T>는 데이터 소스 자체가 아니라 데이터 소스를 순회할 수 있는 열거자를 제공하는 인터페이스이다. 특히 LINQ의 지연 실행 쿼리 결과로 반환되는 경우, 쿼리를 열거할 때마다 데이터 소스에 접근하여 연산을 다시 수행한다. 예를 들어, 데이터베이스 쿼리나 파일 읽기, 복잡한 변환 로직이 포함된 시퀀스를 반복적으로 열거하면, 동일한 작업이 여러 번 실행되어 응용 프로그램의 성능에 심각한 영향을 미칠 수 있다. 또한, 데이터 소스가 매번 변하는 경우(예: 실시간 센서 데이터) 각 열거 시점마다 다른 결과를 반환할 수 있어 논리적 오류를 일으킬 위험이 있다.
이 문제를 해결하는 일반적인 방법은 시퀀스를 한 번만 열거하여 메모리 내 컬렉션으로 구체화하는 것이다. ToList()나 ToArray() 같은 LINQ 메서드를 사용하면 쿼리 결과를 List<T>나 배열 같은 구체적인 컬렉션으로 변환하여 저장할 수 있다. 이후에는 이 컬렉션을 대상으로 여러 번 안전하게 열거 작업을 수행할 수 있다. 다만, 이 방식은 모든 데이터를 한꺼번에 메모리에 로드해야 하므로 데이터 양이 매우 클 경우에는 적합하지 않을 수 있다. 따라서 데이터의 크기, 변동성, 그리고 열거 빈도를 종합적으로 고려하여 IEnumerable<T>를 단일 열거로 제한할지, 아니면 구체화하여 다중 열거를 허용할지 신중히 결정해야 한다.
7.2. 성능 고려사항
7.2. 성능 고려사항
IEnumerable<T>를 사용할 때는 성능에 주의해야 한다. 특히 대규모 데이터를 처리하거나 반복적인 열거 작업이 필요한 경우 성능 저하가 발생할 수 있다. 가장 흔한 문제는 다중 열거로, 동일한 IEnumerable<T> 시퀀스를 여러 번 열거하면 매번 데이터 소스로부터 데이터를 다시 가져와야 하므로 불필요한 계산이나 데이터베이스 쿼리가 반복 실행될 수 있다. 이를 방지하기 위해 시퀀스를 한 번만 열거하거나, 결과를 메모리에 캐싱하는 ToList()나 ToArray() 메서드를 사용하는 것이 좋다.
또한, IEnumerable<T>의 지연 실행 특성은 복잡한 LINQ 쿼리를 체이닝할 때 예상치 못한 성능 문제를 야기할 수 있다. 각 연산이 실제 열거가 발생할 때마다 순차적으로 적용되므로, 쿼리 체인이 길수록 단일 항목을 처리하는 데 걸리는 시간이 증가한다. 데이터 소스가 메모리 내 컬렉션이 아닌 데이터베이스나 네트워크 스트림인 경우, 이러한 지연 실행으로 인한 네트워크 지연이나 입출력 오버헤드가 더욱 두드러질 수 있다.
성능 최적화를 위해서는 시나리오에 맞는 적절한 컬렉션 인터페이스를 선택하는 것이 중요하다. 단순 순회만 필요하면 IEnumerable<T>로 충분하지만, 빈번한 요소 접근이나 컬렉션 조작이 필요하다면 인덱스를 지원하는 IList<T>나 ICollection<T>를 고려해야 한다. 마지막으로, LINQ to Objects와 LINQ to SQL 또는 Entity Framework의 IQueryable<T>를 구분하여 사용해야 하며, 후자의 경우 쿼리가 데이터베이스 서버에서 최적화되어 실행되도록 해야 불필요한 데이터 전송을 막을 수 있다.
