← 문제로

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 이 두 번 찍힌다.

단계별 내부 동작 (출력 근거)

  1. Source().Where(...) — 반복자만 생성(아직 미실행). Source 의 본문도 실행되지 않는다(yield 반복자는 MoveNext 최초 호출 때 본문이 돈다).
  2. query built 출력.
  3. query.Count() → 열거 시작. Where 가 소스를 MoveNext 로 당긴다:
    • Source: begin(반복자 본문 최초 진입) → Source: yield 1filter 1(홀수, 통과) → Source: yield 2filter 2(짝수, 탈락) → Source: yield 3filter 3(통과). 통과 2개 → count=2.
    • Count() 는 값을 세기 위해 끝까지 열거한다.
  4. query.First()다시 처음부터 열거. Source: beginSource: yield 1filter 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 로 구체화하는 시점과 비용, 클로저 캡처·원본 변경과의 상호작용.