콘텐츠로 이동

대용량 문서 생성

Spec: ISO 24495-1:2023, §5 Spec: ISO 9241-112:2025, §6.1.2.3 Evidence: Benchmark-backed

PDF 한 개를 생성하는 일은 함수 호출입니다. 정해진 일정 안에 10 만 개를 생성하는 일은 시스템 문제입니다. 메모리는 한정된 범위 안에 머물러야 하고, 작업은 병렬로 처리되어야 하며, 측정값은 의미가 있어야 합니다. 이 페이지는 처리량 질문에서 출발해 부하를 견디는 배포에 이르기까지 배치 생성 시나리오를 따라갑니다. 솔직한 답은 대표 수치가 아니라 “여러분의 문서로 직접 측정하라”는 것임을 분명히 밝힙니다.

배치 생성은 두 가지 전형적인 방식으로 실패합니다. 첫 번째는 메모리 누적입니다. 오래 살아 있는 워커는 문서를 처리할 때마다 보유 상태를 쌓아 가다가 배치 도중 종료되며, 그 실행은 완료된 것도 아니고 깔끔하게 실패한 것도 아닙니다. 두 번째는 확신에 차 있지만 의미 없는 수치입니다. 사소한 문서로 측정한 벤치마크가 복잡한 문서를 렌더링하는 서버군의 규모를 산정하는 데 쓰이고, 그것이 틀렸다는 사실은 프로덕션 부하에서야 드러납니다.

두 가지 모두 피할 수 있지만, 첫 장애를 겪은 뒤 덧붙이는 방식이 아니라 처음부터 메모리 구조와 측정 방법을 설계에 반영할 때만 가능합니다.

  • 작업 단위는 공유되는 문서가 아니라 폐기 가능한 문서입니다. 프로세스 수명 동안 유지되는 데이터(폰트, 이미지 캐시)는 공유 레지스트리에 두고, 문서는 렌더링마다 생성하고 폐기하십시오.
  • 메모리에는 두 부분이 있으며, 오래 살아 있는 워커에 중요한 것은 그중 하나뿐입니다. 렌더링 중의 일시적인 피크는 예상된 것이지만, 다시 돌아오지 않는 유지된 메모리가 바로 배치를 무너뜨리는 누수입니다.
  • 처리량은 병렬성뿐 아니라 한정된 렌더링당 비용에서 나옵니다. 부하를 견디는 형태는 큐가 상태 없는 워커들에게 작업을 공급하고, 각 워커가 렌더링한 뒤 해제하는 것입니다.
  • 방법이 빠진 수치는 수치가 아닙니다. NextPDF는 렌더링당 측정값을 여러분이 수집하는 데이터로 보고하며, 맥락 없는 속도 주장을 거부합니다. 가장 중요한 수치는 여러분 자신의 템플릿으로 측정한 수치입니다(ISO 24495-1 §5.x11 — 중요한 메시지를 독자가 찾는 곳에 두십시오).

이 아키텍처는 하나의 결정을 중심으로 구축됩니다. 프로세스 수명 동안 살아 있는 상태는 공유되며 변경 불가능하고, 렌더링 동안만 살아 있는 상태는 새로 만들어 폐기됩니다. 폰트는 한 번 파싱한 뒤 잠기는 구조적 데이터이므로, 어떤 렌더링도 폰트를 변경해 다음 렌더링을 오염시킬 수 없습니다. 이미지 캐시는 잠그지 않는, 한정된 LRU(가장 오래 사용되지 않은 항목 우선 제거) 저장소이므로, 요청 사이로 메모리가 누수되지 않으면서도 상한선을 유지합니다. 문서 팩토리는 상태 없는 싱글턴이며, 그것이 생성하는 모든 문서는 폐기 가능합니다.

이 분리 덕분에 워커는 Octane, RoadRunner, Swoole에서 몇 시간 동안 안전하게 실행될 수 있습니다. 이는 “요청 N이 요청 N+1을 손상시키는” 실패 양상을, 문서가 스스로 초기화되기를 바라는 방식이 아니라 구조 자체로 제거합니다.

이 시나리오에는 네 단계가 있습니다.

  1. Warm the shared state once On worker boot, parse and lock the font registry and size the image cache. This cost is paid once, not per document.
  2. Enqueue the work A queue holds the render jobs. The queue is the throughput dial — workers scale horizontally behind it.
  3. Render on a disposable document Each worker creates a fresh document from the factory, renders, emits the bytes, and lets the document go.
  4. Measure, then size Collect per-render time and peak memory. Size the fleet from measurements on your own templates, not a generic figure.
대용량 시나리오를 처음부터 끝까지 보면, 공유되는 변경 불가능한 상태는 한 번만 예열되고, 각 작업은 폐기 가능한 문서에서 렌더링한 뒤 해제하며, 처리량은 하나의 워커를 키우는 것이 아니라 워커를 추가함으로써 확장됩니다.

프레임워크 브리지는 이 형태를 직접 조립하지 않아도 기본값으로 제공합니다. Laravel 서비스 제공자는 폰트 레지스트리를 예열하고 잠근 싱글턴으로 등록하며, 문서는 해석할 때마다 새 인스턴스로 바인딩합니다. 제한된 시도 횟수, 타임아웃, 지수 백오프를 갖춘 큐 작업도 함께 제공합니다. 그 작업은 워커 쪽에서 출력 경로를 검증합니다. 직렬화된 큐 페이로드는 전송 중에 변조될 수 있기 때문입니다. Symfony 및 CodeIgniter 통합도 동일한 폐기 가능한 문서, 공유 레지스트리 규율을 따릅니다.

메모리 모델은 코드로 뒷받침됩니다. Evidence: Code-backed Laravel NextPdfServiceProviderFontRegistry를 예열한 뒤 lock() 처리한 싱글턴으로 등록하고, ImageRegistry를 의도적으로 잠그지 않은 한정-LRU 싱글턴으로 등록하며, Document를 상태 없는 팩토리를 통해 해석할 때마다 바인딩합니다. 폐기 가능한 문서 모델은 설명 문구가 아니라 실제 배선 안에 들어 있습니다. GeneratePdfJobtries, timeout, backoff를 갖추고 handle() 안에서 출력 경로를 다시 검증합니다.

측정 표면은 벤치마크로 뒷받침됩니다. Evidence: Benchmark-backed 엔진은 생성마다 변경 불가능한 RenderReport를 발행하며, 여기에는 밀리초 단위 렌더링 시간, 피크 메모리(바이트 단위), 페이지 수, 경고 횟수, 폴백 발생 횟수가 담깁니다 — 서버군의 규모를 산정하는 데 필요한 정확한 입력값입니다. 별도의 메모리 단편화 분석기는 피크(일시적)와 유지된 메모리를 구별합니다. 그 구별은 오래 살아 있는 워커가 건강한지 아니면 서서히 누수되고 있는지 알려 줍니다. 벤치마크 하니스 자체는 예열을 거친 반복 실행으로 구성됩니다. 단 한 번의 타이밍은 잡음이기 때문입니다.

이 규율은 설계 원칙입니다. Evidence: Design principle NextPDF는 성능을 측정 방법과 함께 보고하며, 맥락 없는 속도 주장을 거부합니다. 이는 이 문서가 작성된 방식과 일치합니다 — Spec: ISO 24495-1:2023, §5 중요한 메시지를 독자가 찾을 곳에 둡니다. 여기서 중요한 메시지는 “여러분 자신의 워크로드를 측정하라”는 것입니다.

아래 코드는 측정을 포함한 폐기 가능한 문서 루프로 이 모델을 보여 줍니다. 엔진이 RenderReport를 생성합니다. 큐는 여러분의 인프라입니다.

<?php
declare(strict_types=1);
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Observability\RenderReport;
use Psr\Log\LoggerInterface;
/**
* One batch worker iteration: render, emit, release, measure.
*
* The factory and its registries are process-lifetime singletons; the
* document is disposable. Retained memory must return to baseline between
* iterations or the worker is leaking.
*
* @param iterable<int, callable(\NextPDF\Core\Document): \NextPDF\Core\Document> $jobs
*/
function runBatch(
DocumentFactoryInterface $factory,
LoggerInterface $logger,
iterable $jobs,
): void {
foreach ($jobs as $jobId => $build) {
$startedAt = hrtime(true);
// Fresh, disposable document — shares the warmed registries.
$doc = $factory->create();
$doc = $build($doc);
$bytes = $doc->getPdfData();
// Hand the bytes off to your sink (object store, response, etc.).
unset($doc, $bytes); // let the per-render state go
$elapsedMs = (hrtime(true) - $startedAt) / 1_000_000;
$logger->info('pdf.render.complete', [
'job_id' => $jobId,
'render_time_ms' => round($elapsedMs, 2),
'peak_memory_mb' => round(memory_get_peak_usage(true) / 1_048_576, 2),
]);
}
}

unset()는 장식이 아닙니다. 렌더링당 상태는 각 반복에서 해제되어 유지된 메모리가 기준선으로 돌아오도록 의도되어 있습니다. 반복을 거치며 기준선이 올라가는 워커가 바로 이 루프가 막도록 설계된 실패입니다.

대표적인 오해는 마치 하나의 답이 있는 것처럼 *“NextPDF는 초당 PDF를 몇 개나 처리할 수 있는가?”*라고 묻는 것입니다. 그런 답은 없으며, 어떤 수치를 하나 인용하는 것이 바로 서버군의 규모가 잘못 산정되는 경로입니다. 렌더링 비용은 문서에 의해 좌우되므로, 실행에 옮길 가치가 있는 유일한 수치는 엔진 자체의 렌더링당 보고서를 사용해 여러분 자신의 템플릿으로 측정한 수치입니다. 문서, 하드웨어, 그리고 그 뒤에 있는 측정 방법이 빠진 수치는 데이터가 아니라 장식입니다.

두 번째 오해는 피크 메모리가 주시해야 할 대상이라는 것입니다. 피크는 일시적이며 예상된 것입니다 — 다시 돌아옵니다. 배치를 무너뜨리는 수치는 돌아오지 않는 유지된 메모리입니다. 바로 그렇기 때문에 엔진이 이 둘을 분리합니다.

  • 보편적인 처리량 수치는 없으며, 이 페이지는 의도적으로 어떤 수치도 제시하지 않습니다. 렌더링 비용은 여러분의 문서에 따라 달라집니다. 렌더링당 보고서로 측정하십시오.
  • 한정된 메모리는 폐기 가능한 문서 모델이 사용되는지에 달려 있습니다. 여러 렌더링에 걸쳐 문서를 보유하거나 변경 가능한 렌더링당 상태를 공유하면 그 보장이 무효가 됩니다. 프레임워크 브리지는 안전한 형태를 기본값으로 합니다. 직접 작성한 배선은 그것을 똑같이 재현해야 합니다.
  • 이미지 캐시는 한정되어 있으며, 무한하지 않습니다. 고유 이미지가 많은 무거운 워크로드에서는 LRU가 항목을 제거합니다. 이는 설계이지 퇴행이 아닙니다.
  • 워커 풀 규모 산정, 큐 선택, 자동 확장은 엔진 밖의 배포 결정입니다. NextPDF 는 측정값과 한정된 기본 구성 요소를 제공합니다. 여러분의 큐를 대신 운영하지는 않습니다.
  • RenderReport는 데이터이지 판정이 아닙니다. 그것은 어떤 렌더링에서 무슨 일이 일어났는지 알려 줍니다. 그것을 용량 계획으로 바꾸는 것은 여러분의 분석입니다.
  • 이 페이지는 측정 표면은 벤치마크로, 메모리 모델은 코드로 뒷받침합니다. 특정 처리율을 주장하지는 않습니다.
Queued high-volume generation primitives — edition availability
Edition Availability
Core

폐기 가능한 문서 모델, 공유되는 변경 불가능한 레지스트리, 렌더링당 RenderReport, 그리고 메모리 단편화 분석기는 Core입니다. 일반적인 대용량 PDF 생성에는 상용 등급이 필요하지 않습니다.

Pro

기본 구성 요소는 같지만, 상용 기능(서명, PDF/A)은 측정해야 하며 추정해서는 안 되는 렌더링당 비용을 더합니다.

Enterprise

기본 구성 요소는 같지만, 구조화 인보이스 및 검증 작업은 페이로드와 규칙 집합 크기에 따라 커지는 렌더링당 비용을 더합니다.

  • 메모리와 스트리밍 — 엔진이 대용량 문서에서 메모리를 한정된 범위 안에 유지하는 방법과 어느 지점에서 스트리밍하는지.
  • 솔직한 벤치마킹 — 방법이 빠진 벤치마크 수치에 어떤 가치가 있는지, 그리고 NextPDF가 성능을 보고하는 방법.
  • NextPDF 프로덕션 운영 — 배치가 실제로 실행될 때 렌더링당 보고서를 상태 신호로 바꾸는 방법.
  • 폐기 가능한 문서 — 단일 렌더링을 위해 생성하고 이후 폐기하는 문서 인스턴스로, 어떤 상태도 다음 렌더링으로 누수되지 않습니다.
  • 공유 레지스트리 — 프로세스 수명 동안 유지되며 예열 후 변경 불가능한 상태(폰트, 이미지 캐시)로, 렌더링마다 비용을 다시 치르지 않고 여러 렌더링에서 재사용됩니다.
  • 피크 메모리 — 렌더링 중의 일시적인 최고 사용량으로, 예상된 것이며 기준선으로 돌아옵니다.
  • 유지된 메모리 — 렌더링이 완료된 뒤에도 여전히 보유 중인 메모리로, 여러 렌더링에 걸쳐 유지된 기준선이 상승하는 것은 누수입니다.
  • 워커 — 큐에서 렌더링 작업을 가져오는 오래 살아 있는 프로세스로, 배치를 안정적으로 처리하려면 메모리가 한정된 범위 안에 머물러야 합니다.
  • RenderReport — 엔진의 변경 불가능한 렌더링당 지표 스냅숏(시간, 피크 메모리, 페이지 수, 경고)으로, 실제 데이터로부터 용량을 산정하는 데 쓰입니다.