콘텐츠로 이동

프로덕션 환경에서의 Artisan

프로덕션 환경에서는 구성된 렌더러와 PSR-3 로거를 주입하고, 여러 렌더링 작업에서 실행 중인 Chrome 프로세스를 재사용하며, 요소가 여러 개인 문서에는 명시적인 높이를 제공하고, 상위 타임아웃으로 렌더링 경로를 제한합니다.

BrowserPool은 하나의 Chrome 프로세스를 계속 유지하며(keepAlive: true), 메모리 증가를 제한하기 위해 100 회 렌더링마다 프로세스를 다시 시작합니다. 이는 장기 실행 CDP 클라이언트에서 알려진 누적 패턴입니다. 여러 문서를 렌더링하는 워커라면 요청마다 렌더러를 하나씩 만들기보다 장기 실행되는 렌더러 하나를 사용하는 것이 좋습니다. 그러면 Chrome 시작 비용이 드물게 발생합니다.

render-service.php
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;
use NextPDF\Artisan\ChromeRendererConfig;
use NextPDF\Artisan\Exception\ChromeNotAvailableException;
use NextPDF\Artisan\Exception\ChromeRenderException;
use Psr\Log\LoggerInterface;
final class ReportRenderer
{
private ChromeHtmlRenderer $renderer;
public function __construct(LoggerInterface $logger)
{
$config = ChromeRendererConfig::fromArray([
'chrome_binary' => getenv('CHROME_BINARY') ?: null,
'render_timeout' => 45,
'max_html_size' => 2_000_000,
'no_sandbox' => (bool) getenv('CHROME_NO_SANDBOX'),
]);
$this->renderer = new ChromeHtmlRenderer($config, $logger);
}
public function render(string $html, float $widthPt, float $heightPt = 0.0): string
{
try {
return $this->renderer->render($html, $widthPt, $heightPt)->getPdfData();
} catch (ChromeNotAvailableException $e) {
// Deployment fault: Chrome runtime missing. Page the on-call owner.
throw $e;
} catch (ChromeRenderException $e) {
// Render-time fault: timeout, crash, empty output. Retryable once.
throw $e;
}
}
public function shutdown(): void
{
$this->renderer->close();
}
}

렌더러는 한 번 생성한 뒤 재사용합니다. 소멸자를 기다리지 않고 Chrome 프로세스를 결정론적으로 해제하려면 워커 종료 시 close()를 호출합니다. 두 catch 분기는 배포 결함(런타임 누락)과 렌더링 시점의 결함(재시도 가능)을 구분합니다. 빈 catch 블록은 사용하지 않습니다.

컨테이너에는 싱글톤으로 연결합니다:

$container->singleton(ReportRenderer::class, fn ($c) =>
new ReportRenderer($c->get(Psr\Log\LoggerInterface::class)));

높이를 생략하면 브리지는 Chrome에서 콘텐츠 높이를 측정한 뒤(body/document 스크롤 및 오프셋 높이의 max), 이를 포인트로 변환하고 약 0.2 인치(약 14.4pt)의 안전 버퍼를 추가합니다. 이 버퍼는 Chrome의 뷰포트 레이아웃과 인쇄 레이아웃 리플로 간의 차이를 흡수합니다. 이 버퍼가 없으면 printToPDF 출력이 두 번째 페이지로 넘어갈 수 있으며, PageImporter(페이지 0 만 처리)는 넘친 부분을 잘라냅니다. 최소 용지 높이로 0.1 인치가 적용됩니다. ChromeHtmlRendererTest::renderUsesAutoFitHeightByDefault, ::renderAutoFitBufferIsAddedNotSubtracted, ::renderAppliesMinimumHeightOf0Point1InchForTinyExplicitHeight 테스트가 이를 검증합니다.

청구서나 인증서 같은 고정 레이아웃 문서에는 명시적인 높이를 포인트 단위로 전달합니다. 높이를 명시하면 버퍼가 추가되지 않으며, 출력은 요청한 용지 크기와 정확히 일치합니다(::renderHonorsExplicitHeightWithoutAutoBuffer가 이를 검증함).

  • 워커마다 렌더러를 하나씩 생성해 재사용합니다. BrowserPool은 실행 중인 브라우저를 재사용하며 100 회 렌더링 경계에서 자동으로 다시 시작합니다.
  • 100 회 렌더링 경계보다 먼저 새 Chrome 프로세스를 사용하고 싶다면 워커 종료 시점 또는 대규모 배치 사이에 close()를 호출합니다.
  • 소멸자도 close()를 호출하지만, 명시적으로 close()를 호출하는 방식이 결정론적이며 장기 실행 프로세스에서 권장됩니다.
  • 다시 시작 알림은 렌더링 횟수와 함께 notice 수준으로 기록됩니다. 다시 시작 비율이 높아지면 경고를 발생시키세요. 이는 예상보다 무거운 문서가 처리되고 있음을 의미합니다.

PSR-3 로거를 주입합니다. 발생 이벤트와 해당 수준은 다음과 같습니다:

이벤트수준컨텍스트
렌더링 시작debugsize, width, height
렌더링 완료debugpdfSize, contentHeight
브라우저 실행infobinary
브라우저 다시 시작noticecount
브라우저 종료debugrenderCount

HTML, PDF 바이트 또는 추출된 텍스트는 기록하지 않습니다. 이는 페이로드를 운영 로그에서 제외하라는 NIST SP 800-92 지침에 부합합니다. start/complete 쌍으로 지연 시간 SLO를 구성하고, notice 이벤트로 다시 시작 비율 경고를 구성합니다.

  • 사이드카 Chrome: PHP 워커와 동일한 컨테이너에서 Chrome을 실행하고 chrome_binary를 고정합니다. 샌드박스를 지원하는 컨테이너를 프로비저닝합니다. /integrations/artisan/chrome-renderer-setup/를 참조하세요.
  • 컨테이너 없음 / CLI: Artisan에는 DI 컨테이너가 없습니다. CLI 러너에서 Premium e-invoice 계약을 처리할 때는 EInvoiceServiceFactory를 사용합니다. /integrations/artisan/boot-and-discovery/를 참조하세요.
  • 리소스 제한: 상위 요청 예산 및 호스트 cgroup/ulimit과 함께 render_timeout을 구성합니다. 위협 모델은 /integrations/artisan/security-and-operations/에서 참조하세요.
  • 렌더링 도중 중단된 렌더러도 Chrome 페이지를 닫으며(finally), 풀은 계속 사용 가능한 상태로 유지됩니다.
  • 하나의 렌더러를 여러 threads/processes에 걸쳐 재사용하는 것은 지원되지 않습니다. 하나의 렌더러는 하나의 Chrome 프로세스를 소유합니다.
  • 100 회 렌더링마다 다시 시작하는 동작은 고정되어 있습니다. 예측 가능한 지연 시간 스파이크를 고려해 배치 크기를 조정하세요.

정상 상태의 비용은 브리지 오버헤드가 아니라 입력에 대한 Chrome 레이아웃과 printToPDF에서 발생합니다. 시작 비용은 keepAlive로 분산됩니다. 100 회 렌더링마다(프로세스 다시 시작) 지연 시간 스파이크가 발생할 수 있습니다. 이를 장애로 처리하기보다 SLO에 반영하세요.

프로덕션 경로에는 신뢰할 수 없는 HTML이 도착합니다. /integrations/artisan/security-and-operations/를 다시 읽어 보세요. 네트워크 장벽은 구성과 관계없이 유지되지만, no_sandbox: true는 Chrome 프로세스 격리를 제거하고 입력에 대한 신뢰 요구 사항을 높입니다.

컨테이너가 없는 워커에서는 Premium이 설치되지 않은 경우 EInvoiceServiceFactorynull을 반환하므로, 오픈 소스 렌더링 경로는 변경 없이 실행됩니다. 렌더링된 문서에서 e-invoice 임베딩 및 검증을 활성화하려면 Pro/Enterprise를 설치하세요.

  • /integrations/artisan/quickstart/
  • /integrations/artisan/configuration/
  • /integrations/artisan/security-and-operations/
  • /integrations/artisan/chrome-renderer-setup/
  • /integrations/artisan/troubleshooting/