생성된 대용량 PDF를 HTTP 응답으로 스트리밍
한눈에 보기
섹션 제목: “한눈에 보기”컨트롤러 내부에서 대용량 PDF를 생성하고, 응답 버퍼에 두 번째 전체 복사본을 남겨 두지 않은 채 바이트를 반환하려고 합니다. 각 프레임워크 통합은 자체 PdfResponse 팩토리의 스트리밍 변형인 streamInline() 및 streamDownload()을 제공합니다. 이 메서드들은 콜백으로 PDF 본문을 고정된 64 KB 청크 단위로 클라이언트에 기록하는 프레임워크 StreamedResponse를 반환합니다.
이 방식을 선택하기 전에 메모리 모델을 있는 그대로 이해해야 합니다. 엔진은 먼저 전체 문서를 메모리에서 빌드합니다. 스트리밍 콜백은 getPdfData()를 호출하며, 이 메서드는 전체 PDF를 하나의 문자열로 구체화한 뒤 그 문자열을 64 KB 조각 단위로 순회합니다. 피크 메모리에서 줄어드는 것은 두 번째 복사본, 즉 프레임워크가 Content-Length를 측정하는 동안 버퍼링된 Illuminate\Http\Response 또는 Symfony\Component\HttpFoundation\Response가 보관하게 될 복사본입니다. 스트리밍 변형은 길이를 측정하지 않으므로 Content-Length를 생략합니다. 응답 본문과 문서 문자열을 동시에 보관하는 일은 결코 없습니다. 이는 진정한 증분 스트리밍이 아닙니다. NextPDF에는 증분 라이터 표면이 없으므로, 첫 바이트가 소켓에 도달하기 전에 문서가 완전히 구체화됩니다.
작업 중 예상치 못한 일이 없도록, 사전에 명시한 전제 조건은 다음과 같습니다.
- NextPDF 코어가 설치되어 있고, 프레임워크 통합 중 하나인
nextpdf/laravel또는nextpdf/symfony가 설치되어 있으며 검색 가능합니다. - 사용 중인 프레임워크에서 요청을 컨트롤러로 라우팅하는 방법을 이미 알고 있습니다.
- 이미 컨트롤러에서 생성된 PDF 반환하기를 읽었습니다. 이 문서는 이 레시피가 기반으로 삼는 버퍼링된
inline()및download()팩토리를 다룹니다.
이 사용 가이드는 Laravel과 Symfony가 공유하는 StreamedResponse 패턴을 중심으로 합니다. CodeIgniter 4는 동일한 streamInline() / streamDownload() 메서드 이름을 제공하지만, 해당 메서드는 바이트를 CodeIgniter\HTTP\DownloadResponse로 감싸며, 이는 콜백 기반 StreamedResponse가 아닙니다. 엣지 케이스 섹션에 그 차이를 기록해 두었습니다.
사용 중인 프레임워크에 맞는 통합을 설치하십시오. 다음 중 하나를 실행하십시오.
composer require nextpdf/laravelcomposer require nextpdf/symfonyLaravel의 경우 설치 후 구성을 게시하십시오.
php artisan vendor:publish --tag=nextpdf-configSymfony는 Flex를 통해 번들을 자동으로 등록합니다. 계속하기 전에 사용 중인 프레임워크의 설치 페이지에서 자동 등록 여부를 확인하십시오.
개념 개요
섹션 제목: “개념 개요”버퍼링된 응답 팩토리인 PdfResponse::download() 또는 PdfResponse::inline()은 getPdfData()를 호출하고, 반환된 문자열을 Response 객체에 저장한 뒤, Content-Length를 strlen()으로 설정합니다. 그러면 프레임워크는 응답의 수명 동안 그 문자열을 보관합니다. 대용량 문서의 경우, 이는 문서 문자열과 응답 본문 문자열이 동시에 메모리에 존재한다는 뜻입니다.
스트리밍 팩토리는 다른 형태를 취합니다. PdfResponse::streamDownload() 및 PdfResponse::streamInline()은 콜백으로 빌드된 StreamedResponse를 반환합니다. 프레임워크는 본문을 전송할 준비가 되었을 때만 그 콜백을 호출합니다. 콜백 내부에서 통합은 getPdfData()를 한 번 호출하고, 반환된 문자열을 64 KB 청크로 분할한 다음, 각 청크를 echo한 뒤 flush()를 수행합니다. 본문의 영구적인 두 번째 복사본은 유지되지 않으며, Content-Length 헤더도 방출되지 않습니다.
이 페이지의 모든 결정은 두 가지 사실에 의해 좌우됩니다.
- 빌드는 즉시적이고, 전송은 청크 단위입니다.
getPdfData()는NextPDF\Core\Document에서 라이터를 호출하고 전체 PDF를 하나의 문자열로 반환합니다. 64 KB 청킹은 이미 빌드된 바이트가 프로세스를 떠나는 방식만 제어합니다. 피크 메모리는 작은 스트리밍 윈도가 아니라 완성된 문서 하나의 크기로 제한됩니다. Content-Length없음. 스트리밍 변형은 콜백 내부에서 본문을 빌드하지 않고는 본문 길이를 알 수 없으므로 헤더를 생략합니다. 클라이언트 진행률 표시줄,Range요청, 또는 길이에 민감한 프록시는 크기를 확인할 수 없습니다. 알려진 길이가 절약되는 응답 복사본보다 더 중요할 때는 버퍼링된download()/inline()을 선택하십시오.
프레임워크의 관용적인 해석 경로를 통해 문서를 가져오십시오.
- Laravel: 컨테이너에서
NextPDF\Contracts\DocumentFactoryInterface를 해석하고create()를 호출합니다. 이는 스트리밍 팩토리가 받아들이는 구체 타입인 새로운NextPDF\Core\Document를 반환합니다. - Symfony:
NextPDF\Symfony\Service\PdfFactory를 주입하고create()를 호출합니다. 이는 구성된 기본값이 적용된 새로운NextPDF\Core\Document를 반환합니다.
API 표면
섹션 제목: “API 표면”| 관심사 | Laravel | Symfony |
|---|---|---|
| 새 문서 | app(DocumentFactoryInterface::class)->create() | PdfFactory::create() |
| 스트리밍 인라인 | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| 스트리밍 다운로드 | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| 반환 타입 | Symfony\Component\HttpFoundation\StreamedResponse | Symfony\Component\HttpFoundation\StreamedResponse |
| 콜백 내부의 빌드 호출 | NextPDF\Core\Document::getPdfData() | NextPDF\Core\Document::getPdfData() |
| 청크 크기 | 64 KB (결정적 str_split) | 64 KB (결정적 substr 루프) |
Laravel PdfResponse는 NextPDF\Laravel\Http\PdfResponse에 위치하며, Symfony용은 NextPDF\Symfony\Http\PdfResponse에 위치합니다. 두 스트리밍 팩토리 모두 동일한 Symfony\Component\HttpFoundation\StreamedResponse 타입을 반환합니다. 둘 다 동일하게 고정된 OWASP(Open Web Application Security Project) 응답 강화 헤더 세트(X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: default-src 'none', X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer)를 적용하며, 둘 다 다운로드 파일 이름을 정제합니다. 이러한 헤더는 직접 추가하지 마십시오.
두 팩토리 모두 전체 PDF 바이너리를 빌드하여 반환하는 동일한 공통 기반 코어 표면인 NextPDF\Core\Document::getPdfData(): string을 호출합니다. 같은 계열의 메서드인 save(string $path): void는 동일한 바이트를 원자적 라이터를 통해 디스크에 기록합니다. 이 레시피는 대상이 파일이 아니라 HTTP 소켓이므로 getPdfData()를 사용합니다.
코드 샘플 — 빠른 시작
섹션 제목: “코드 샘플 — 빠른 시작”각 프레임워크에서 사용하는 최소 스트리밍 다운로드 액션입니다. 문서 호출은 동일한 코어 표면이며, 컨트롤러 스캐폴딩만 다릅니다. 스트리밍 팩토리는 프레임워크에 콜백을 넘겨주므로 액션은 즉시 반환됩니다. 본문은 프레임워크가 응답을 전송할 때 빌드되어 플러시됩니다.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Laravel\Http\PdfResponse;use Symfony\Component\HttpFoundation\StreamedResponse;
final class ReportController extends Controller{ public function annualReport(): StreamedResponse { $document = app(DocumentFactoryInterface::class)->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;use NextPDF\Symfony\Service\PdfFactory;use Symfony\Component\HttpFoundation\StreamedResponse;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/report', name: 'report_pdf')] public function annualReport(PdfFactory $pdf): StreamedResponse { $document = $pdf->create(); $document->addPage(); $document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf'); }}다운로드를 강제하는 대신 브라우저 탭에서 미리 보려면 streamDownload(...) 대신 streamInline(...)을 호출하십시오. Content-Disposition은 inline이 되며, 다른 모든 헤더는 동일하게 유지됩니다.
코드 샘플 — 프로덕션
섹션 제목: “코드 샘플 — 프로덕션”프로덕션 액션은 의존성을 주입하고, 경로 입력을 검증하며, 빌드 중 발생할 수 있는 가장 구체적인 예외를 잡고, 트레이스를 유출하지 않고 실패 클래스를 기록한 뒤, 정의된 HTTP 오류를 반환합니다. 아래 예제는 Laravel 생성자 주입을 사용합니다. Symfony에 해당하는 코드도 동일한 형태를 따르며, 액션마다 PdfFactory를 주입합니다.
getPdfData()는 스트리밍 콜백 내부에서 실행되므로, 여기서 발생하는 예외는 프레임워크가 헤더 전송을 시작한 이후에 드러납니다. 오류 처리를 의미 있게 유지하려면, 응답을 돌려주기 전에 문서(실패할 수 있는 단계)를 빌드하고 그 단계에서 빌드 실패를 잡으십시오. 그러면 콜백 내부에서는 이미 빌드된 바이트를 청크 단위로 전송하는 일만 발생합니다.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;use NextPDF\Contracts\DocumentFactoryInterface;use NextPDF\Core\Document;use NextPDF\Exception\NextPdfException;use NextPDF\Laravel\Http\PdfResponse;use Psr\Log\LoggerInterface;use Symfony\Component\HttpFoundation\StreamedResponse;
final class StatementController extends Controller{ private const int MAX_STATEMENT_ID = 9_999_999;
public function __construct( private readonly DocumentFactoryInterface $documents, private readonly LoggerInterface $logger, ) {}
public function show(int $statementId): StreamedResponse|Response { // Validate input at the boundary before any build work runs. if ($statementId < 1 || $statementId > self::MAX_STATEMENT_ID) { return new Response('Invalid statement identifier.', 422); }
try { // Build the whole document up front. getPdfData(), invoked inside // the streamed callback, materializes the full PDF in memory, so // do the failure-prone build here, where the catch can still set a // clean HTTP status before any byte is sent. $document = $this->buildStatement($statementId); $document->getPdfData(); } catch (NextPdfException $exception) { // Log the exception class, never the message or a stack trace, so // internal detail does not leak into the log sink. $this->logger->error('Statement PDF build failed', [ 'statement_id' => $statementId, 'exception' => $exception::class, ]);
return new Response('Could not generate the statement PDF.', 500); }
// The build succeeded. The streamed factory rebuilds the bytes inside // its callback and flushes them to the client in 64 KB chunks. return PdfResponse::streamDownload( $document, "statement-{$statementId}.pdf", ); }
private function buildStatement(int $statementId): Document { $document = $this->documents->create(); $document->addPage(); $document->cell(0, 10, "Statement #{$statementId}", newLine: true);
return $document; }}모든 빌드 실패에 대해 하나의 핸들러를 두고 싶다면, 모든 NextPDF 예외가 확장하는 추상 기반 클래스인 NextPDF\Exception\NextPdfException을 잡으십시오. 특정 원인에 대응하려면, getPdfData()가 발생시킬 수 있는 구체 하위 타입을 먼저 잡으십시오. 콘텐츠가 페이지 지오메트리에 맞지 않을 때는 NextPDF\Exception\PageLayoutException, 스트림 압축이 실패할 때는 NextPDF\Exception\CompressionException, 잘못된 출력 구성에 대해서는 NextPDF\Exception\InvalidConfigException입니다. 빈 catch 블록은 절대 작성하지 마십시오. 여기의 각 분기는 실패 클래스를 기록하고 정의된 상태를 반환합니다.
액션마다 새 문서를 해석하면 테스트에서 팩토리를 교체 가능한 상태로 유지할 수 있습니다. 이전 콘텐츠 상태가 이월될 수 있으므로, 단일 장기 실행 워커 프로세스 내에서 하나의 컨트롤러 인스턴스를 서로 관련 없는 두 문서에 재사용하지 마십시오.
엣지 케이스 및 주의 사항
섹션 제목: “엣지 케이스 및 주의 사항”- 검증 후 스트리밍 패턴에서는 문서가 두 번 빌드됩니다. 프로덕션 샘플은 빌드를 검증하기 위해
getPdfData()를 한 번 호출하고, 그런 다음 팩토리가 콜백 내부에서 다시 호출합니다. 실패 지점을 헤더보다 앞쪽으로 옮기는 데 드는 비용입니다. 특정 문서에서 이중 빌드 비용이 너무 크다면 사전 빌드 프로브를 건너뛰고, 콜백 내부의 빌드 실패가 이미 시작된 응답을 중단한다는 점을 받아들이십시오. Content-Length없음. 스트리밍 변형은 헤더를 생략합니다. 다운로드 진행률 표시줄과Range요청은 작동하지 않습니다. 알려진 길이가 필요할 때는 버퍼링된download()/inline()을 사용하십시오.- 버퍼링 프록시는 이점을 무효화합니다. 본문을 전달하기 전에 전체 본문을 캡처하는 리버스 프록시나 PHP 출력 버퍼는 전체 PDF를 다시 보관하여 절약한 복사본 이점을 없애 버립니다.
application/pdf응답을 스트리밍하도록 프록시를 구성하거나, 해당 경로에서 버퍼링된 응답을 사용하십시오. - CodeIgniter 4는 콜백 스트리밍 방식이 아닙니다. CodeIgniter 통합은 동일한
streamInline()/streamDownload()메서드 이름을 제공하지만, 전체 본문을 보관하는CodeIgniter\HTTP\DownloadResponse를 반환하며, 이는 콜백 기반StreamedResponse가 아닙니다. 이 페이지의 StreamedResponse 패턴은 Laravel과 Symfony에만 적용됩니다. - 반환 후에는 본문에 기록하지 마십시오. 스트리밍 콜백이 출력을 소유합니다.
StreamedResponse를 프레임워크에 넘겨준 후에는 응답 본문을 직접echo하거나 기록하지 마십시오. - 서명된 문서는 빠르게 실패합니다. 고수준 PAdES 서명을 위해 설정된 문서에서
getPdfData()를 호출하면 서명되지 않은 파일을 방출하는 대신NextPDF\Exception\NotImplementedException을 발생시킵니다. 서명된 출력은 이 레시피가 아니라 문서화된 서명 경로를 통해 스트리밍하십시오.
스트리밍은 문서 빌드가 아니라 응답 복사본을 제한합니다. getPdfData()가 첫 번째 청크가 전송되기 전에 전체 문서를 구체화하므로, 피크 메모리는 대략 완성된 PDF 하나의 크기입니다. 정말 크거나 페이지 수가 많은 문서에서는 전송이 아니라 빌드 자체가 요청 예산을 지배합니다. 큐에 등록된 작업으로 생성을 요청 스레드에서 분리하십시오. 큐에 등록된 작업에서 PDF 생성하기를 참조하십시오.
64 KB 청크 크기는 두 통합 모두에서 고정되어 있으며 결정적입니다. 이는 전송 세분성만 제어하며 전송되는 총 바이트 수나 피크 메모리를 변경하지 않습니다. 절약되는 응답 복사본이 제약이고 진행률 표시줄이 필요하지 않을 때는 스트리밍 변형을 선택하십시오. 알려진 Content-Length의 이점을 얻는 작고 지연 시간에 민감한 응답에는 버퍼링된 변형을 선택하십시오.
보안 참고 사항
섹션 제목: “보안 참고 사항”- 빌드하기 전에 입력을 검증하십시오. 프로덕션 액션은 빌드 작업이 실행되기 전에 범위를 벗어난 식별자를
422로 거부합니다. 검증되지 않은 입력을 빌드 작업이나 파일 이름에 절대 삽입하지 마십시오. - 파일 이름 정제가 자동으로 적용됩니다. 두 스트리밍 팩토리 모두 파일 이름을 정제하고 OWASP 응답 강화 헤더 세트를 추가합니다. 제어된 값을 전달하고, 팩토리가 두 번째 계층에서 이를 정제하도록 하십시오. 파일 이름을 직접 인코딩하지 마십시오.
- 동시 메모리를 제한하십시오. 요청마다 전체 PDF가 메모리에서 구체화되므로, 대규모 동시 트래픽은 피크 메모리를 배가시킵니다. 메모리 고갈형 서비스 거부를 완화하기 위해 빌드를 유발하는 입력에 크기 및 속도 제한을 적용하십시오.
- 메시지가 아니라 실패 클래스를 기록하십시오. catch 블록은
$exception::class와 상관관계 식별자를 기록하며, 예외 메시지나 스택 트레이스는 결코 기록하지 않습니다. 로그 싱크에 남는 원시 트레이스는 정보 누출입니다. - 빈 catch 금지. 이 페이지의 모든 catch 분기는 기록하고 정의된 오류 응답을 반환합니다.
적합성
섹션 제목: “적합성”이 가이드는 규범적 표준 관련 주장을 하지 않습니다. 여기에 표시된 모든 클래스, 메서드, 헤더는 명명된 통합의 검증된 공개 표면입니다. NextPDF\Core\Document::getPdfData(), NextPDF\Laravel\Http\PdfResponse 및 NextPDF\Symfony\Http\PdfResponse 스트리밍 팩토리, 그리고 Symfony\Component\HttpFoundation\StreamedResponse 반환 타입이 그렇습니다. 팩토리가 적용하는 OWASP 응답 강화 헤더의 의미는 아래 더 보기 섹션에 연결된 각 통합의 보안 및 운영 페이지에 인용과 함께 문서화되어 있습니다. 이 쿡북 페이지는 사용법을 다시 설명하고, 규범적 인용은 해당 페이지에 위임합니다.
더 보기
섹션 제목: “더 보기”- 컨트롤러에서 생성된 PDF 반환하기: 버퍼링된
inline()및download()대응 항목입니다. - 큐에 등록된 작업에서 PDF 생성하기: 빌드를 요청 스레드에서 분리합니다.
- Laravel 프로덕션 사용법: DI로 연결된 컨트롤러, OWASP 헤더 세트, 컨테이너 바인딩 계약입니다.
- Symfony 프로덕션 사용법: 스트리밍 콜백, 64 KB 청크 이미터, 빌더 로케이터입니다.