프로그래머 혹은 소프트웨어 엔지니어의 배경 지식은 컴퓨터 과학이고, 기초가 탄탄할 수록 게임이든 앱이든 잘 만들 수 있습니다. 이 글에서는 우리가 거의 매일 사용하고 있는 C#의 IEnumerator
, IEnumerable
의 탄생 과정을 살펴봄으로서 이해의 폭을 넓히고자 합니다.
일단 우리가 이해하고 있는 오브젝트는 단순히 게터/세터(getter/setter)의 집합이라는 사실에서 출발해 봅시다. 아래 Person
클래스는 Age
세터와 Name
게터를 정의하고 있습니다.
public class Person
{
public int Age { set { ... } }
public string Name { get { ... } }
}
여기서 클래스는 단순히 게터/세터를 모아놓은 것에 불과합니다.
그럼 게터는 뭘까요? C#의 람다식으로 표현하면 ()
를 받아서 무엇인가를 리턴하는 함수입니다 ()
는 인자가 없다는 뜻이므로, 아무 인자도 받지 않고 무엇인가를 만들어내는 게터는 수학적 의미의 함수는 아니고, 사이드 이펙트 함수입니다. 부를 때마다 값을 계속 만들어내는 일종의 생산자라고도 생각할 수 있습니다.
() => A
() => string
의 의미에 대해 다시 살펴봅시다. 이 게터의 타입은 Func<string>
이고, 호출을 하면 매번string
의 인스턴스를 리턴한다는 뜻입니다. 하지만 엄밀히 말하면 C#의 타입 시스템은 거짓말을 하고 있습니다. 이 함수는 string
만 리턴할 수 있는 게 아니라 사실은 예외를 던질 수도 있기 때문입니다. 혹은 이 함수가 프로그램을 종료시킬 수도 있습니다.
() => { throw new Exception(); }
그런데 게터는 어디서 나오는 걸까요? 게터를 얻어 오는 방법은 간단합니다. 다소 말장난 같긴 하지만, 게터를 리턴하는 게터를 정의하면 됩니다. 예를 들어 () => string
게터를 리턴하는 게터는 아래와 같이 정의할 수 있습니다. 물론 앞서 살펴본 것처럼 () => string
은 string
만 리턴하는 것은 아니고, 예외를 던지거나 프로그램을 종료시킬 수도 있습니다.
() => (() => string)
람다식은 알아보기 힘들기 때문에 다시 클래스 표기법으로 돌아가겠습니다. C#은 독립적인 메소드가 없기 때문에 인터페이스를 정의하고, 메소드를 정의해야 합니다. 게터를 리턴하는 게터와, 게터를 각각 별도의 인터페이스로 정리해 보겠습니다.
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T>
{
bool MoveNext();
T Current { get; }
}
재미있게도 게터를 리턴하는 게터는 IEnumerable
이고, 게터는 IEnumerator
입니다. 왜 이렇게 될까요?
일단 게터를 먼저 살펴보면, MoveNext()
함수가 하는 일이 게터와 정확히 일치함을 알 수 있습니다.MoveNext()
는 true
를 리턴하면 다음 값을, false
를 리턴하면 종료를 뜻합니다. 또한 타입에는 표현되지 않았으면 예외를 던질 수도 있습니다. 참고로 Current
프로퍼티는 사이드 이펙트가 없으므로 언제든 호출할 수 있습니다.
IEnumerable
은 게터에 해당하는 IEnumerator
를 리턴하는 GetEnumerator()
메소드를 정의하고 있으므로, 게터를 리턴하는 게터임을 알 수 있습니다.
이런 관계를 어떻게 해석해야 할까요? 앞서 () => string
은 string
값을 생성하는 일종의 생성자라고 표현했습니다. 우리가 일상적으로 쓰는 사이드 이펙트가 있는 함수라는 게 호출할 때마다 새로운 값을 리턴해 주기 때문입니다. 또 생각해 보면 IEnumerator<string>
라는 것도 일종의 생산자입니다. 부를 때마다 새로운 값을 리턴해 주기 때문입니다. () => string
도 예외를 던지거나 종료될 수 있습니다. IEnumerator<string>
도 마찬가지로 예외를 던지거나 종료될 수 있습니다. 따라서 () => string
이라는 건 IEnumerator<string>
과 하는 일이 다르지 않습니다.
바꿔 말하면 이론적으로는 IEnumerable
이라는 건 우리가 늘 사용하고 있는 게터를 인터페이스로 정의한 것에 지나지 않습니다. 또한 이런 게터를 얻어올 수 있는 방법을 IEnumerator
라는 인터페이스로 정의한 것이고요. 물론 그래서 의미가 없다는 뜻은 아닙니다. .NET에 존재하는 수많은 콜렉션 클래스들을 동일한 인터페이스로 사용할 수 있게 되었고, 그래서 LINQ와 같은 기술도 등장할 수 있게 되었기 때문입니다. 다만, 우리가 뭔가 새로운 것으로 생각하는 것이 사실은 우리가 늘 쓰고 있는 개념에 새로운 옷을 입힌 것과 다르지 않다는 것을 이야기하고 싶었습니다.
Pingback: IEnumerator, IEnumerable의 의미 | 서광열의 C# 스쿨