큐 작업으로 PDF 생성하기
한눈에 보기
섹션 제목: “한눈에 보기”부하가 큰 PDF 생성은 요청 스레드에서 처리할 작업이 아닙니다. 각 프레임워크 통합은 워커에서 PDF를 빌드하고 저장하는 큐 기반 생성 인터페이스를 제공하므로, 작업을 디스패치한 즉시 HTTP 요청이 반환됩니다. 이 가이드는 Laravel(GeneratePdfJob), Symfony(Messenger를 통한 GeneratePdfMessage), CodeIgniter 4(GeneratePdfJob, codeigniter4/queue 사용)에 대한 큐 기반 경로를 다룹니다.
사전 요구 사항은 다음과 같습니다.
- NextPDF 코어와 프레임워크 통합 하나가 설치되어 있습니다.
- 워커 전송 계층이 구성되어 있습니다. Laravel 큐 커넥션, Symfony Messenger 전송 계층 또는
codeigniter4/queue가 설치된 CodeIgniter 4 큐가 이에 해당합니다. - 해당 전송 계층에 대해 워커 프로세스가 실행 중입니다.
이 가이드는 이미 큐가 있는 애플리케이션을 전제로 합니다. 큐 또는 Messenger 설정 자체에 대해서는 사용 중인 프레임워크의 문서를 참고하세요.
통합을 설치한 다음, 사용 중인 프레임워크가 요구하는 큐 의존성을 설치하세요.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter는 큐 패키지가 필요합니다. 이 통합은 해당 패키지를 개발 전용 의존성으로 선언하므로, 워커를 실행하는 애플리케이션에서 직접 require 하세요.
composer require nextpdf/codeigniter codeigniter4/queueLaravel의 경우 config/nextpdf.php(queue.connection, queue.queue, queue.timeout)에서 큐 커넥션을 구성하고, 해당 커넥션의 워커를 실행하세요.
개념 개요
섹션 제목: “개념 개요”각 통합은 동일한 개념을 저마다의 방식으로 표현합니다.
- Laravel은
NextPDF\Laravel\Jobs\GeneratePdfJob을 제공하며, 이는ShouldQueue잡입니다. 출력 경로와 빌더 클로저를 함께 전달하여 디스패치합니다. 클로저는 컨테이너에서 해석된 문서를 받아 구성된 문서를 반환합니다. 잡은 워커에서 반환된 문서를 해당 경로에 저장합니다. 선택적 성공 및 실패 콜백도 받습니다. - Symfony는 Messenger 버스에서 디스패치되는
readonly메시지인NextPDF\Symfony\Message\GeneratePdfMessage를 제공합니다. 여기에 더해 PSR-11 서비스 로케이터에서 클래스 이름으로 빌더를 해석하는GeneratePdfHandler를 제공합니다. 문서 유형별로NextPDF\Symfony\Message\PdfBuilderInterface를 구현합니다. - CodeIgniter 4는
NextPDF\CodeIgniter\Jobs\GeneratePdfJob을 제공하며, 이는Config\Queue::$jobHandlers에 이름 키로 등록됩니다. 빌더 참조, 출력 경로, 컨텍스트 배열과 함께 등록된 이름으로 잡을 푸시합니다. 빌더는App\PdfBuilders네임스페이스 안의 정적 메서드입니다.
세 가지 모두 하나의 보안 원칙을 공유합니다. 출력 경로를 검증한다는 점입니다. 페이로드는 디스패치와 실행 사이에 큐에 머무를 수 있으므로, Symfony와 CodeIgniter는 소비 시점에 이를 다시 검증합니다. 빌더는 워커에서 새 문서를 대상으로 실행되므로, 동시에 실행되는 잡들이 문서 상태를 공유하는 일은 결코 없습니다.
API 표면
섹션 제목: “API 표면”| 관심사 | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| 큐 단위 | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (큐 핸들러) |
| 디스패치 | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| 빌더 형태 | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document — 네임스페이스 App\PdfBuilders |
| 경로 / 입력 가드 | 잡이 워커에서 출력 경로를 검증함 | DTO가 생성 시 검증하고, 핸들러가 소비 시 다시 검증함 | 잡이 경로를 WRITEPATH/pdfs/로 한정하고, 빌더 네임스페이스를 허용 목록으로 관리함 |
| 실패 인터페이스 | failed()는 tries 이후 호출됨; onFailure는 최종 실패 시 호출됨 | Messenger 재시도 전략; 타입이 지정된 검증 오류 | InvalidArgumentException / QueueException |
코드 예제 — 빠른 시작
섹션 제목: “코드 예제 — 빠른 시작”각 프레임워크에서 사용하는 최소 디스패치 예입니다.
<?php
declare(strict_types=1);
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;
GeneratePdfJob::dispatch( storage_path('app/reports/january-2026.pdf'), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, 'January report', newLine: true),);출력 경로는 반드시 .pdf로 끝나야 하며, 잡은 쓰기 전에 워커에서 해당 경로를 검증합니다.
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Pdf\InvoicePdfBuilder;use NextPDF\Symfony\Message\GeneratePdfMessage;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Messenger\MessageBusInterface;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/invoice/{id}/queue', name: 'invoice_queue')] public function queue(MessageBusInterface $bus, int $id): Response { $bus->dispatch(new GeneratePdfMessage( builderClass: InvoicePdfBuilder::class, outputPath: '/var/storage/invoices/' . $id . '.pdf', builderContext: ['invoice_id' => $id], ));
return new Response('PDF generation queued.', 202); }}<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
final class InvoiceController extends BaseController{ public function queueInvoice(int $id): ResponseInterface { service('queue')->push('pdf-queue', 'generate-pdf', [ 'builder' => 'App\\PdfBuilders\\InvoiceBuilder::build', 'outputPath' => WRITEPATH . 'pdfs/invoice-' . $id . '.pdf', 'context' => ['invoice_id' => $id], ]);
return $this->response ->setStatusCode(ResponseInterface::HTTP_ACCEPTED) ->setJSON(['status' => 'queued', 'invoice_id' => $id]); }}CodeIgniter에서는 잡 클래스 문자열이 아니라 jobHandlers 키('generate-pdf')를 푸시합니다. 먼저 app/Config/Queue.php에 핸들러를 등록하세요.
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Queue\Config\Queue as BaseQueue;use NextPDF\CodeIgniter\Jobs\GeneratePdfJob;
final class Queue extends BaseQueue{ /** @var array<string, class-string> */ public array $jobHandlers = [ 'generate-pdf' => GeneratePdfJob::class, ];}코드 예제 — 프로덕션
섹션 제목: “코드 예제 — 프로덕션”프로덕션 디스패치는 성공 및 실패 콜백(Laravel) 또는 명시적으로 등록한 빌더와 타입이 지정된 핸들러(Symfony)를 연결하고, PSR-3 로거를 통해 로그를 남깁니다. 아래 Laravel 예시는 두 콜백을 모두 사용하여 디스패치합니다.
<?php
declare(strict_types=1);
namespace App\Jobs;
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;use Psr\Log\LoggerInterface;use Throwable;
final class DispatchMonthlyStatement{ public function __construct(private readonly LoggerInterface $logger) {}
public function __invoke(int $accountId): void { // dispatch() is public static: it constructs the job from the // arguments it receives. Pass every argument — including the // callbacks — to the static call, not to a separately built instance. GeneratePdfJob::dispatch( storage_path("app/statements/{$accountId}.pdf"), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, "Statement for account {$accountId}", newLine: true), function (string $path) use ($accountId): void { $this->logger->info('Statement PDF written', [ 'account_id' => $accountId, 'path' => $path, ]); }, function (Throwable $exception) use ($accountId): void { $this->logger->error('Statement PDF failed', [ 'account_id' => $accountId, 'exception' => $exception::class, ]); }, ); }}성공 콜백은 출력 경로를 받고, 실패 콜백은 Throwable을 받습니다. 잡은 실패 처리 경로를 실행하기 전에 tries(기본값 3)를 모두 소진합니다. timeout은 nextpdf.queue.timeout을 통해 조정하세요. tries와 backoff 값은 public 속성이므로, 이를 변경하려면 GeneratePdfJob을 서브클래싱하세요.
Symfony의 경우 빌더를 구현하고 서비스 로케이터에 등록하여, 핸들러가 등록된 빌더에만 접근할 수 있도록 하세요.
<?php
declare(strict_types=1);
namespace App\Pdf;
use NextPDF\Core\Document;use NextPDF\Symfony\Message\PdfBuilderInterface;
final class InvoicePdfBuilder implements PdfBuilderInterface{ /** @param array<string, mixed> $context */ public function build(Document $document, array $context): Document { $document->addPage(); $document->setFont('dejavusans', '', 12); $document->cell(0, 10, 'Invoice #' . $context['invoice_id']);
return $document; }}services: App\Pdf\InvoicePdfBuilder: ~
nextpdf.pdf_builder_locator: class: Symfony\Component\DependencyInjection\ServiceLocator arguments: - 'App\Pdf\InvoicePdfBuilder': '@App\Pdf\InvoicePdfBuilder' tags: ['container.service_locator']
NextPDF\Symfony\Message\GeneratePdfHandler: arguments: $builderLocator: '@nextpdf.pdf_builder_locator'CodeIgniter의 경우 빌더를 App\PdfBuilders 아래의 정적 메서드로 구현하세요. 잡은 해당 네임스페이스를 벗어난 빌더 참조와 WRITEPATH/pdfs/를 벗어난 출력 경로를 모두 거부합니다.
<?php
declare(strict_types=1);
namespace App\PdfBuilders;
use NextPDF\Core\Document;
final class InvoiceBuilder{ /** @param array<string, mixed> $context */ public static function build(Document $document, array $context): Document { $invoiceId = (int) ($context['invoice_id'] ?? 0);
$document->addPage(); $document->cell(0, 10, "Invoice #{$invoiceId}");
return $document; }}각 프레임워크에서 워커를 실행하세요.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queue의존성에서 발생한 할당 누수가 무한정 누적되지 않도록, 제한된 수명(--limit / --memory-limit / --time-limit)으로 Laravel과 Symfony 워커를 재활용하세요.
예외 상황 및 주의 사항
섹션 제목: “예외 상황 및 주의 사항”- 저장되는 것은 빌더의 반환 값입니다. 모든 통합에서 워커는 처음 해석된 인스턴스가 아니라 빌더가 반환한 문서를 저장합니다. 항상 빌더에서 구성된 문서를 반환하세요.
- 경로 검증은 워커에서 실행됩니다. Symfony는 생성 시점에 출력 경로를 검증하고, 소비 시점에 다시 검증합니다. CodeIgniter는 경로를
WRITEPATH/pdfs/로 한정하고, 경로 탐색과 형제 접두사 경로를 거부합니다. 디스패치 시점에는 안전했지만 소비 시점에 안전하지 않은 경로도 여전히 거부됩니다. - CodeIgniter는 클래스가 아니라 이름을 푸시합니다.
GeneratePdfJob::class를 잡 이름으로 푸시하면 푸시 시점에 큐에 의해 거부됩니다. 대신jobHandlers키를 푸시하세요. - Laravel 콜백은 정적 디스패치에 전달해야 합니다. 잡 인스턴스를 만든 다음
$job->dispatch(...)를 호출하면 해당 인스턴스와 콜백이 버려집니다. 콜백을GeneratePdfJob::dispatch(...)에 전달하세요. - 워커에서 안전하게 동작하는 레지스트리. 글꼴 레지스트리는 프로세스 수명 동안 잠긴 싱글톤이고, 이미지 레지스트리는 크기가 제한된 캐시입니다. 문서는 잡마다 새로 만들어집니다. 워커에서 공유 문서를 요청하지 마세요.
- 워커에서 서명하기. 큐 작업에서 서명되거나 PDF/A 형식의 출력을 생성하려면 워커 환경에 상용 NextPDF 에디션이 설치되어 있어야 하며, 그렇지 않으면 서명 서비스는
null로 해석됩니다. 서명하기 전에 null 검사를 수행하세요.
PDF 생성을 큐 작업으로 옮기면 HTTP 요청에서 전체 PDF 빌드 시간이 빠집니다. 즉, 작업을 디스패치하면 요청이 반환됩니다. 글꼴 및 이미지 레지스트리는 워커 수명에 걸쳐 설정 비용을 분산하므로, 잡당 비용은 문서 구성 및 콘텐츠 출력으로 한정됩니다. 처리 중인 잡 수를 워커 풀에 맞게 조정하고, 글꼴 워밍업이 첫 번째 잡에서가 아니라 워커 부팅 시 한 번 일어나도록 preload_fonts(Laravel, Symfony)를 미리 채워 두세요.
보안 참고 사항
섹션 제목: “보안 참고 사항”- 브로커에 접근할 수 있다면 큐 페이로드는 공격자의 영향을 받을 수 있으므로, 페이로드 내의 출력 경로와 빌더 참조를 신뢰할 수 없는 것으로 취급하세요. 통합은 경로 검증으로 이를 강제하며, CodeIgniter에서는 빌더 네임스페이스 허용 목록도 사용합니다.
- 심층 방어 차원에서 워커 파일 시스템 권한을 의도한 출력 디렉터리로 제한하여, 어떻게든 검증을 통과한 변조된 경로라도 해당 디렉터리를 벗어날 수 없도록 하세요.
- 실패 콜백에서는 예외 클래스와 상관관계 식별자를 로깅하고, 메시지나 트레이스는 절대 로깅하지 마세요.
- 빈
catch블록은 절대 작성하지 마세요. 이 문서의 모든 실패 콜백은 로깅을 수행하고 컨텍스트를 함께 전달합니다.
전체 큐 위협 모델(페이로드 검증, 콜러블 허용 목록, 경로 제한)은 각 통합의 보안 및 운영 페이지에서 다룹니다.
적합성
섹션 제목: “적합성”이 가이드는 어떤 규범적 표준 주장도 하지 않습니다. 여기에 제시된 모든 API 호출은 명시된 통합에서 검증된 공개 표면입니다. 큐 기반 경로가 의존하는 컨테이너 바인딩 보장(해석할 때마다 새 문서, 잠긴 글꼴 레지스트리)은 아래 함께 보기에 링크된 상위 프로덕션 사용 페이지에 PSR 인용과 함께 문서화되어 있습니다. 이 쿡북 페이지는 사용법을 다시 설명할 뿐이며, 인용은 해당 페이지에 맡깁니다.
함께 보기
섹션 제목: “함께 보기”- 컨트롤러에서 생성된 PDF 반환하기 — 동기 방식에 해당하는 가이드입니다.
- Laravel 프로덕션 사용 —
GeneratePdfJob, 콜백, 큐 튜닝 표. - Symfony 프로덕션 사용 — Messenger 워커 안전성 및 빌더 로케이터.
- CodeIgniter 프로덕션 사용 —
GeneratePdfJob,jobHandlers, 경로 제한.