31. LINQ 지연 실행과 다중 열거의 함정
난이도 중내 리뷰 · C#
해설 · C#
해설 — LINQ 지연 실행과 다중 열거의 함정
난이도: 중
예측 출력
query built
Source: begin
Source: yield 1
filter 1
Source: yield 2
filter 2
Source: yield 3
filter 3
count=2
Source: begin
Source: yield 1
filter 1
first=1
핵심 개념
- 지연 실행(deferred execution):
Source().Where(...)는 아무것도 열거하지 않는다.Where는 반복자 래퍼(상태 머신)를 만들어 돌려줄 뿐이다. 실제 소스 순회는 결과를 소비할 때(foreach, Count, First, ToList 등) 비로소 시작된다. 그래서query built가 어떤 소스 로그보다 먼저 나온다. - 스트리밍 vs 버퍼링 연산자:
Where/Select는 요소를 하나씩 흘려보내는 스트리밍이다.Count(),ToList()같은 종료 연산자가 실제로 열거를 구동한다. - 다중 열거(multiple enumeration):
query는 "실행 계획"일 뿐 결과를 저장하지 않는다.Count()가 한 번,First()가 또 한 번 소스를 처음부터 다시 열거한다. 그래서Source: begin이 두 번 찍힌다.
단계별 내부 동작 (출력 근거)
Source().Where(...)— 반복자만 생성(아직 미실행).Source의 본문도 실행되지 않는다(yield 반복자는MoveNext최초 호출 때 본문이 돈다).query built출력.query.Count()→ 열거 시작.Where가 소스를MoveNext로 당긴다:Source: begin(반복자 본문 최초 진입) →Source: yield 1→filter 1(홀수, 통과) →Source: yield 2→filter 2(짝수, 탈락) →Source: yield 3→filter 3(통과). 통과 2개 →count=2.Count()는 값을 세기 위해 끝까지 열거한다.
query.First()→ 다시 처음부터 열거.Source: begin→Source: yield 1→filter 1(통과) → 첫 요소를 찾자마자First()는 단락(short-circuit) 하여 열거를 멈춘다.first=1. (yield 2/3 는 안 나옴.)
흔한 오해·함정
- 부수효과·비용의 중복 실행: 소스가 DB 쿼리/HTTP 호출/파일 읽기라면,
Count()+First()처럼 두 번 열거하는 순간 쿼리가 두 번 나간다. 로그·카운터 같은 부수효과도 중복된다. 성능·정합성 버그의 흔한 원인. - 지연 실행 × 클로저 캡처: 루프 변수를 캡처한 쿼리를 나중에 열거하면 캡처 시점이 아니라 열거 시점의
변수 값을 본다(특히
for루프에서). C# 5+ 의foreach는 반복마다 새 변수라 이 함정이 완화됐다. - 원본 컬렉션 변경 후 재열거: 지연 쿼리는 열거할 때 원본을 다시 읽으므로, 그 사이 원본이 바뀌면 두
열거의 결과가 달라진다.
List를 열거 중 수정하면InvalidOperationException. - 한 번만 소비 가능한 소스: 네트워크 스트림 등 재열거가 불가능한 소스에 다중 열거를 하면 빈 결과/예외.
수정
결과를 한 번만 계산해 구체화(materialize) 한다.
var results = query.ToList(); // 여기서 딱 한 번 열거
int count = results.Count; // 재열거 없음
int first = results[0]; // 재열거 없음
또는 필요한 것을 한 번의 열거로 계산(예: foreach 한 번, 또는 Aggregate). 소스가 비싸면 반드시
ToList()/ToArray() 로 캐시한다.
면접 포인트
- 지연 실행이 무엇이고,
query변수가 "결과"가 아니라 "실행 계획"인 이유. Count()/First()가 각각 소스를 어떻게 열거/단락하는가, 다중 열거가 왜 비싼가.- ToList/ToArray 로 구체화하는 시점과 비용, 클로저 캡처·원본 변경과의 상호작용.