C#에서 yield 키워드는 컬렉션의 모든 요소를 즉시 생성하고 반환하는 대신, 요청이 있을 때마다 하나씩 값을 생성하여 반환하는 이터레이터 메서드 또는 비동기 스트림을 정의할 때 사용됩니다. 이는 특히 대량의 데이터를 처리하거나 무한 시퀀스를 다룰 때 메모리 효율성을 크게 향상시킬 수 있으며, 코드의 가독성을 높여줍니다. yield 키워드는 주로 yield return과 yield break 두 가지 형태로 사용됩니다.
주요 활용 사례
1. 대규모 데이터 처리
방대한 양의 데이터를 다룰 때, 모든 데이터를 한 번에 메모리에 로드하는 것은 메모리 부족 문제를 야기할 수 있습니다. yield를 사용하면 데이터를 요청 시점에 필요한 만큼만 생성하여 반환하므로, 메모리 사용량을 최소화할 수 있습니다.
2. 사용자 정의 이터레이션 로직 구현
특정 조건에 따라 데이터를 필터링하거나, 복잡한 순서로 데이터를 반복해야 할 때 yield를 통해 맞춤형 이터레이션 동작을 손쉽게 구현할 수 있습니다.
3. 비동기 데이터 스트림 생성
비동기 프로그래밍 환경에서 데이터가 시간이 지남에 따라 점진적으로 생성될 필요가 있을 때, yield를 IAsyncEnumerable<T>와 결합하여 비동기 데이터 스트림을 구현할 수 있습니다. 이는 실시간 데이터 처리나 네트워크 스트리밍과 같은 시나리오에 유용합니다.
사용 예시
1. 동기 이터레이터를 위한 yield return
다음 예시는 yield return을 사용하여 일련의 정수를 순차적으로 생성하는 동기 이터레이터 메서드를 보여줍니다.
using System;
using System.Collections.Generic;
class DataGenerator
{
// 지정된 개수만큼 정수 시퀀스를 생성합니다.
static IEnumerable<int> ProduceSequence(int count)
{
for (int k = 0; k < count; k++)
{
// 각 반복마다 값을 하나씩 반환합니다.
yield return k + 1;
}
}
static void Main()
{
Console.WriteLine("동기 시퀀스 생성:");
// foreach 루프를 사용하여 이터레이터의 값을 소비합니다.
foreach (int val in ProduceSequence(4))
{
Console.WriteLine($"값: {val}");
}
}
}
2. 이터레이터 조기 종료를 위한 yield break
yield break는 특정 조건이 충족되었을 때 이터레이터의 실행을 즉시 중단하고 시퀀스 생성을 종료할 때 사용됩니다.
using System;
using System.Collections.Generic;
class LimitedGenerator
{
// 특정 값에 도달하면 시퀀스 생성을 중단합니다.
static IEnumerable<string> GetLimitedItems(int maxItems)
{
string[] items = { "Apple", "Banana", "Cherry", "Date", "Elderberry" };
for (int i = 0; i < items.Length; i++)
{
if (i >= maxItems)
{
// maxItems 개수만큼만 반환하고 이터레이터를 종료합니다.
yield break;
}
yield return items[i];
}
}
static void Main()
{
Console.WriteLine("\n제한된 시퀀스 생성 (3개 항목):");
foreach (string fruit in GetLimitedItems(3))
{
Console.WriteLine($"과일: {fruit}");
}
Console.WriteLine("\n제한된 시퀀스 생성 (전체 항목):");
foreach (string fruit in GetLimitedItems(10)) // maxItems가 items.Length보다 커도 yield break에 의해 안전하게 처리됩니다.
{
Console.WriteLine($"과일: {fruit}");
}
}
}
3. 비동기 스트림을 위한 yield return
IAsyncEnumerable<T>와 함께 yield return을 사용하면 비동기적으로 값을 생성하는 스트림을 구현할 수 있습니다. 이는 C# 8.0 이상에서 지원됩니다.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class AsyncDataStreamer
{
// 지정된 개수만큼 비동기적으로 데이터 스트림을 생성합니다.
static async IAsyncEnumerable<int> StreamDelayedIntegers(int quantity)
{
for (int i = 0; i < quantity; i++)
{
await Task.Delay(50); // 비동기 작업 시뮬레이션
yield return (i + 1) * 10; // 비동기적으로 값을 반환합니다.
}
}
static async Task Main()
{
Console.WriteLine("\n비동기 데이터 스트림 시작:");
// await foreach를 사용하여 비동기 스트림을 소비합니다.
await foreach (int data in StreamDelayedIntegers(3))
{
Console.WriteLine($"수신된 비동기 데이터: {data}");
}
Console.WriteLine("비동기 데이터 스트림 완료.");
}
}
고려사항
메서드 제약 사항
yield 키워드를 포함하는 메서드에는 몇 가지 제약이 따릅니다. ref 또는 out 매개변수를 가질 수 없으며, 익명 메서드나 람다 식에서는 사용할 수 없습니다. 또한, try-catch 블록 내에서 직접 yield return을 사용할 수는 없지만, try-finally 블록 내에서는 사용할 수 있습니다.
성능 영향
yield는 메모리 사용량을 줄이는 데 효과적이지만, 각 yield return 호출마다 이터레이터의 상태를 저장하고 복원하는 오버헤드가 발생합니다. 따라서 매우 작은 데이터 집합을 다루는 경우에는 일반적인 컬렉션을 사용하는 것이 더 효율적일 수 있습니다. 성능 프로파일링을 통해 최적의 접근 방식을 결정하는 것이 중요합니다.
비동기 스트림 호환성
IAsyncEnumerable<T>를 사용한 비동기 스트림은 C# 8.0 이상 버전에서만 지원됩니다. 또한, 이러한 비동기 스트림을 소비할 때는 반드시 await foreach 구문을 사용해야 합니다.
상태 관리
이터레이터 메서드는 실행을 일시 중지하고 다시 시작할 때 내부 상태를 유지합니다. 이는 C# 컴파일러가 이터레이터 메서드를 상태 기계로 변환하기 때문입니다. 이 상태 관리는 다중 스레드 환경에서 예상치 못한 동작을 초래할 수 있으므로, 스레드 안전성을 고려해야 합니다.