PHP 애플리케이션을 운영하다 보면 갑자기 'Fatal error: Allowed memory size of X bytes exhausted' 에러가 터진다. 서버는 멀쩡한데, 갑자기 메모리가 부족하다는 것. 대부분의 개발자는 단순히 php.ini의 memory_limit을 올리고 문제를 덮어버린다. 하지만 이건 근본 해결이 아니다. 이번에는 왜 메모리가 부족해지는지, 어디서 메모리를 낭비하는지, 그리고 정말로 설정을 바꿔야 할 때는 언제인지를 정확히 정리해서 소개하겠다.

 

1. PHP 메모리 제한의 정체 파악하기

PHP는 보안상의 이유로 스크립트 하나가 사용할 수 있는 메모리에 상한선을 둔다. 기본값은 보통 128MB다. 이 한계를 넘으면 즉시 스크립트가 죽는다. 그런데 여기서 주목할 점이 있다. 메모리 제한은 스크립트 전체에 적용되는 게 아니라, 각 요청(request)마다 독립적으로 적용된다는 것이다.

예를 들어 Apache에서 10개의 워커 프로세스가 동시에 PHP를 실행하고 있다면, 각 프로세스가 128MB씩 사용할 수 있다. 즉, 총 1280MB가 필요할 수 있다는 뜻이다. 하지만 개발자들은 이런 메커니즘을 모르고 메모리 부족이 발생하면 memory_limit을 1GB로 올려버린다. 서버 상황에 맞지 않는 설정이다.

 

2. 메모리 누수의 흔한 원인들

무제한으로 메모리를 사용하는 코드 패턴이 있다. 가장 흔한 게 반복문에서 배열을 계속 쌓는 것이다.

✗ 잘못된 코드
$data = [];
while(true) {
    $result = $db->query("SELECT * FROM large_table");
    $data = array_merge($data, $result);
}

이 코드는 데이터베이스에서 계속 조회하면서 결과를 배열에 쌓는다. 메모리가 가득 찰 때까지 멈추지 않는다. 진짜 문제는 데이터를 처리하지 않으면서 그냥 쌓기만 한다는 것이다.

✓ 올바른 코드 (메모리 효율적)
$stmt = $db->query("SELECT * FROM large_table");
foreach($stmt as $row) {
    processRow($row);  // 행 하나씩 처리
    unset($row);       // 즉시 메모리 해제
}

PDO의 fetch 모드를 반복하거나 Generator를 사용하면, 한 번에 하나씩만 메모리에 올려진다. 메모리 사용량이 극적으로 줄어든다.

또 다른 흔한 경우는 무한 문자열 연결이다.

✗ 잘못된 코드
$output = "";
for($i = 0; $i < 1000000; $i++) {
    $output .= "line " . $i . "\n";
}

PHP에서 문자열은 불변이다. 매번 새로운 문자열을 만들고 이전 문자열은 메모리에 버려진다. 반복 횟수가 많으면 메모리가 기하급수적으로 증가한다.

✓ 올바른 코드 (메모리 효율적)
$output = [];
for($i = 0; $i < 1000000; $i++) {
    $output[] = "line " . $i;
}
echo implode("\n", $output);

배열에 문자열을 담아두었다가 한 번에 join하면, 버려지는 문자열이 없다. 속도도 빠르고 메모리도 절약된다.

 

3. PHP ini 메모리 설정 올바르게 이해하기

memory_limit은 스크립트가 할당받을 수 있는 최대 메모리다. 이것만으로는 부족하다. 추가 설정이 필요하다.

설정 항목 역할 권장값
memory_limit 스크립트 하나가 사용 가능한 최대 메모리 256M~512M (워커당)
post_max_size POST 요청 바디 최대 크기 memory_limit보다 작게
upload_max_filesize 파일 업로드 최대 크기 post_max_size보다 작게
max_execution_time 스크립트 실행 시간 제한 (초) 30~300 (배치는 더 길게)

잘못된 설정 예를 보자.

✗ 잘못된 php.ini
memory_limit = 1G
post_max_size = 2G
upload_max_filesize = 3G
max_execution_time = 0

이렇게 설정하면 메모리 부족 오류를 피할 수 있지만, 문제가 가려질 뿐 해결되지 않는다. 또한 메모리 낭비 코드가 무한정 실행되면서 다른 사용자의 요청을 처리하지 못한다.

✓ 올바른 php.ini
memory_limit = 256M
post_max_size = 50M
upload_max_filesize = 50M
max_execution_time = 60

제한을 명확히 설정하면 문제 코드가 즉시 눈에 띈다. 개발 단계에서 발견할 수 있다.

 

4. OPcache 설정이 메모리 누수를 악화시키는 경우

PHP-FPM과 OPcache를 함께 쓸 때, 잘못된 설정이 메모리 부족을 초래할 수 있다. OPcache는 컴파일된 PHP 코드를 메모리에 캐시하는 기능이다. 하지만 캐시 크기를 너무 크게 설정하면 실제 애플리케이션이 사용할 메모리가 줄어든다.

✗ 잘못된 opcache 설정
opcache.memory_consumption=512    ; 512MB 캐시
php memory_limit = 256M
PHP-FPM 워커 수 = 20

이 경우 총 필요 메모리는 최소 (512 + 256*20) = 5632MB다. 서버에 8GB만 있으면 부족하다.

✓ 올바른 opcache 설정
opcache.memory_consumption=128    ; 128MB 캐시 (프로젝트 크기에 맞게)
opcache.validate_timestamps=1      ; 개발 중엔 켜기
opcache.revalidate_freq=60         ; 60초마다 재검증
php memory_limit = 256M
PHP-FPM 워커 수 = 10               ; 워커 수도 조절

실제로 프로젝트의 PHP 파일 크기를 확인하고 그에 맞는 캐시 크기를 정하는 게 중요하다.

du -sh /var/www/html/*.php

만약 PHP 파일이 50MB라면, OPcache 메모리는 최소 64~128MB면 충분하다. 512MB는 낭비다.

 

5. 실제 메모리 사용량 디버깅하기

현재 스크립트가 얼마나 많은 메모리를 쓰고 있는지 확인하려면 memory_get_usage()를 쓴다.

echo memory_get_usage() / 1024 / 1024 . " MB\n";           // 현재 사용 메모리
echo memory_get_peak_usage() / 1024 / 1024 . " MB\n";    // 최대 사용 메모리

더 정확한 분석을 원하면 xdebug를 활용한다. 또는 프로파일링 도구로 메모리 할당을 추적할 수 있다.

php -d display_errors=1 -d memory_limit=512M script.php

스크립트 실행 중에 메모리 사용량을 기록해두고, 어느 부분에서 급증하는지 찾는다.

✓ 메모리 사용량 체크포인트
echo "Initial: " . memory_get_usage(true) / 1024 / 1024 . " MB\n";

$large_array = range(1, 1000000);
echo "After array: " . memory_get_usage(true) / 1024 / 1024 . " MB\n";

unset($large_array);
echo "After unset: " . memory_get_usage(true) / 1024 / 1024 . " MB\n";

실행 결과가 예상치 못하게 높다면, 그 부분의 알고리즘을 다시 생각해봐야 한다.

 

6. 주의사항과 흔한 실수

✗ memory_limit을 무한정 올리기. 이건 증상만 숨기고 근본 원인을 방치하는 것이다. 언젠가는 서버 메모리가 부족해진다.

✓ 먼저 코드 리뷰로 메모리 누수 지점을 찾고, 그래도 부족하면 그때만 설정을 조정한다.

✗ 배치 작업이 memory_limit을 자주 넘긴다고 해서 max_execution_time도 0으로 설정하기. 이렇게 하면 무한 루프가 생겨도 서버가 계속 실행한다.

✓ 배치 작업은 별도의 php.ini 설정을 따로 만들어서, 메모리와 타임아웃을 적절히 높이되 무한정 높이지는 않는다.

✗ Generator를 쓸 때도 중간에 배열로 변환하기. 이렇게 하면 Generator의 메모리 절약 이점이 없어진다.

✓ Generator는 처음부터 끝까지 반복만 하고, 중간에 변환하지 않는다.

 

핵심 정리

memory_limit 에러는 대부분 코드의 문제다. 무한정 배열을 쌓거나, 문자열을 반복해서 연결하거나, 대용량 데이터를 한 번에 로드하는 식이다. 이런 문제를 찾지 않고 memory_limit만 올리면, 서버가 실제로 메모리 부족에 빠졌을 때 아무런 경고 없이 죽어버린다.

작은 개선이 모여서 큰 효과를 만든다는 점을 잊지 말자. 배열 대신 Generator를 쓰거나, 문자열을 implode로 묶거나, 대용량 파일을 한 줄씩 읽는 이런 습관들이 축적되면, 메모리 부족 문제는 거의 없어진다. 이 글의 '메모리 누수의 흔한 원인들' 섹션을 참고해서, 지금 운영 중인 애플리케이션에서 문제가 될 만한 패턴이 있는지 한 번 검토해보자. 그러면 다음번 메모리 부족 오류는 훨씬 쉽게 대응할 수 있을 것이다.