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

Enumerator 파헤치기2

by 경자꿈사 2025. 2. 4.

지난 글에서 배열이나 해시, 범위 등의 객체로부터 Enumerator 객체를 생성할 수 있는 것을 알았다.
배열이나 해시, 범위처럼 여러 요소를 포함하거나 요소들의 집합을 표현하며, 각 요소에 접근하고 순회할 수 있는 기능을 제공하는 객체를 컬렉션이라고 부른다.
아래 그림을 보면 여러 컬렉션 객체들에 대해 블록을 전달하지 않고 each, map, select 메서드 등을 호출하여 Enumerator 객체를 생성했다.
여기서 Enumerator 객체의 inspect 값을 보면 해당 Enumerator 객체가 어떤 컬렉션 객체의 어떤 메서드로부터 생성이 되었는지 보여주는데, 이 정보를 Enumerator 객체가 가지고 있다는 것은 어떤 의미일까?

아래 그림처럼 서로 다른 메서드를 통해 생성한 Enumerator 객체로 each 메서드를 호출해 보면 Enumerator 객체가 그러한 정보를 왜 갖고 있는지 알 수 있다.

화면에 출력되는 내용을 보면 Enumerator 객체에 대해 호출한 세 번의 each 메서드 모두 원래의 배열이 갖고 있는 요소들을 모두 순회하면서 블록을 실행하는 것은 동일하지만 
최종 결괏값은 서로 다른 것을 볼 수 있다.
아래 그림을 보면 원래의 배열 [1, 2, 3]에 대해 블록을 전달하여 직접 each, map, select를 호출했을 때 나오는 결괏값과 각각 동일함을 알 수 있다.

이미 알고 있겠지만, each 메서드는 호출 대상 객체를 그대로 반환하고, map은 블록을 실행한 결괏값을 담은 새로운 객체를 반환하고, select는 블록의 결괏값이 참으로 평가되는 요소들만 담은 새로운 객체를 반환한다.
그리고 puts 메서드는 객체의 문자열 표현을 출력하고 결괏값으로 nil을 반환한다. 
그래서 블록을 실행한 결괏값은 nil이 되어 map 메서드의 결괏값은 [nil, nil, nil]이 되고, select 메서드의 결괏값은 (nil은 거짓으로 평가되므로) []이 된다.

즉, Enumerator 객체가 자신을 생성해낸 컬렉션 객체와 메서드의 정보를 알고 있기 때문에 Enumerator 객체에 대해 each를 호출하면 (어떤 객체의 어떤 메서드로부터 생성되었는지에 따라) 서로 다르게 동작할 수 있는 것이다.
그러면 어떻게 그러한 정보를 가지고 each 메서드가 서로 다른 동작을 하도록 만들 수 있을까?
아래 MyEnumerator 클래스의 코드를 보자. 
initialize 메서드는 앞서 얘기한 컬렉션 객체와 메서드의 정보를 인수로 받을 수 있게 정의했고, each 메서드에서는 @object가 참조하는 객체에 대해 @method가 나타내는 메서드를 블록과 함께 호출함으로써 처리를 위임하고 있다.

class MyEnumerator
  def initialize(object, method)
    @object = object
    @method = method
  end
  
  def each(&block)
    @object.send(@method, &block)
  end
end

D:/blog/ruby 폴더 아래 enumerator 폴더를 하나 만들고 그 폴더 안에 my_enumerator.rb 파일을 만들어 MyEnumerator 클래스의 코드를 저장하자.
이제 enumerator 폴더의 경로에서 irb를 실행한 후 다음 그림처럼 MyEnumerator 클래스를 테스트해보자.
테스트 코드는 MyEnumerator 클래스의 객체를 직접 생성한다는 것을 제외하고는 앞의 Enumerator 클래스를 사용한 예제 코드와 동일하다.

화면에 출력되는 결과 역시 Enumerator 클래스를 사용한 예제와 동일한데, MyEnumerator 객체를 생성할 때 건넨 인수에 따라 each 메서드가 다르게 동작하는 걸 볼 수 있다.
아래는 배열 대신 해시를 사용하여 Enumerator와 MyEnumerator 클래스의 each 메서드를 테스트해 본 것인데, 실행 결과가 같은 것을 볼 수 있다.
반복되는 코드를 없애기 위해 미리 Proc 객체를 하나 만들어 두고 each 메서드를 호출할 때 & 연산자를 사용하여 Proc 객체를 블록으로 변환해서 넘겼다.
그리고 메서드 체이닝을 사용하여 Enumerator나 MyEnumerator 객체를 변수에 담지 않고 바로 each 메서드를 호출하였다.

Enumerable 모듈이 제공하는 여러 메서드들 중에 each_with_index와 each_with_object라는 게 있는데, 이름 그대로 각각의 요소를 순회하면서 인덱스 또는 (인수로 전달한) 객체를 요소와 함께 블록에 전달해 준다.
아래 간단한 예를 볼 수 있다. each_with_index는 each와 동일하게 결괏값은 호출 대상 배열이 되고 each_with_object는 인수로 전달했던 객체를 결괏값으로 돌려준다.
그리고 each_with_index와 each_with_object 둘 다 블록 없이 호출하면 Enumerator 객체를 생성해서 반환하는데, 특히 each_with_object 메서드를 통해 생성된 Enumerator 객체의 경우 인수로 전달한 객체(아래의 예에서는 빈 문자열)에 대한 정보(객체 참조)도 가지고 있음을 알 수 있다.

each_with_object 같은 경우에는 인수를 전달하지 않으면 예외가 발생하는 걸 볼 수 있다.

아래 그림처럼 each_with_index와 each_with_object를 통해 생성한 Enumerator 객체에 대해 each 메서드를 호출해 보면 역시 잘 동작하는 것을 볼 수 있다.

그리고 MyEnumerator 클래스를 사용하여 인수가 필요 없는 each_with_index 메서드를 테스트해 봐도 잘 동작한다.

 

그러면 MyEnumerator 클래스에 each_with_object처럼 인수가 필요한 메서드에 대해서도 처리할 수 있도록 코드를 수정해 보자.
아래 코드를 보면 변경 사항이 많지 않은데, initialize 메서드에 가변 인수를 받을 수 있는 파라미터를 하나 추가했고, 그렇게 받은 가변 인수를 each 메서드에서 send 메서드를 호출할 때 추가로 전달했다.

class MyEnumerator
  def initialize(object, method, *method_args)
    @object = object
    @method = method
    @method_args = method_args
  end
  
  def each(&block)
    @object.send(@method, *@method_args, &block)
  end
end

테스트를 위해 먼저 수정한 MyEnumerator 코드를 my_enumerator.rb 파일에 저장하고, my_enumerator.rb 파일이 있는 폴더의 위치에서 irb를 실행하여 다음 그림처럼 코드를 입력해 보자.
Enumerator 클래스를 사용하여 테스트했을 때와 동일하게 동작하는 것을 볼 수 있을 것이다.

다음 글에서는 Enumerator 클래스가 제공하는 인스턴스 메서드 중에서 with_index와 with_object에 대해서 살펴보자.

 

See you again~~