PHP로 수십만 건의 대용량 DB 레코드나 수백 메가바이트(MB)에 달하는 CSV 파일을 처리하다가 Allowed memory size exhausted 에러를 마주친 경험이 있을 것이다.
다만 대부분의 개발자들은 php.ini의 memory_limit 수치를 무작정 256M, 1G로 올리는 임시방편으로 해결하려고 하거나 정확한 메모리 점유 원인을 모르는 경우가 많다.
이번에는 PHP의 메모리 효율성을 극적으로 올려주는 Generator와 yield 키워드가 정확히 무엇인지, 왜 필요한지, 그리고 메모리 고갈 없이 대용량 데이터를 처리하는 방법을 완벽하게 정리해서 소개하겠다.
제너레이터(Generator)는 이터레이터(Iterator) 객체를 아주 손쉽게 구현할 수 있도록 도와주는 PHP의 특별한 기능이다.
일반적인 함수는 실행이 끝나면 결과값 전체를 배열이나 객체 형태로 반환(return)하고 메모리에서 소멸하지만, 제너레이터 함수는 yield 키워드를 만나면 실행 상태를 일시 중지하고 값을 하나씩 호출자에게 전달한다.
즉, 100만 개의 리스트를 만들 때 일반 함수는 100만 개 항목 전체를 메모리에 적재한 후 한 번에 반환하지만, 제너레이터는 반복문이 돌 때마다 딱 1개의 값만 메모리에 올리고 사용이 끝나면 버린다. 이를 지연 평가(Lazy Evaluation) 방식이라고 부른다.
두 방식의 주요 차이점을 한눈에 정리하면 다음과 같다.
| 비교 항목 | 일반 배열 반환 방식 | Generator (yield) 방식 |
|---|---|---|
| 메모리 점유량 | 전체 데이터 크기에 비례하여 증가 (비효율) | 항목 1개 수준의 최소 메모리 유지 (최적) |
| 데이터 생성 시점 | 함수 호출 시 모든 데이터를 즉시 메모리에 적재 | foreach 순회 시 요청할 때마다 1개씩 생성 (Lazy) |
| 데이터 재사용 | 반환된 배열을 여러 번 자유롭게 순회 가능 | 기본적으로 단일 순회만 가능 (Rewind 불가) |
| 적합한 활용처 | 소규모 데이터, 필터링 및 복잡한 배열 조작 필요 시 | 대용량 CSV 파일 읽기, 대규모 DB 커서 조회, 스트리밍 |
실무에서 자주 접하는 대용량 CSV 파일 읽기 예제로 두 방식의 코드 구조와 메모리 사용량 차이를 확인해 보자.
파일 크기가 클수록 메모리 사용량이 급증하여 Allowed memory size exhausted 에러가 발생하게 된다.
<?php
function readCsvToArray(string $filePath): array {
$handle = fopen($filePath, 'r');
$data = [];
// 파일 전체 데이터를 메모리 배열에 쌓음 (위험)
while (($row = fgetcsv($handle)) !== false) {
$data[] = $row;
}
fclose($handle);
return $data;
}
// 10만 건 CSV 읽기 시도
$rows = readCsvToArray('large_data.csv');
foreach ($rows as $row) {
// 데이터 처리 작업
}
echo "피크 메모리 사용량: " . round(memory_get_peak_usage(true) / 1024 / 1024, 2) . " MB
";
?>
한 번에 한 줄씩만 메모리에 올려 처리하므로 파일 용량이 1GB든 10GB든 메모리 사용량이 약 몇 megabyte 수준으로 일정하게 유지된다.
<?php
function readCsvWithGenerator(string $filePath): Generator {
$handle = fopen($filePath, 'r');
if (!$handle) {
return;
}
try {
while (($row = fgetcsv($handle)) !== false) {
yield $row; // 한 줄씩 반환하고 함수 실행을 일시 중지
}
} finally {
fclose($handle); // 순회가 완전히 끝나거나 중간에 break 되어도 핸들 닫기
}
}
// 10만 건 CSV 읽기 시도 (메모리 변화 최소화)
$rows = readCsvWithGenerator('large_data.csv');
foreach ($rows as $row) {
// 데이터 처리 작업
}
echo "피크 메모리 사용량: " . round(memory_get_peak_usage(true) / 1024 / 1024, 2) . " MB
";
?>
# ✗ 잘못된 방식 (배열 적재)
피크 메모리 사용량: 64.25 MB
# ✓ 올바른 방식 (Generator 사용)
피크 메모리 사용량: 2.00 MB
Generator를 다룰 때 실무 개발자들이 흔히 범하는 실수와 주의해야 할 점이다.
1) Generator는 다시 순회(Rewind)할 수 없다.
제너레이터 객체는 일회성 데이터 스트림이다. foreach로 한 번 순회를 끝낸 제너레이터 변수를 다시 foreach문에 넣으면 Cannot traverse an already closed generator 에러가 발생한다. 동일한 데이터를 다시 순회해야 한다면 제너레이터 함수를 새로 호출하여 새로운 객체를 생성해야 한다.
2) array_map(), array_filter() 등 표준 배열 함수와 직접 호환되지 않는다.
제너레이터는 배열이 아니라 Generator 객체이므로 array_map()이나 count() 같은 배열 전용 함수에 직접 넘길 수 없다. 필요하다면 iterator_to_array()를 사용할 수 있지만, 그 순간 모든 데이터가 다시 배열로 메모리에 로드되어 제너레이터의 이점이 사라진다. 배열 변환 대신 제너레이터 내부에서 직접 조건을 걸어 yield 하거나 별도의 제너레이터 처리 함수를 체이닝해야 한다.
PHP에서 대용량 데이터를 처리할 때 Generator와 yield는 메모리 고갈 문제를 근본적으로 해결하는 핵심 기술이다. 무작정 서버의 memory_limit 한도를 늘리는 방법은 임시방편일 뿐, 데이터가 더 커지면 결국 동일한 서버 장애로 이어진다.
대용량 데이터 처리는 메모리 효율적인 코드 작성 구조가 핵심이다. 개발 과정에서 작성하는 작은 최적화 습관이 모여서 서버의 안정성과 전체 시스템의 성능이라는 큰 효과를 만든다는 점을 잊지 말자. 이 글의 CSV 읽기 예제나 DB 커서 처리 방식을 프로젝트에 적용해 보면, 서버 리소스를 극적으로 절약하는 최종 결과를 얻을 수 있을 것이다.