본문 바로가기
카테고리 없음

Enumerator 파헤치기6

by 경자꿈사 2025. 2. 12.

이전 글에서는 Enumerator의 객체를 항상 배열이나 해시 등의 컬렉션 객체를 통해 생성했었는데, 블록을 전달하여 Enumerator 객체를 직접 생성하는 것도 가능하다.
아래 그림에서 new 메서드에 전달한 블록의 내용을 보면 블록 파라미터 y에 대해 << 연산자 메서드를 세 번 호출하면서 정수를 하나씩 건네주고 있다.
그리고 그렇게 생성한 Enumerator 객체를 사용하여 (컬렉션 객체를 통해 생성했을 때와 마찬가지로) next 메서드로 해당 값들을 하나씩 순차적으로 접근할 수 있고, 
rewind 메서드로 다시 순회 위치를 처음으로 되돌릴 수도 있으며, with_index 메서드로 값들을 인덱스와 함께 순회하면서 블록을 실행할 수도 있다.

그러면 이렇게 블록을 전달하여 Enumerator 객체를 직접 생성하는 방식을 언제 사용하면 좋을까?
차이점을 보면 [1, 2, 3].each 등으로 컬렉션으로부터 생성한 Enumerator 객체를 사용할 때는 이미 존재하는 값들(컬렉션 객체의 요소들)을 사용하는 것이지만, 
블록을 전달하여 직접 생성한 Enumerator 객체를 사용할 때는 (next 메서드 등의 호출로 인해서) 실제로 값이 필요한 시점에 동적으로 생성하여 사용한다.
아래 그림을 보면 1부터 100까지의 숫자를 하나씩 더해가면서 그 합계 값을 전달하도록 Enumerator 객체를 생성했다.
next 메서드를 3번 호출하였는데, 호출할 때마다 다음 값을 계산하여 전달해 주는 것을 볼 수 있다.
그리고 Enumerator 클래스도 Enumerable 모듈을 인클루드하고 있기 때문에 take_while 메서드를 사용해서 블록의 결괏값이 처음으로 거짓이 되기 전까지의 값들을 배열로 받아 올 수 있다.
take_while 메서드가 값 전체를 순회하기 위해 Enumerator 객체 생성 시 전달한 블록을 새롭게 다시 실행하는 것을 볼 수 있는데, next 메서드가 참조하는 내부 상태 값(순회 위치)은 여전히 유지됨을 알 수 있다.

아래 날씨 정보를 알려주는 WeatherAPI 클래스의 코드가 있는데, 날짜를 인수로 주고 get_weather 메서드를 호출하면 해당 날짜와 날씨의 정보를 해시에 담아 돌려준다.
WeatherAPI 클래스의 코드를 D:/blog/ruby/enumerator 폴더 아래 weather_api.rb 파일에 저장하자.

class WeatherAPI
  def initialize
    @weathers = ["맑음", "흐림", "안개", "소나기", "폭우", "진눈깨비", "눈보라"]
    @idx = -1
  end

  def get_weather(date)
    @idx = @idx + 1 < @weathers.size ? @idx + 1 : 0
    { date: date, weather: @weathers[@idx] }
  end
end

그다음 '달력 만들기' 글을 통해 만들었던 Calendar 클래스의 소스 파일(D:/blog/ruby/calendar/calendar.rb)을 enumerator 폴더로 복사해 오자.
파일을 복사한 다음 enumerator 폴더의 위치에서 irb를 실행하여 다음 그림처럼 코드를 입력해 보면 현재 달력을 화면에 이쁘게 표시해 주는 걸 볼 수 있다.

WeatherAPI 클래스의 테스트 코드를 작성할 때 Calendar 클래스를 사용하려고 하는데, Calendar 클래스에는 현재 특정 월의 1일부터 마지막 날까지 순회하면서 원하는 코드를 실행할 수 있는 메서드가 없다.
그러면 먼저 Calendar 클래스에 아래처럼 each와 each_formatted 메서드를 추가해 보자.
each 메서드는 블록 파라미터로 Date 객체를 넘겨주지만 each_formatted 메서드는 인수로 받은 형식대로 Date 객체를 문자열로 변환하여 블록 파라미터에 전달한다.
그리고 each_formatted 메서드는 순회를 책임지는 each 메서드를 사용하기 때문에 메서드 작성 시 날짜 포맷팅에만 신경 쓰면 되었다.

require 'date'

class Calendar
  attr_reader :year, :month
  attr_writer :prev_month, :next_month  
  protected :prev_month=, :next_month=

  def initialize(date = Date.today)
    @first_date = Date.new(date.year, date.month, 1)
    @last_date = Date.new(date.year, date.month, -1)

    @year = @first_date.year
    @month = @first_date.month
  end

  def each
    @last_date.day.times { |i| yield(@first_date + i) }
  end
  
  def each_formatted(fmt = "%Y-%m-%d")
    each { |d| yield(d.strftime(fmt)) }
  end

  def prev_month
    if @prev_month.nil?
      @prev_month = Calendar.new(@first_date - 1)
      @prev_month.next_month = self
    end
    @prev_month
  end

  def next_month
    if @next_month.nil?
      @next_month = Calendar.new(@last_date + 1)
      @next_month.prev_month = self
    end
    @next_month
  end

  def print
    puts "#{year}년 #{month}월".center(20)
    puts "일 월 화 수 목 금 토"  
  
    arr = [""] * @first_date.wday
    arr.concat((1..@last_date.day).to_a)

    arr.each_slice(7) do |week|
      puts week.map { |day| day.to_s.rjust(2) }.join(" ")
    end
  end
end

이제 준비가 끝났으니 enumerator 폴더의 위치에서 irb를 실행하여 다음 그림처럼 WeatherAPI 클래스에 대해 간단한 테스트를 진행해 보자.

앞의 테스트 코드를 Enumerator 클래스를 사용하여 아래 그림처럼 다시 작성해 보았다. 코드만 길어지고 별다른 장점은 없어 보인다.

하지만 그렇게 판단하기에는 아직 이르다. 만약 해당 월에서 마지막으로 날씨가 맑은 날짜를 찾으려면 어떻게 해야 할까?
다음 그림처럼 작성하면 어렵지 않게 원하는 날짜를 찾을 수 있다.

그러면 해당 월에서 날씨가 맑은 날짜가 총 며칠인지 세어보려면 어떻게 해야 할까? 이 또한 다음 그림처럼 작성하면 쉽게 계산할 수 있다.

그런데, 이러한 방식으로 원하는 조건을 만족하는 코드를 작성하다 보면 코드의 양이 계속적으로 늘어날 뿐만 아니라 중복되는 코드의 양 또한 늘어나게 된다.
앞의 두 예제 코드를 Enumerator 클래스를 사용하여 작성하면 아래 그림처럼 간단하게 해결된다.
Enumerator 클래스를 사용하면 문제 해결을 위해 Enumerable 모듈이 제공하는 많은 메서드들을 쉽게 활용할 수 있게 되는 것이다.

 

See you again~~