[루비 사용자 가이드] 이터레이터(Iterator)

루비/레일스 프로그래밍/루비 사용자 가이드 2007.04.30 08:17

이터레이터(interator)라는 개념이 루비에서 처음 비롯된 것은 아닙니다. 그것은 객체지향 언어에서 일반적으로 사용되는 개념입니다. 비록 이터레이터라 불리지는 않았지만 Lisp에서도 동일한 것이 있습니다. 이터레이터라는 개념은 친근하지 않은 것이기 때문에 좀 더 자세히 설명할 필요가 있습니다.

아다시피 이터레이트(iterate)라는 동사는 같은 일을 여러 번 반복하는 것을 의미합니다. 따라서 이터레이터는 같은 일을 여러번 반복하는 어떤 것을 의미합니다.

코드를 작성할 때 여러 상황에서 루프가 필요합니다. C에서는 forwhile을 사용합니다. 예를 들면 다음과 같습니다.

char *str;
for (str = "abcdefg"; *str != '\0'; str++) {
  /* process a character here */
}

C의 for(...) 문법은 루프와 관련된 추상화(abstraction)를 돕지만, 프로그래머가 스트링의 내부 구조를 알아야만 *str 을 null과 비교하는 테스트를 작성할 수 있습니다. 이런 특징으로 인해 C가 저수준 언어처럼 느껴지게 됩니다. 고수준 언어는 이터레이션을 더 유연하게 지원하는 특징이 있습니다. 다음 sh 셀(shell) 스크립트를 살펴보세요:

#!/bin/sh

for i in *.[ch]; do
  # ... 여기서 각각의 파일에 대해 작업을 진행
done

현재 디렉터리에 있는 모든 C 소스 파일과 C 헤더 파일이 처리됩니다. 그리고 커맨드 셀이 파일 이름을 하나씩 얻어서 변수 i에 저장하는 작업의 세부 사항을 알아서 처리합니다. 나는 이렇게 동작하는 것이 C보다 더 고수준이라 생각합니다. 여러분은 안 그러신가요?

또한, 더 고려해야 할 것이 있습니다. 어떤 언어가 내장 데이터 타입에 대해 이터레이터를 제공하는 것은 좋은 일이지만, 프로그래머가 작성한 데이터 타입에 대해서는 저수준의 루프를 작성해야만 한다면 실망스러운 일입니다. OOP(객체지향프로그래밍,Object Oriented Programming)에서 프로그래머는 여러 데이터 타입을 정의하기 때문에, 중대한 문제가 됩 수 있습니다.

따라서, 모든 OOP 언어는 이터레이션을 위한 기능을 몇 가지 포함하고 있습니다. 몇몇 언어는 이러한 목적으로 특별한 클래스를 제공하기도 합니다. 반면 루비는 이터레이터를 직접 정의할 수 있도록 되어 있습니다.

루비의 String 타입에는 몇 가지 쓸모 있는 이터레이터가 포함되어 있습니다:

ruby> "abc".each_byte{|c| printf "<%c>", c}; print "\n"
<a><b><c>
   nil

each_byte는 스트링의 각 문자를 이터레이션합니다. 각각의 문자는 지역 변수 c에 저장됩니다. 같은 작업을 C언어 같은 스타일로 처리할수도 있습니다...

ruby> s="abc";i=0
   0
ruby> while i<s.length
    |    printf "<%c>", s[i]; i+=1
    | end; print "\n"
<a><b><c>
   nil

... 하지만 each_byte을 사용하는 것이 개념적으로 더 단순하고, 게다가 String 클래스(class)가 나중에 크게 변경되더라도 계속 동작할 가능성이 더 큽니다. 이터레이터를 사용하는 것의 잇점중 하나는, 대상 클래스 구현이 변경되더라도 프로그램이 망가지는 일이 더 적다는 것입니다. 이런 특성은 일반적으로 좋은 코드의 특징 중 하나입니다.(네~네~, 성질 급한 독자는 클래스(class)가 뭐냐고 벌써 질문을 하시는군요. 조금만 참아주세요. 나중에 다~ 나오게 됩니다.)

String이 제공하는 또 다른 이터레이터는 each_line입니다.

ruby> "a\nb\nc\n".each_line{|l| print l}
a
b
c
   nil

C에서 프로그램을 짤 때 가장 많은 노력이 들어가는 부분(구분자(delimiter) 찾기, 서브스트링 얻기, 등)을 이터레이터를 사용하면 쉽게 해결할 수 있습니다.

이전의 글에 나타난 for 문은 each 이터레이터로 이터레이션을 합니다. Stringeacheach_line과 동일합니다. 따라서 앞의 프로그램은 for를 가지고 다음과 같이 쓸 수도 있습니다:

ruby> for l in "a\nb\nc\n"
    |   print l
    | end
a
b
c
   nil

이터레이션 루프와 함께 retry를 사용할 수 있습니다. retry를 사용하면 루프를 이터레이션의 시작부터 다시 재시작합니다.

ruby> c=0
   0
ruby> for i in 0..4
    |   print i
    |   if i == 2 and c == 0
    |     c = 1
    |     print "\n"
    |     retry
    |   end
    | end; print "\n"
012
01234
   nil

앞의 예의 retryredo로 바꾸면 전체 이터레이션이 아닌 현재의 이터레이션만 다시 시작하게 되며, 다음과 같은 결과를 얻게 됩니다:

012
234

yield는 이터레이터의 정의 내부에 자주 보입니다. yield는 컨트롤을 이터레이터에 전달된 코드 블럭으로 옮깁니다(이것에 대해서는 프로시저 객체에서 더 자세히 설명할 예정입니다). 다음 예는 코드 블럭을 주어진 횟수만큼 반복하는 repeat라는 이터레이터를 정의합니다.

ruby> def repeat(num)
    |   while num > 0
    |     yield
    |     num -= 1
    |   end
    | end
   nil
ruby> repeat(3) { puts "foo" }
foo
foo
foo
   nil

retry를 가지고, 루비가 제공하는 표준 while처럼 동작하는 이터레이터를 작성할수도 있습니다.

ruby> def WHILE(cond)
    |   return if not cond
    |   yield
    |   retry
    | end
   nil
ruby> i=0; WHILE(i<3) { print i; i+=1 }
012   nil

이제 이터레이터가 어떤 것인지 감을 잡으셨나요? 약간의 제약이 있긴 하지만, 여러분 자신만의 이터레이터를 만들 수도 있습니다. 그리고 실제로 새로운 데이터 타입을 정의할 때마다 적절한 이터레이터를 함께 정의하는 것이 편리할 경우가 많습니다. 이런 면에서 앞에서 들었던 이터레이터의 예제들은 그리 많이 유용하지는 않습니다. 실용적인 이터레이터에 대해서는 클래스가 무엇인지를 더 잘 이해한 다음에 이야기할 수 있을겁니다.


Trackbacks 0 : Comments 0

Write a comment