제너레이터 이해하기 share

today 2016-10-02 face Posted by appkr turned_in Learn & Think forum 0

제너레이터(generator)는 대용량 데이터를 순회하며 작업할 때 유용한 기능이며, 대부분의 프로그래밍 언어에서 찾아볼 수 있다. 언어만 다를 뿐 개념은 모두 같다. 이 포스트는 자바스크립트 및 PHP 제너레이터에 대한 필자 나름의 이해 방식을 기록한 것이다.

• • •

1. 예제 - ES6

1 | function * numbers(){
2 |   console.log('Hello Generator!');
3 |   yield 1;
4 |   yield 2;
5 | }

let iterator = numbers();

ES6에서는 함수 선언에 별표(*)를 붙여서 함수가 아니라 제너레이터임을 표시한다. iterator 변수에 방금 만든 제너레이터를 할당했다. 변수 이름에서 알 수 있듯이 함수의 모양을 빌렸지만, 개념적으로는 이터레이터(반복기)다.

함수 시작부에 console.log('Hello Generator!');를 찍었지만 아무일도 일어나지 않았다. 지금은 대기 상태라 할 수 있다. 제너레이터를 실행하려면 next() 메서드를 호출하면 된다.

console.log(iterator.next());
// Hello Generator!
// Object {value: 1, done: false}

이제 제너레이터 객체를 생성하고 첫번째 yield 키워드까지 실행한다. yield 키워드는 값을 반환한다는 측면에서 return 키워드와 비슷하면서도 다르다. return은 뒤에 나오는 로직을 전부 무시하지만, yielditerator가 불러주면 또 다시 동작한다. done: false를 주목한다.

또 실행한다.

console.log(iterator.next());
// Object {value: 2, done: false}

console.log(...)와 첫번째 yield 키워드는 건너뛰었다. 두번째 yield 키워드에 의해 value는 2로 바뀌었지만 아직 done: flase이다.

한번 더 실행한다.

console.log(iterator.next());
// Object {value: undefined, done: true}

이제 더 이상 실행할 yield 키워드가 없다. done: true로 바뀌었다.

Generator in action

2. 해부하기

앞 절에서 실험한 내용을 그림으로 정리하면 다음과 같다.

iterator                                numbers(generator instance)
|                                       |
|next();───────────────────────────────>|console.log(...); 실행
|                                       |yield 1; 실행하고 상태 저장
|                       Hello Generator!|
|                {value: 1, done: false}|
|<──────────────────────────────────────|
|next();───────────────────────────────>|yield 2; 실행하고 상태 저장
|                {value: 2, done: false}|
|<──────────────────────────────────────|
|next();───────────────────────────────>|더 이상 실행할 로직이 없음
|         {value: undefined, done: true}|
|<──────────────────────────────────────|
|                                       |

전부 정리해 보면 제너레이터는(개인적인 이해일 뿐이다)

  • 일회용 이터레이터다.
  • 호출하는 쪽에서 이터레이션의 시작과 다음 이터레이션을 제어할 수 있다(On-demand Iteration).
  • 어디까지 실행했는지 상태를 가진 객체다.

3. 예제 - PHP

실전에 사용한 예제는 AWS PHP SDK에서 찾아 볼 수 있다. AWS SDK에서 이터레이션은 거의 대부분 제너레이터를 사용하는 것을 볼 수 있다.

<?php
// https://github.com/aws/aws-sdk-php/blob/master/src/functions.php#L49

namespace Aws;

/**
 * Applies a map function $f to each value in a collection.
 *
 * @param mixed    $iterable Iterable sequence of data.
 * @param callable $f        Map function to apply.
 *
 * @return \Generator
 */
function map($iterable, callable $f)
{
    foreach ($iterable as $value) {
        yield $f($value);
    }
}

이렇게 사용할 수 있다.

<?php

$generator = Aws\map(range(1, 1000000), function ($value) {
    return $value * 2;
});

foreach ($generator as $number) {
    echo $number, PHP_EOL;
}

이 예제에서는 큰 차이를 못 느낄 수 있지만, 배열 요소 하나가 큰 데이터를 가지고 있을 때는 제너레이터를 쓰지 않고는 php.ini 설정에서 memory_limit 값을 엄청 늘려야 할 것이다. 배열 순회에 필요한 모든 데이터를 메모리에 적재한 후 실행하는 것과, 이번 순회에 필요한 데이터만 읽어오는 차이가 있다.

comments powered by Disqus
keyboard_arrow_up