엘로퀀트 모델로 대량의 데이터 처리하기 chunk(), cursor(), each() free

2019-08-26

엄청나게 큰 데이터베이스를 다룬다면 생각지도 못한 곳에서 메모리 부족 이슈가 발생할 수 있습니다. 데이터베이스 조회 결과가 수십 만개경우 이를 메모리에 올리지 못하는 것이죠. 애플리케이션을 개발하는 초기에는 데이터가 얼마 없어서 문제 없이 동작하다가 실제 서비스에 들어가서 데이터가 누적되면 이와 관련된 문제가 발생하곤 합니다.


라라벨은 많은 양의 데이터를 처리하는 기능을 제공합니다. chunk(), cursor(), each() 가 그것인데요, 각각의 매서드를 알아보도록 하겠습니다.


chunk()


chunk() 매서드는 쿼리 결과를 한꺼번에 가져오는 대신 사용자가 지정한 수 만큼씩 분할해서 가져옵니다.


Flight::chunk(200, function ($flights) {
foreach ($flights as $flight) {
//
}
});

위와 같이 하면 200개씩 가져옵니다. 가져온 데이터는 $flights에 담아 클로저로 넘겨서 처리합니다.


each()


여기서 이야기하는 each()는 엘로퀀트 모델의 것을 뜻합니다. 컬렉션의 each() 매서드와는 다른 것입니다.


스크린샷 2019-08-26 오후 2.59.14.png


라라벨은 each() 매서드를 청크 + 아이템별 콜백 실행으로 설명하고 있습니다.


프릭 반 더 허르텐은 each()를 chunk()와 같은을 하지만 더 고상한 방고 이야기 합니다.


image


each()는 두번째 인자로 청크할 갯수를 받는데, 필수는 아니며 기본값은 1000 입니다. 앞서 chunk() 매서드의 예제를 each()로 구현하면 다음과 같습니다.


Flight::each(function ($flight) {

}, 200);

자세히 보면 chunk()는 컬렉션을 넘기고 클로저에서 foreach()로 개별 아이템을 처리하는 반면, each()는 바로 모델을 넘기는 걸 알 수 있습니다. 코드가 더 간략해지긴 하네요.


cursor()


cursor()는 쿼리를 수행한 후 결과를 한꺼번에 fetch하는게 아니제너레이터로 하나씩 fetch하는 방으로 메모리 사용량을 줄입니다.


foreach (Flight::where('foo', 'bar')->cursor() as $flight) {
//
}

성능 비교


ryo511 이는 Qiita 사용자가 chunk()와 cursor() 성능을 비교해서 공개한 바 있습니다.(2016년이조금 오래된 자료긴 합니다.)


ryo511의 결론은 cursor()는 빠르고, chunk()는 메모리를 적게 사용한다 입니다.

+--------------+------------+------------+

| | Time(sec) | Memory(MB) |

+--------------+------------+------------+

| get() | 0.8 | 132 |

| chunk(100) | 19.9 | 10 |

| chunk(1000) | 2.3 | 12 |

| chunk(10000) | 1.1 | 34 |

| cursor() | 0.5 | 45 |

+--------------+------------+------------+


마치며


우선은 쿼리 하나에 데이터가 수십만개씩 조회되는 서비스를 만들고 싶네요. 하핫

개인적으로는 get()은 되도록 자제하고 each()나 cursor()를 습관적으로 사용해볼까 합니다. 위의 성능 비교를 보니 each()는 사용자가 사용하는 기능에 주로 사용하고, cursor()는 관리자가 사용하는 기능에 어울릴 것 같아요. 혹시 의견있으시면 회신 부탁드려요~








1일1식라라벨 40호

2019년 8월 26


이현석

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