PHP Generator free

2019-09-10

원래는 라라벨 6에 추가된 레이지 컬렉션을 소개하려고 했는데, 레이지 컬렉션의 근간인 제너레이터를 먼저 소개하는게 좋을것 같아서 순서를 바꿔봤습니다. 라라캐스트 Laravel Explained의 세번째 에피소드인 Explain PHP Generator 를 기반으로 설명드리겠습니다.


Generator 란


PHP 공 매뉴얼에서 제너레이터를 찾아보면 다음과 같이 설명되어 있습니다.



Generators provide an easy way to implement simple iterators without the overhead or complexity of implementing a class that implements the Iterator interface.

Generator는 Iterator 인터페이스를 구현할 필요 없이 간단한 이터레이터를 구현하는 쉬운 방법을 제공한다.



말이 조금 어려울 수 있는데, 이터레이터는 반복해서 순차적으로 처리할 수 있는 객체입니다. foreach(), for() 등으로 반복처리 할 수 있는 녀석들이고 보면 됩니다. 원래 이터레이터는 이터레이터 인터페이스를 구현해서 만들어야 합니다. 이터레이터 인터페이스는 아래 다섯 매서드를 구현해야 합니다.


`Iterator extends Traversable {
/* Methods */
abstract public current ( void ) : mixed // 현재 항목의 값을 반환합니다.
abstract public key ( void ) : scalar // 현재 항목의 키를 반환합니다.
abstract public next ( void ) : void // 다음 항목으로 이동합니다.
abstract public rewind ( void ) : void // 처음 항목으로 이동합니다.
abstract public valid ( void ) : bool // 현재 항목이 유효한지 확인합니다.
}
`

제너레이터는 되감기(rewind) 기능이 없이 단순히 다음 항목으로만 이동할 수 있어서 ‘간단한 이터레이터’고 표현한 것 같습니다. 되감기 기능이 없기 때문에 한 번만 사용 가능합니다.


`public function doStuff(Traversable $things) {
foreach ($things as $thing) { /* ... */ }
foreach ($things as $thing) { /* ... */ } // 제너레이터면 에러 발생!
}
`

(예제 소스: Introduction to Iterators and Generators in PHP)


제너레이터는 왜 쓰는가?


그냥 배열을 써서 반복처리하면 되는데 굳이 제너레이터가 필요한 이유는 대량의 데이터를 처리하기 위해서입니다.


`<?php
$items = range(<span class="il">1</span>, 100000000);

foreach($items as $item){
echo $item;
}
`

반복처리할 데이터를 배열에 담는 경우 위 처럼 너무 데이터가 많으면 메모리에 올릴 수가 없어 에러가 납니다. 사용자가 많은 애플리케이션이면 되도록 요청당 적은 메모리를 사용해야 같은 메모리로 더 많은 요청을 처리할 수 있겠죠. 제너레이터는 모든 데이터를 한 번에 배열에 넣는대신 한 번에 하나씩 데이터를 처리합니다.


사용방법은 간단합니다.


`function customRange($begin, $end) {
for ($i = $begin; $i <= $end; $i++) {
yield $i;
}
}

$items = customRange(<span class="il">1</span>, 100000000);

foreach($items as $item){
echo $item;
}
`

배열을 만들어 반환하는 대신 개별 아이템을 yield 하게 하면 됩니다.


하나의 함수에 여러 yield가 포함될 수 있습니다. 반복 처리가 한 번 실행될 때마다 다음 yield로 커서가 이동한다고 생각하시면 됩니다.


`<?php
function test()
{
yeild <span class="il">1</span>;
yield 2;
yield 3;
}

foreach(test() as $item) {
dump($item);
}
`

위의 출력 결과는


`<span class="il">1</span>
2
3
`

입니다.


Use cases for PHP generators는 글에서는 이러한 특성을 이용해서 여러 출처에서 가져온 데이터를 합치는 예제를 보여줬습니다.


`private function getEbooks()
{
$ebooks = [];

// fetch from the DB
$stmt = $this->db->prepare("SELECT * FROM ebook_catalog");
$stmt->execute();
$stmt->setFetchMode(\PDO::FETCH_ASSOC);

foreach ($stmt as $data) {
$ebooks[] = $this->hydrateEbook($data);
}

// and from Elasticsearch (findAll uses ES scan/scroll)
$cursor = $this->esClient->findAll();

foreach ($cursor as $data) {
$ebooks[] = $this->hydrateEbook($data);
}

return $ebooks;
}
`

위의 코드는 제너레이터를 사용하지 않는 코드로, 두 출처에서 가져온 데이터를 배열로 합쳐서 반환합니다.


`private function getEbooks()
{
// fetch from the DB
$stmt = $this->db->prepare("SELECT * FROM ebook_catalog");
$stmt->execute();
$stmt->setFetchMode(\PDO::FETCH_ASSOC);

foreach ($stmt as $data) {
yield $this->hydrateEbook($data);
}

// and from Elasticsearch (findAll uses ES scan/scroll)
$cursor = $this->esClient->findAll();

foreach ($cursor as $data) {
yield $this->hydrateEbook($data);
}
}
`

제너레이터를 적용한 코드입니다. 단지 배열에 넣는 부분이 yield 구문으로 바뀌고 배열을 반환하는 구문이 제거된 차이밖에 없습니다. ‘쉽게’ 이터레이터를 구현한다는 표현이 조금 와닿는 예제인 것 같습니다. 글에서는 한 단계 더 나아가 다음과 같이 리팩토링 합니다.


`private function getEbooks()
{
yield from $this->getEbooksFromDatabase();
yield from $this->getEbooksFromEs();
}

private function getEbooksFromDatabase()
{
$stmt = $this->db->prepare("SELECT * FROM ebook_catalog");
$stmt->execute();
$stmt->setFetchMode(\PDO::FETCH_ASSOC);

foreach ($stmt as $data) {
yield $this->hydrateEbook($data);
}
}

private function getEbooksFromEs()
{
// and from Elasticsearch (findAll uses ES scan/scroll)
$cursor = $this->esClient->findAll();

foreach ($cursor as $data) {
yield $this->hydrateEbook($data);
}
}
`

yield from 구문은 PHP 7에 추가된 제너레이터 델리게이션이는 기능으로, 다른 제너레이터에서 yield된 값을 yield 합니다.


제너레이터를 수동으로 조작하기


앞서 예제에서 봤듯이 제너레이터는 foreach(), for() 같은 반복구문으로 처리할 수 있습니다. 상황에 따서는 반복구문으로 처리하지 않고 직접 데이터를 뽑아 써야할 경우가 생길 수 있습니다.


`<?php
function test()
{
yeild <span class="il">1</span>;
yield 2;
yield 3;
}

$generator = test();

dump($generator[0]);
`

위 코드는 에러가 납니다. 배열이 아니기 때문에 [0] 인덱스를 사용할 수 없습니다. 이터레이터이기 때문에 current() 매서드로 값을 가져오고 다음 값을 가져오려면 next()를 출한 후 다시 current()를 출해야 합니다.


`<?php
function test()
{
yeild <span class="il">1</span>;
yield 2;
yield 3;
}

$generator = test();

dump($generator->current());

$generator->next(); // 다음 값으로 커서를 이동시킵니다.
dump($generator->current());
`

마치며


예전부터 느낀건데 제너레이터를 쓰면 좋은 건 알겠는데, 언제 쓰면 좋을지 잘 모르겠더고요. 막상 코드를 짜다보면 귀찮기도 해서 그냥 배열로 짜기 수고. 이번에 제너레이터에 대해 다시 정리하면서 조금은 감이 온 것 같은데 그래도 여전히 실전에서 잘 떠올릴 수 있을지 모르겠습니다. 저 나름대로는 한 가지 명확한 상황은 정리가 되었습니다. ‘반복처리할 배열을 생성할 이 있으면 제너레이터를 쓰는게 좋겠다’ 입니다.


오늘 자료를 찾아보면서 제너레이터를 쓰면 좋을 만한 상황에 대한 몇가지 힌트는 얻을 수 있었습니다.



  1. 데이터베이스에서 대량의 데이터를 가져와서 처리할 때

  2. fopen, fgets 등으로 파을 열어 데이터를 처리할 때

  3. CSV, xlsx 등의 데이터를 처리할 때

  4. 디렉터리에서 파을 찾아 처리할 때


제 경우 2,3,4 같은 경우는 별로 자주 발생하지 않았고 1번이 가장 자주 접하게 될 상황 같은데, 1번의 경우 기존에 벨 쿼리빌더의 cursor()를 활용한다던가, 새로 나온 레이지 컬렉션등을 사용하면 되고 굳이 직접 제너레이터를 사용하진 않아도 될 것 같아요. 내은 레이지 컬렉션에 대해 알아보도록 하겠습니다!


1 1  51

2019년 9월 10


이현석

메쉬 코리아 개발자. 바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 2020년에 출간될 Laravel Up & Running 2nd Edition을 번역하고 있습니다.