CodeGym /행동 /C# SELF /지연 실행

지연 실행

C# SELF
레벨 34 , 레슨 0
사용 가능

1. 소개

상상해봐: 너가 LINQ 쿼리를 길고 멋지게 짰어. 그리고 "와, 이제 다 처리되겠지!"라고 생각하지. 근데 아무 일도 안 일어나. 요소를 세거나 결과를 배열로 바꾸기 전까지는 말이야. 이건 버그가 아니라, 기능이야.

.NET LINQ는 지연 실행을 써: 쿼리는 네가 진짜 데이터를 꺼내려고 할 때까지 실행되지 않아. 이건 게으른 웨이터랑 비슷해 — 주문은 받았지만, 네가 "음식 가져와!"라고 말하기 전까지는 주방에 안 가.

이런 동작은 lazy evaluation(Lazy Evaluation)이라고 불리고, LINQ가 엄청 크거나 끝이 없거나 리소스를 많이 쓰는 데이터 소스에서 특히 효율적으로 동작하게 해줘.

지연 실행은 LINQ 쿼리가 정의된 직후에 바로 실행되지 않는다는 뜻이야. 네가 실제로 컬렉션에서 데이터를 꺼내거나 볼 때만 동작을 시작해.

예시-설명


var numbers = new List<int> { 1, 2, 3, 4, 5 };

// LINQ 쿼리
var query = numbers.Where(n =>{
    Console.WriteLine($"검사 중 {n}");
    return n % 2 == 0;
});
Console.WriteLine("쿼리만 정의했고, 숫자는 아직 검사 안 했어!");

// 이제서야 순회가 시작됨!
foreach (var n in query)
{
    Console.WriteLine($"짝수 찾음: {n}");
}

어떻게 될까?
이 코드를 실행하면 foreach 루프 전에는 콘솔에 아무것도 안 찍혀 — Where 안의 조건도 실행 안 돼. 네가 요소를 순회하기 시작하면(예를 들어 foreach로), 그때부터 쿼리가 실행돼.

이게 바로 deferred execution — 요청하기 전엔 아무도 아무것도 안 해!

2. 왜 지연 실행이 필요할까?

지연 실행은 코드를 더 깔끔하게 만들 뿐만 아니라, 진짜 효율적으로 만들어줘. 어차피 결과가 필요 없을 수도 있는데, 미리 뭔가를 계산할 필요가 있을까? 특히 컬렉션이 엄청 크거나, 데이터가 계속 들어오는 스트림일 때 — 끝이 없는 소스라고 생각해봐. 이걸 한 번에 다 메모리에 올리는 건 말도 안 돼.

게다가, 지연 실행 덕분에 쿼리를 자유롭게 조합하고 확장할 수 있어. LINQ 연산자를 여러 개 체이닝해도, 바로 실행되지 않으니까 걱정할 필요 없어. 진짜로 데이터를 꺼낼 때만 실행돼 — 그 전엔 절대 안 돼.

비유

지연 실행은 핸드폰에 있는 장보기 리스트랑 비슷해: 물건을 추가하거나 수정하는 건 얼마든지 할 수 있지만, 진짜로 뭔가를 사러 갈 때만(그때서야 리스트를 쓰지) 마트에 가는 거야.

3. 내부적으로 어떻게 동작할까?

IEnumerable<T>을 반환하는 LINQ 쿼리는 보통 이터레이터(yield return)나 특별한 lazy 구조로 구현돼. 네가 컬렉션을 순회할 때마다(예를 들어 foreachToList() 호출) 쿼리가 새로 실행돼.

중요한 관찰

쿼리를 정의하고 실행하기 전 사이에 원본 컬렉션을 바꾸면, 새로 추가되거나 바뀐 데이터도 결과에 포함돼.

예시:


var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);
numbers.Add(4); // 새로운 숫자 추가
foreach (var n in query)
{
    Console.WriteLine(n); // 2, 3, 4 출력
}

다이어그램: LINQ 쿼리는 언제 실행될까?


flowchart TD
    A[LINQ 쿼리 정의] --> B{쿼리 실행?}
    B -- 아니 --> C[대기]
    B -- 예 (예: foreach, ToList) --> D[쿼리 실행]
    D --> E[결과 또는 동작]
deferred execution 다이어그램: 쿼리는 "진짜로 쓸 때까지" 기다림

4. LINQ의 "게으름"이 중요한 예시들

게으른 필터링


var bigNumbers = Enumerable.Range(1, 1_000_000_000)
    .Where(n => n % 123_456 == 0);

foreach (var n in bigNumbers.Take(5))
{
    Console.WriteLine(n);
}

여기서 무슨 일이?
쿼리는 이론상 10억 개짜리 컬렉션을 만들지만, 실제로는 5개 숫자만 필터링해서 반환해! 나머지는 계산도 안 되고, 메모리도 안 써.

중첩 쿼리와 트랜잭션

예를 들어, 주문과 상품 리스트가 있고, 특정 상품이 들어있는 첫 5개 주문만 뽑고 싶다고 해보자.


var orders = GetBigOrderList(); // 여기엔 수천 개 주문이 있다고 치자

var filtered = orders
    .Where(order => order.Products.Any(p => p.Name == "커피"))
    .Take(5);

foreach(var o in filtered)
{
    Console.WriteLine(o.Id);
}

LINQ는 5개 찾으면 나머지 주문은 아예 안 봐!

5. 예상 못한 상황들

지연 실행 때문에 예상치 못한 상황이 생길 수 있는 경우:

쿼리 정의 후 데이터 소스 변경

아까처럼, 쿼리 정의와 실제 실행 사이에 원본 컬렉션을 바꾸면, 쿼리는 새 데이터를 볼 수 있어.

한 쿼리를 여러 번 순회

LINQ 쿼리는 순회할 때마다 새로 실행돼.


var query = numbers.Where(n => {
    Console.WriteLine($"검사 중 {n}");
    return n % 2 == 0;
});

foreach(var n in query) {} // 한 번 순회
foreach(var n in query) {} // 두 번째 순회 — 다시 다 계산함

결과를 반복해서 쓰고 싶으면, materialize(즉, .ToList().ToArray()로 바꿔) 해둬.

6. LINQ 쿼리가 지연되지 않는 경우?

모든 LINQ 연산이 지연되는 건 아니야. 어떤 메서드는 즉시("탐욕적") 실행(Immediate Execution)을 해. 예를 들면:

  • .ToList()
  • .ToArray()
  • .Count()
  • .Average()
  • .Sum()
  • .First(), .Last(), .Single()

이런 메서드는 LINQ 쿼리를 바로 실행시켜, 왜냐면 IEnumerable이 아니라 완성된 결과를 반환하거든.

예시:


var query = numbers.Where(n => n > 2);
var result = query.ToList(); // 여기서 쿼리가 바로 실행됨!

7. "lazy evaluation"(Lazy Evaluation) 개념

지연 실행(deferred execution)은 .NET에서 lazy evaluation(Lazy Evaluation)의 한 예야.

lazy evaluation은 결과가 진짜 필요할 때까지 계산하지 않는 거야. LINQ 말고도, C#에는 lazy 계산을 위한 다른 방법도 있어.

Lazy<T> 클래스

C#은 Lazy<T>라는 특별한 타입을 제공해서, 값이 "필요할 때만" 계산되게 해.

간단한 예시:


// 게으른 숫자 만들기 — 진짜로 요청할 때만 계산됨
var lazyValue = new Lazy<int>(() =>
{
    Console.WriteLine("값 계산 중!");
    return 42;
});

Console.WriteLine("Lazy 객체는 만들어졌지만, 값은 아직 계산 안 됨");
Console.WriteLine($"값: {lazyValue.Value}"); // 여기서 계산이 일어남

이게 왜 필요할까?
예를 들어, 거의 안 쓰는 파라미터가 있는데, 계산이 오래 걸리거나 리소스를 많이 쓴다면 이렇게 쓸 수 있어.

8. 표: LINQ 메서드와 실행 방식

메서드 지연 실행 즉시 실행
Where
Select
Take
Skip
Join
ToList
ToArray
Count
First

9. 흔한 실수와 구현 특징

반복 순회로 "쓸데없는 일" 발생

지연 실행은 순회할 때마다 새로 계산하니까, 쓸데없이 여러 번 계산할 수 있어.


var expensiveQuery = bigList.Where(x => SomeHeavyCalculation(x));

var result1 = expensiveQuery.ToList(); // 한 번 계산
var result2 = expensiveQuery.ToList(); // 두 번째 계산 (결과는 같지만 시간 낭비)

해결법 — 한 번만 materialize해서 캐싱:
var cached = expensiveQuery.ToList();

"예외 잡기 힘들다"

쿼리 안 함수가 예외를 던지면, 쿼리 정의할 때가 아니라 컬렉션을 순회할 때 예외가 발생해.

순회 중 컬렉션 수정

순회 중에 컬렉션을 바꾸면 InvalidOperationException 예외가 날 수 있어.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION