Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

블록 하나로 끝내는 Ruby 열거자(Enumerator) 만들기

컬렉션을 열거자(enumerator)로 다루면 #map이나 #reduce 같은 익숙한 메서드를 추가 코드 없이 그대로 활용할 수 있습니다. 정말 강력한 기능이죠.

예전에는 열거자를 정의하는 일이 꽤 번거로웠습니다. 새 클래스를 만들고, Enumerable 모듈을 포함시키고, #each 메서드까지 직접 정의해야 했으니까요.

하지만 Ruby 1.9부터는 훨씬 가벼운 방식으로 즉석에서 열거자를 정의할 수 있게 되었습니다. 지금부터 그 방법을 살펴보겠습니다.

Enumerator 클래스 소개

Enumerator 클래스를 사용하면 블록 문법만으로 일회성 열거자를 간단히 정의할 수 있습니다. 아래 예제에서는 무한히 이어지는 난수 시퀀스를 반환하는 열거자를 만들어 보겠습니다.

e = Enumerator.new do |y|
  loop do
    y << rand(10) # The << operator "yields" a value.
  end
end

# Make the enumerator "yield" 10 values, then stop
puts e.first(10).inspect # => [6, 6, 7, 2, 2, 9, 6, 8, 2, 1]

코드에서 << 연산자가 조금 특이하게 사용된 것을 눈치채셨을 겁니다. 이것은 y.yield 메서드의 축약 표현으로, 열거자가 생성하는 각 항목마다 호출됩니다. 블록에 전달되는 y는 Yielder 객체로, 여기에 값을 넘기면 열거자가 해당 값을 외부로 내보냅니다. 다소 마법처럼 느껴진다면 걱정하지 마세요. 실제로 그렇게 설계된 것이니까요.

컬렉션 크기 다루기

컬렉션의 크기를 구하는 것은 Ruby의 지연(lazy) 열거자에게 골칫거리입니다. 항목 수를 세려면 컬렉션 전체를 로드해야 하는데, 이는 지연 열거자를 사용하는 취지와 정면으로 배치되기 때문입니다.

물론 우회 방법이 있습니다. Enumerator를 생성하는 시점에 컬렉션의 크기를 이미 알고 있다면, 생성자에 인자로 직접 전달하면 됩니다.

# You can pass the length as an argument to the constructor, if you have it
e = Enumerator.new(10) do |y|
  10.times { y << rand }
end

이렇게 크기를 지정해 두면 #size, #count 같은 메서드가 전체 컬렉션을 순회하지 않고도 결과를 즉시 반환할 수 있습니다.

실제 프로젝트 적용 사례

바로 어제 저는 Honeybadger의 새 문서 사이트를 작업하고 있었습니다. Jekyll로 구축된 이 사이트에서, 문서 내 <h2>와 <h3> 태그를 기반으로 목차(table of contents)를 생성하는 플러그인을 개발하는 중이었습니다.

<h2> 태그로 정의된 섹션에 어떤 <h3> 태그들이 속하는지 파악하는 일은 꽤 까다롭습니다. nokogiri로 HTML을 파싱한 뒤, 결과 문서를 형제 노드 단위로 훑어야 하기 때문입니다. 그래서 저는 이 로직을 추상화해 Enumerator로 만들었습니다. 실제 코드는 다음과 같습니다.

def subheadings(el)
  Enumerator.new do |y|
    next_el = el.next_sibling
    while next_el && next_el.name != "h2"
      if next_el.name == "h3"
        y << next_el
      end
      next_el = next_el.next_sibling
    end
  end
end

이렇게 만든 열거자 덕분에 복잡한 DOM 순회 로직은 캡슐화하고, 바깥에서는 #each, #map, #select 같은 Enumerable 메서드를 자유롭게 조합해 사용할 수 있었습니다.


무한 시퀀스나 외부 데이터 스트림처럼 한 번에 모두 메모리에 올릴 수 없는 데이터를 다룰 때 Enumerator는 특히 빛을 발합니다. 반복 로직이 복잡해지기 시작한다면, 이 패턴을 적극적으로 활용해 보시기 바랍니다.