콘텐츠로 이동

메모리와 스트리밍

Spec: ISO 32000-2, §7.5.4 Evidence: Mixed evidence

대용량 PDF라고 해서 큰 메모리가 필요해서는 안 됩니다. 이 페이지에서는 문서가 커질 때 NextPDF가 어떻게 프로세스 힙을 제한된 범위 안에 유지하는지, 어디에서 메모리에 누적하는 대신 디스크로 스트리밍하는지, 그리고 여기서 “성능 예산”이 무엇을 의미하는지 설명합니다. 이는 헤드라인 수치가 아니라 검증되는 계약입니다.

PDF 형식은 생성기가 많은 메모리를 사용하도록 강제하지 않습니다. 교차 참조 테이블이 모든 간접 객체의 바이트 오프셋을 기록하므로 리더는 전체 파일을 메모리에 두지 않고 파일에 임의 접근할 수만 있으면 됩니다. 생성기도 이를 그대로 반영할 수 있습니다. 객체가 완성되는 대로 내보내고 그 객체가 어디에 놓였는지만 기억하면 됩니다. 반대로 전체 문서가 최종 쓰기 시점까지 힙에 남아 있으면 페이지 수에 따라 메모리가 선형적으로 증가하며, 백 페이지일 때는 문제없던 보고서가 오만 페이지일 때는 프로세스 실패를 일으킵니다.

배치 및 워커 워크로드에서는 이것이 안정적인 서비스와 부하가 걸렸을 때 예측 불가능하게 실패하는 서비스를 가르는 차이입니다. 제한된 메모리는 바라는 수치가 아니라 설계로 구현해야 하는 속성입니다.

  • 스트리밍 라이터는 메모리를 문서당 제한된 범위 안에 유지하도록 설계되었습니다. 각 페이지는 확정되는 즉시 출력에 기록됩니다. 그런 다음 해당 버퍼가 해제됩니다.
  • 그렇지 않으면 객체 수에 따라 늘어날 부기 정보, 즉 교차 참조 오프셋과 페이지 트리 Kids 참조는 php://temp/maxmemory:0으로 연 임시 스트림에 기록되며, 이 스트림은 PHP 힙을 채우는 대신 즉시 디스크로 넘깁니다.
  • 설계 목표는 페이지당 O(1) 힙입니다. 즉 페이지가 추가되어도 문서를 보유하는 비용이 더 늘지 않습니다. 이것이 라이터가 현재 형태로 설계된 엔지니어링 목표입니다.
  • 성능 예산은 문서화 시스템 안에 실제로 존재하는 구조화된 개념입니다. 즉 검증되는 계약으로 표현된 벽시계 시간 상한과 최대 메모리 상한입니다. 이는 하나의 의무를 명시합니다. 벤치마크 결과가 아닙니다.
  • 구체적인 수치는 살아 있는 신호로 취급되어, 명시된 방법에 따라 측정되며, 조용히 낡아 버릴 수 있는 산문 속에 고정되지 않습니다.

스트리밍 라이터의 전체 구조는 한 가지 결정에서 비롯됩니다. 내보낼 수 있는 것은 절대 붙잡아 두지 않는다는 것입니다.

  1. Start page A single active cursor; no document-wide page graph in memory.
  2. Finalise page Page content + page object written straight to the output stream.
  3. Release buffer The finalised page buffer is dropped; the heap returns to baseline.
  4. Record offset to disk Xref and Kids entries go to php://temp/maxmemory:0 — immediate disk spill.
  5. Close Pages-tree root, Catalog, and trailer written once at the end.
스트리밍 라이터의 페이지별 주기. 각 페이지가 내보내지고 해제되며, 증가하는 부기 정보는 디스크 기반 임시 스트림으로 보내지므로 힙이 페이지 수에 따라 증가하지 않습니다.

디스크로 넘기는 세부 사항이 가장 핵심적인 부분입니다. PHP의 php://temp는 소량을 메모리에 유지하다가 임계값을 초과할 때만 디스크로 넘깁니다. 라이터는 그 임시 스트림을 maxmemory:0 옵션으로 엽니다. 이 옵션은 스트림이 즉시 디스크로 넘어가도록 강제합니다. 즉 메모리 내 임계값이 0입니다. 실질적인 효과는, 정의상 문서와 함께 증가하는 객체별 부기 정보가 힙에 결코 누적되지 않는다는 것입니다. 대신 크기가 제약되지 않는 디스크에 누적됩니다. 이 옵션이 없으면 기본 메모리 내 윈도우가 가득 찬 뒤에야 디스크로 넘어가는데, 이는 제한된 메모리 목표가 가장 중요한 순간에 바로 그 목표를 무너뜨립니다.

성능 예산은 이야기의 나머지 절반이며, 마케팅 주장이 아니라 문서화 시스템의 계약입니다. 스키마는 예산을 두 개의 제한된 정수로 정의합니다. 즉 밀리초 단위의 벽시계 시간 상한과 메비바이트 단위의 최대 상주 메모리 상한입니다. 예산을 선언하는 레시피는 검증할 수 있는 의무를 선언하며, 이는 타입이 지정된 시그니처가 컴파일러가 검증할 수 있는 의무를 선언하는 것과 같습니다. 예산의 가치는 값이 작다는 데 있지 않고 명시되고 강제된다는 데 있습니다.

이 페이지는 Evidence: Mixed evidence 이며, 이 혼합은 의도적입니다. 근거가 실제로 세 가지 종류이기 때문입니다.

  • 코드로 뒷받침되는 메커니즘. src/Writer/Streaming/StreamingPdfWriter.php의 스트리밍 라이터는 페이지별 내보내기-후-해제 주기를 문서화하고 구현하며, xref 및 Kids 스트림을 php://temp/maxmemory:0으로 열어 즉각적인 디스크 넘김을 강제함으로써 “PHP 메모리가 객체 수와 무관하게 제한된 상태로 유지됩니다”라는 속성을 뒷받침합니다. 스트리밍 방식의 단일 커서와 트리를 보유하지 않는 설계는 ADR-001에 기록된 아키텍처 결정이기도 합니다(렌더링 파이프라인은 O(n) 노드가 아니라 최대 O(depth) 상태만 보유합니다).
  • 설계 원칙으로서의 예산. performance_budget 필드는 문서화 스키마에 실제로 존재하는 선택적 구성 요소이며, 명시적인 상한과 함께 { wall_ms, peak_mb }로 정의됩니다. 이는 설계상 강제 가능한 계약입니다.
  • 살아 있는 신호로서의 벤치마크. ADR-001은 통제된 대용량 문서의 최대 메모리와 벽시계 시간 수치가 명시된 방법에 따라 수집되고 기록되어야 하는 경험적 목표이며, 산문에서 단언되는 수치가 아님을 분명히 밝힙니다. 따라서 이 페이지는 메커니즘과 계약을 명시하고, 구체적인 수치는 그것을 측정하는 곳을 가리킵니다.

이 형식 덕분에 그 목표는 막연한 바람이 아니라 합리적인 목표가 됩니다. 교차 참조 테이블이 객체별 오프셋 인덱스이기 때문에, 즉 Spec: ISO 32000-2, §7.5.4 , 생성기는 객체를 완성하는 대로 기록하고 그 오프셋만 보관할 수 있습니다. 제한된 메모리는 파일 형식과 충돌하는 것이 아니라 그 형식과 일관됩니다.

제한된 메모리는 설정으로 켜는 플래그가 아니라 생성 방식의 속성입니다. 각 문서를 확정한 뒤 해제하는 배치 루프는 실행 내내 힙을 평탄하게 유지합니다.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Core\PdfFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// Process-lifetime, shared once.
$factory = PdfFactory::new()
->withCompress(true)
->withDocumentFactory(new DocumentFactory(
new FontRegistry(),
new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024),
));
// Per-document, created and released each iteration.
foreach ($invoiceBatch as $invoice) {
$doc = $factory->create();
$doc->addPage();
$doc->writeHtml($invoice->toHtml());
$doc->save($invoice->outputPath());
unset($doc); // the document model is not carried into the next iteration
}

레지스트리를 공유하는 이유는 폰트와 이미지를 한 번만 파싱하는 것이 워커에서 핵심이기 때문입니다. 문서는 공유되지 않으며 매회 해제됩니다. 이것이 배치 메모리를 배치 전체가 아니라 문서 하나로 제한하는 요소입니다.

가장 흔한 오해는 “제한된 메모리”를 벤치마크 주장으로 다루는 것, 즉 인용할 메가바이트 수치를 기대하는 것입니다. 이는 이 페이지의 취지를 거꾸로 이해한 것입니다. 여기서의 보장은 구조적입니다. 라이터는 페이지가 추가되어도 문서를 보유하는 비용이 더 들지 않도록 만들어졌습니다. 구체적인 최댓값 수치는 페이지 내용, 폰트, 이미지에 따라 달라지며 측정 방법이 함께 제시될 때만 의미가 있습니다. 그래서 그 수치는 이 문장이 아니라 벤치마크에 속합니다.

두 번째 함정은 php://temp가 이미 여러분을 보호한다고 가정하는 것입니다. 실제로 보호하긴 합니다. 다만 기본 메모리 내 윈도우가 가득 찬 뒤에야 그렇습니다. 디스크 넘김을 즉각적으로 만드는 것은 maxmemory:0 옵션입니다. 이 세부 사항이 곧 메커니즘입니다. 이 옵션이 없으면 그 속성은 정작 필요한 대용량 문서에서 성립하지 않습니다.

이 페이지는 스트리밍 메커니즘과 성능 예산의 의미를 설명합니다. 이 페이지는 측정된 최대 메모리나 처리량 수치를 명시하지 않습니다. 그러한 수치는 선언된 방법에 따른 벤치마킹 규율에서 산출되며, ADR-001은 경험적 수치를 그 측정에 명시적으로 위임합니다. “문서당” 제한된다는 것이 단일 문서의 내용과 무관하게 일정하다는 뜻은 아닙니다. 대형 임베디드 이미지가 많은 페이지는 여전히 그 이미지가 드는 만큼의 비용이 듭니다. 증가하지 않는 것은 페이지별 부기 정보와 보유되는 페이지 그래프입니다. 모든 생성 경로가 스트리밍 라이터인 것은 아닙니다. 어떤 경로가 스트리밍하고 어떤 경로가 버퍼링하는지는 이 개요가 아니라 코드와 파이프라인 구조가 결정합니다. 설명된 메커니즘은 이 페이지의 검토일 기준으로 정확합니다. 권위 있는 출처는 코어 저장소의 src/Writer/Streaming/와 ADR-001입니다.

스트리밍 및 제한된 메모리 설계는 Core의 속성입니다. 에디션이 이를 바꾸지 않습니다.

Bounded-memory streaming writer — edition availability
Edition Availability
Core Core는 스트리밍 방식의 디스크 넘김 라이터 설계를 제공합니다.
Pro Pro는 동일한 제한된 메모리 라이터를 상속하며, 다른 메모리 모델이 아니라 기능을 추가합니다.
Enterprise Enterprise는 동일한 제한된 메모리 라이터를 상속하며, 다른 메모리 모델이 아니라 기능을 추가합니다.
  • 제한된 메모리 — 페이지가 추가되어도 문서를 보유하는 데 더 많은 힙을 쓰지 않는 설계 속성(페이지당 O(1) 목표).
  • 스트리밍 라이터 — 전체 문서를 보유하는 대신 각 페이지를 출력으로 내보내고 그 버퍼를 해제하는 라이터.
  • php://temp/maxmemory:0 — 즉시 디스크로 넘기도록 강제된 PHP 임시 스트림으로, 증가하는 객체별 부기 정보에 사용됩니다.
  • 성능 예산 — 구조화된 문서화 계약. 즉 명시되고 검증 가능한 벽시계 시간 상한과 최대 메모리 상한입니다.
  • 살아 있는 신호 — 산문에 박혀 있는 고정된 수치가 아니라, 명시된 조건에서 그 방법과 함께 보고되는 측정값.