IEnumerable과 IEnumerable<T>

Hyeon O·2025년 6월 25일

Unity

목록 보기
6/15

개요

C# 언어의 핵심 인터페이스인 IEnumerableIEnumerable<T> 에 대해서 개념적으로 이해하고

Unity 환경에서 데이터 드리븐 설계에 사용한 사례를 가져와서 정리하였다.

개념

IEnumerable은 C#에서 컬렉션 또는 데이터 집합을 순회(iteration)할 수 있도록 해주는 인터페이스이다.

여기서 순회가 가능하다는 말은, 어떤 데이터 집합 안에 들어가 있는 요소들을 하나씩 꺼내면서 처리할 수 있다는 의미이다.

즉, foreach문을 통해 데이터를 하나씩 처리할 수 있는 객체를 의미하며,

List, Array, Dictionary, HashSet 등 대부분의 컬렉션 클래스는 이 인터페이스를 구현하고 있다.

그렇다면 IEnumerable와 IEnumerable의 차이는 뭘까?

항목IEnumerableIEnumerable<T>
타입비제네릭제네릭
반환 타입IEnumeratorIEnumerator<T>
요소 타입항상 object명시된 타입 T
사용 예옛날 .NET, 비타입 안전 상황LINQ, 타입 안전, 성능 최적화
박싱 발생있음 (object 캐스팅 필요)없음

공통적으로 순회 가능한 객체를 정의하는 인터페이스이다.

다만 반환 타입과 요소 타입에서 차이가 있다. 아래 코드를 보면

// IEnumerable (object로 반환 → 캐스팅 필요)
IEnumerable oldList = new List<int> { 1, 2, 3 };
foreach (object item in oldList)
    Debug.Log((int)item); // 직접 캐스팅 필요

// IEnumerable<T> (타입 명확 → 안전함)
IEnumerable<int> typedList = new List<int> { 1, 2, 3 };
foreach (int item in typedList)
    Debug.Log(item); // 캐스팅 필요 없음

IEnumerableIEnumerable<T>의 실질적인 차이를 보여준다.

IEnumerable 제네릭이 아니기 때문에 요소의 타입이 object로 간주된다.

그래서 item은 object로 받아지며, 실제 사용할 땐 명시적 캐스팅이 필요하다.

여기서 실수로 잘못된 타입을 캐스팅하면 InvalidCastException 이 발생할 수 있다.

즉, 타입 안정성이 없다.

반면IEnumerable<int>제네릭 인터페이스라서, 요소의 타입이 처음부터 int로 고정된다.

foreach 내부에서도 컴파일러가 타입을 알고 있어서 캐스팅 없이 바로 접근 가능하다.

즉, 타입 안정성이 보장된다.

이러한 이유로 순회 가능과 타입 안정성과 성능까지 고려된 IEnumerable<T> 를 주로 사용한다.

IEnumerable vs IEnumerator

IEnumerator 이거 어디선가 많이 보았을 것 같다.

게임 구현에 있어서 코루틴을 많이 사용하는데, 이때 코루틴을 구현할 때 항상 쓰던 녀석이다.

IEnumerable 은 "순회 가능한 컬렉션"을 정의한다고 했었다.

반면 IEnumerator 는 개념적으로 실제 순회 동작을 정의한다.

아래는 이 둘의 관계 구조를 코드로 표현한 것이다.

foreach (var item in collection) {
  // 내부적으로 동작:
  var enumerator = collection.GetEnumerator(); // ← IEnumerable이 제공
  while (enumerator.MoveNext()) {              // ← IEnumerator가 수행
      var current = enumerator.Current;
  }
}

IEnumerable 이 순회할 수 있는 집합을 제공하고,

IEnumerator 가 하나씩 꺼내주어, 수행하는 것으로 이해하면 된다.

아니 그러면 코루틴에 IEnumerator 는 뭘까?

아래는 코루틴의 실행 흐름을 이해해보자.

IEnumerator FadeOut()
{
    for (float t = 1f; t >= 0; t -= Time.deltaTime)
    {
        canvas.alpha = t;
        yield return null; // 다음 프레임까지 중단
    }
}
  1. StartCoroutine(FadeOut()) 호출 → IEnumerator 리턴
  2. Unity가 이 객체를 내부 리스트에 등록
  3. 매 프레임마다 MoveNext() 호출
  4. yield return을 기준으로 잠깐 중단
  5. MoveNext()가 false가 되면 종료

IEnumerator.Current의 리턴값이 WaitForSeconds, null, WaitUntil 등등 타이밍 신호를 반환하여 Unity가 이 값을 보고 언제 다시 MoveNext()를 호출할 지 결정한다.

컬렉션 순회에 있어 IEnumerator 를 살펴보면

List<int> numbers = new List<int> { 10, 20, 30 };
IEnumerator<int> enumerator = numbers.GetEnumerator();

while (enumerator.MoveNext())
{
    Debug.Log(enumerator.Current);
}
  1. GetEnumerator() → 반복자 객체 생성
  2. MoveNext() → 내부 커서 이동
  3. Current → 현재 요소 반환
  4. 끝나면 false 반환 → 종료

모든 작업이 즉시 동기 실행이 되며,
MoveNext()는 단순히 다음 요소가 있는지 확인하는 함수이다.
Current는 그 시점의 데이터를 반환 (절대 yield return 아니다)

핵심적인 차이를 정리하면 아래와 같다.

구분Unity 코루틴일반 컬렉션 순회
yield return의 의미프레임 중단/제어 신호값 전달
CurrentWaitForSeconds, null 등 시간 제어 객체현재 요소
MoveNext() 호출자Unity 엔진 내부 루프개발자 코드 (foreach, while)
용도시간 지연 기반 흐름데이터 반복 처리

활용

이전에 구현한 데이터 드리븐 설계를 위한 CSV 읽는 로직을 예시로 들었다.

 private static IEnumerable<string[]> ParseLines(TextAsset ta)
 {
     if (ta == null)
         throw new System.IO.FileNotFoundException(
             $"CSV asset not found in Resources/CSV");

     var lines = ta.text.Split('\n');

     // 0: 헤더, 1: 타입 선언 → 2번부터 실데이터
     for (int i = 2; i < lines.Length; ++i)
     {
         var raw = lines[i].Trim();
         if (string.IsNullOrEmpty(raw)) continue;

         // 쉼표 안의 공백 제거
         var cols = raw.Split(',');
         for (int c = 0; c < cols.Length; ++c)
             cols[c] = cols[c].Trim();

         yield return cols;
     }
 }

private static IEnumerable<string[]> ParseLines(TextAsset ta)

이 함수가 IEnumerable<string[]>를 반환한다는 건 다음을 의미한다.

  1. 한 줄씩 지연 처리가 가능하다
    • ParseLines는 내부적으로 yield return을 사용하고 있다.
    • 따라서 전체 CSV 데이터를 한꺼번에 파싱하는 게 아니라 "필요할 때 한 줄씩" 처리하게 된다.
    • 특히 실데이터가 수천 줄일 경우, 메모리 낭비 없이 효율적으로 순회 가능하다.
  2. foreach를 쓸 수 있는 열거 가능한 데이터이다.
    • IEnumerable<string[]> 덕분에 foreach를 통해 가볍게 각 줄을 순회할 수 있다.
    • List<string[]>와는 달리, 전체 데이터를 미리 메모리에 담지 않아도 된다.

여기서 왜 IEnumerable<T> 가 좋은 선택일까

일단 성능 최적화에 있다.

yield return을 쓰면 그 시점에서 데이터를 만들고 반환하므로, 불필요한 연산/할당 방지한다.

예를 들어, Load<T>()에서 일부 줄만 필요할 수도 있는데, 이 방식이면 필요한 만큼만 처리 가능하다.

두 번째로, 유연한 처리 흐름이다.

데이터를 순회하면서, Func<string[], T> map을 통해 어떤 형태로든 자유롭게 가공 가능하다.

세 번째로 책임을 분리한 것이다.

ParseLine()은 텍스트 분해 및 순회만 담당하고

Load()는 데이터 로딩 및 가공만 담당한다.

이 구조가 가능한 이유가 IEnumable<T>가 중간에서 연결 고리 역할을 해주기 때문이다.

시각적으로 정리하면 다음과 같다.

Resources/TextAsset
     ↓
 ParseLines() : IEnumerable<string[]>(yield return)
 foreachmap() → T
     ↓
 List<T> → 배열로 변환

ParseLines()가 데이터 스트림을 만들어주고, foreach에서 그걸 순차 소비하는 구조다.

profile
천천히, 꾸준하게, 끝까지

0개의 댓글