콘텐츠로 이동

오류를 기능으로 다루기

Spec: ISO 9241-110, §5.6.4 Evidence: Code-backed

NextPDF는 예외 계층을 API 표면으로 다루며, 예외를 던지는 메서드만큼 주의 깊게 설계합니다. 실패는 구체적이고 타입이 지정되며, 필요한 세분화 수준에서 잡을 수 있고, 로그에 적합한 구조화된 컨텍스트를 담습니다.

이 페이지는 엔진 자체 소스를 바탕으로 그 표면을 보여 줍니다. 기본 타입, 타입이 지정된 서브클래스, 근본 원인을 메시지에 결합하는 명명된 생성자, 그리고 모든 NextPDF 예외가 노출하는 구조화된 컨텍스트를 다룹니다.

오류 메시지는 엔진이 가장 안 좋은 순간에 개발자에게 건네는 말입니다. 프로덕션 환경, 새벽 2 a.m., 그리고 이미 출시되었어야 할 문서가 있는 순간입니다. 그때 메시지가 무엇을 알려 주느냐에 따라 다음 단계가 간단한 수정이 될지, 긴 조사로 이어질지 결정됩니다.

일반적인 RuntimeException: something went wrong은 방향을 제시하지 않습니다. 엔진이 실패했다는 사실만 알려 줄 뿐, 무엇이 실패했는지, 어디에서 실패했는지, 무엇을 해야 하는지는 결코 알려 주지 않습니다. 인적 요인 지침은 이 점을 분명히 말합니다. 오류는 수정이 연구 과제가 아니라 명확한 다음 단계가 될 만큼 스스로를 충분히 설명해야 합니다 ( Spec: ISO 9241-110, §5.6.4.3 ). 원인과 해결책을 명시하는 예외는 단순한 부가 기능이 아닙니다. 그것은 5분짜리 수정과 5시간짜리 수정의 차이입니다.

  • 모든 NextPDF 실패는 하나의 추상 기본 클래스인 NextPdfException을 확장하므로, 단일 타입으로 모든 라이브러리 오류를 잡을 수 있습니다.
  • 그 아래에는 구체적이고 타입이 지정된 서브클래스들이 있습니다 — 찾을 수 없는 폰트, 유효하지 않은 구성, 실패한 서명 작업 — 따라서 처리할 수 있는 실패를 정확히 잡을 수 있습니다.
  • 모든 NextPDF 예외는 ContextAwareExceptionInterface를 구현하고 getContext()를 노출합니다. 이는 구조화되고 로그에 안전한 맵이므로, 진단 정보를 얻기 위해 메시지 문자열을 파싱할 필요가 전혀 없습니다.
  • 메시지는 실행 가능합니다. 명명된 생성자는 일반적인 템플릿 대신 실제 근본 원인(그리고 종종 수정 방법)을 메시지에 결합합니다.
  • 각 예외 클래스는 누가 이를 처리할 수 있는지 — 개발자, 인프라 또는 라이브러리 호출자 — 를 문서화하므로, 분류는 스택 추적을 읽기 전에 시작됩니다.

이 계층은 얕고 의도적으로 설계되었습니다. 하나의 기본 클래스, 도메인별 타입 계층, 그리고 모두가 따르는 계약으로 구성됩니다.

하나의 기본 클래스, 설계상 catch-all. NextPdfException은 추상이며, RuntimeException을 확장하고, ContextAwareExceptionInterface를 구현합니다:

abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface
{
/** @return array<string, mixed> */
public function getContext(): array
{
return [];
}
}

추상으로 둔 데에는 의도가 있습니다. 기본 클래스는 직접 던져지는 일이 없으므로, 막연한 기본 클래스를 실수로 잡는 일이 결코 없습니다. 최후의 방어선에서는 이 기본 클래스를 의도적으로 잡고, 구체적인 조치를 취할 수 있을 때에는 특정 서브클래스를 잡습니다.

구체적이고 타입이 지정된 서브클래스. 누락된 폰트는 일반적인 오류가 아닙니다. FontNotFoundException이며, 조치에 필요한 데이터를 담고 있습니다:

final class FontNotFoundException extends NextPdfException
{
public function __construct(
private readonly string $fontName,
private readonly array $searchPaths,
private readonly bool $fallbackAttempted,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('Font "%s" not found. Searched: [%s].', $fontName, \implode(', ', $searchPaths)),
0,
$previous,
);
}
// getFontName(), getSearchPaths(), wasFallbackAttempted(), getContext()
}

메시지는 폰트와 검색된 정확한 경로를 명시합니다. 어떤 디렉터리가 누락되었는지 추측할 필요가 없습니다. 예외가 알려 줍니다.

문자열 긁어내기가 아닌 구조화된 컨텍스트. 모든 예외는 snake_case이며 프리미티브만으로 구성되어 로그나 APM 페이로드로 바로 직렬화하기에 안전한 맵을 반환합니다:

public function getContext(): array
{
return [
'config_key' => $this->configKey,
'given_value' => $this->givenValue,
'expected_type' => $this->expectedType,
];
}

계약은 그 이유를 명시적으로 밝힙니다. 로깅 미들웨어는 어떤 NextPDF 예외에 대해서도 메시지를 파싱하지 않고 $logger->error($e->getMessage(), $e->getContext())를 호출할 수 있습니다. 메시지는 사람을 위한 것입니다. 컨텍스트는 기계를 위한 것입니다. 어느 쪽도 다른 쪽의 역할을 대신할 필요가 없습니다.

명명된 생성자를 통한 실행 가능한 메시지. 바로 이 지점에서 오류는 우발적인 산물이 아니라 설계된 표면이 됩니다. SignatureException은 단지 “B-LT 레벨에서 서명에 실패했습니다”라고만 말하지 않습니다. 실제 근본 원인, 그리고 종종 정확한 해결책을 메시지에 결합하는 명명된 생성자를 제공합니다:

public static function tsaUrlEmpty(string $signatureLevel): self
{
return new self('', $signatureLevel, null,
'TSA endpoint URL is empty: pass a non-empty `tsaUrl` to the TsaClient '
. 'constructor (e.g. "https://timestamp.example.com/tsa") or remove the '
. 'TSA client wiring if no timestamping is required at this signature level');
}

메시지는 무엇이 잘못되었는지 그리고 그에 대해 무엇을 해야 하는지를 명시합니다. 누락된 기능 패키지, 없는 HTTP 클라이언트, 실수로 선택된 다이제스트 전용 알고리즘, 알고리즘과 일치하지 않는 키 타입 등을 위한 형제 생성자들이 있습니다. 각 생성자는 한 부류의 실패를 개발자가 엔진 소스를 읽지 않고도 조치할 수 있는 한 문장으로 바꿉니다.

의도적으로 시끄러운 실패. 일부 예외는 조용히 지나갈 공백을 의도적으로 시끄러운 실패로 바꾸기 위해 존재합니다. NotImplementedException은 기계가 grep할 수 있는 feature 레이블과 followUp 참조를 담고 있습니다:

final class NotImplementedException extends NextPdfException
{
public function __construct(
public readonly string $feature,
public readonly string $followUp,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('%s is not implemented in this release. %s', $feature, $followUp),
0, $previous,
);
}
}

도달했지만 연결되지 않은 경로는 그럴듯한 no-op를 반환하는 대신 이 예외를 던집니다. 같은 발상은 StrictModeViolation에도 적용되며, 그 서브클래스들은 벗어난 구문에 대한 짧고 grep할 수 있는 레이블과 선택적인 위치 및 인용 컨텍스트를 담고 있습니다. 사양 위반은 조용히 잘못된 렌더링이 아니라, 타입이 지정되고 컨텍스트가 있는 중단이 됩니다.

클래스 자체에 담긴 분류 메타데이터. 각 예외 클래스는 docblock에 누가 이를 처리할 수 있는지를 명시합니다. 예를 들어, FontNotFoundException은 “Developer (verify font path) or Infrastructure (fix file permissions)“입니다. InvalidConfigException은 “Developer (fix configuration before calling NextPDF)“입니다. NotImplementedException은 “Library callers — either remove the call or pin to a future release”입니다. 분류는 스택 추적을 보기 전에 시작됩니다. “이건 내 문제인가, 아니면 운영팀의 문제인가?”라는 질문에는 이미 답이 적혀 있기 때문입니다.

아래 표는 이 설계의 각 속성과 그 효과를 요약합니다.

설계 속성소스에서무엇을 가져다주는가
하나의 추상 기본 클래스NextPdfException (추상, 컨텍스트 인터페이스 구현)하나의 타입으로 모든 라이브러리 오류를 잡되, 막연한 기본 클래스를 실수로 잡는 일은 결코 없음
구체적이고 타입이 지정된 서브클래스FontNotFoundException, InvalidConfigException, SignatureException, …처리할 수 있는 실패를 정확히 잡음
구조화된 컨텍스트getContext() — snake_case 프리미티브만메시지 문자열을 파싱하지 않고 로깅하거나 APM으로 전송
실행 가능한 메시지명명된 생성자가 근본 원인 + 해결책을 결합템플릿이 아니라 실행할 수 있는 문장
의도적으로 시끄러움NotImplementedException, StrictModeViolation조용한 공백이 타입이 지정되고 grep할 수 있는 중단이 됨
분류 메타데이터각 클래스 docblock의 “Actionable by:“스택 추적을 읽기 전에 누구의 문제인지 파악

이 페이지는 Evidence: Code-backed 입니다. 모든 클래스, 시그니처, 메시지 형태는 엔진의 예외 네임스페이스에서 그대로 가져온 것이며, 의역하지 않았습니다.

  • 추상 기본 클래스와 그 ContextAwareExceptionInterface 계약, 타입이 지정된 서브클래스, getContext() 형태, 그리고 SignatureException 명명된 생성자는 소스에서 그대로 인용한 것입니다.
  • “Actionable by:” 분류 라인은 그 동일한 파일들에 있는 클래스 docblock 계약입니다.
  • 인적 요인 근거는 Spec: ISO 9241-110 입니다 — 고쳐질 만큼 충분히 스스로를 설명하는 오류에 관한 §5.6.4.3, 그리고 §6 사용 오류 강건성 원칙입니다. 엔진은 개발자를 사용자로 보고, 예외를 그 조항들을 충족해야 하는 인터페이스로 취급합니다.

최후의 방어선에서는 넓게 잡고, 조치할 수 있는 지점에서는 구체적으로 잡으며, 구조화된 컨텍스트를 로거에 바로 전달합니다 — 메시지 파싱은 없습니다.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
function renderInvoice(LoggerInterface $logger): ?string
{
try {
$document = Document::createStandalone();
$document->setTitle('Invoice 2026-0042');
$document->addPage();
$document->setFont('BrandSans', '', 12);
$document->cell(0, 10, 'Thank you for your business.', newLine: true);
return $document->getPdfData();
} catch (FontNotFoundException $e) {
// Specific: we can recover — fall back to a built-in font.
// getContext() is log-safe structured data, not a parsed string.
$logger->warning($e->getMessage(), $e->getContext());
return null; // caller re-renders with 'helvetica'
} catch (NextPdfException $e) {
// Backstop: any other NextPDF failure, still with structured context.
$logger->error($e->getMessage(), $e->getContext());
return null;
}
}

구체적인 catch는 예외 타입이 복구 가능성을 알려 주기 때문에 복구합니다. 최후의 방어선은 그 외 모든 것에 대해 구조화된 컨텍스트를 로깅합니다. 애플리케이션은 무슨 일이 일어났는지 알아내기 위해 메시지를 읽는 일이 결코 없습니다.

흔한 오해는 깊은 예외 트리가 과도한 엔지니어링이며, 하나의 오류 타입이 더 단순할 것이라는 생각입니다. 그 방식은 엔진에는 더 단순할 수 있지만 호출자에게는 더 나쁩니다. 하나의 타입은 모든 실패가 일반적인 스택 추적이 되고 복구 로직이 문자열 매칭이 된다는 뜻입니다. 그 매칭은 취약하며, 다음번 메시지 문구 수정만으로도 깨집니다. 작고 구체적인 계층은 그 지식을 타입 시스템으로 옮기며, 그곳에서 컴파일러와 호출자의 catch 블록이 그것을 사용할 수 있습니다.

두 번째 오해는 메시지와 컨텍스트가 중복된다는 것입니다. 그렇지 않습니다. 메시지는 로그 줄을 읽는 사람을 위한 산문입니다. 컨텍스트는 코드 라우팅, 알림 또는 대시보드를 위한 타입이 지정된 맵입니다. 이 둘을 혼동할 때 생기는 문제가 바로 getContext() 계약이 없애기 위해 존재하는 문자열 파싱 함정입니다.

이 계층은 의도적으로 얕습니다. NextPDF는 생각할 수 있는 모든 실패에 대해 별도의 예외 클래스를 만들지 않습니다. 실패를 구체적으로 잡는 일이 호출자에게 합리적인 선택일 때 하나를 만듭니다. 지나치게 잘게 나누면 문자열 파싱 문제를 산만하게 늘어진 catch 목록 문제로 바꾸는 셈입니다.

getContext()는 로그와 APM에 맞게 구조화되어 있으므로, 계약상 프리미티브와 프리미티브 목록만 반환하며 중첩된 객체는 반환하지 않습니다. 이는 엔진 내부 상태를 직렬화한 스냅샷이 아니라 진단용 컨텍스트입니다. 또한 외부 스키마를 구축하기 위한 안정적인 와이어 포맷도 아닙니다.

이 페이지는 예외 설계 표면을 설명합니다. 정확한 예외 집합과 그 필드는 엔진과 함께 진화합니다. 여기에 인용된 클래스와 형태는 이 검토 시점 기준으로 최신이며, 고정된 카탈로그가 아니라 계약을 예시로 보여 주는 것입니다. 계약 — 하나의 기본 클래스, 타입이 지정된 서브클래스, 구조화된 컨텍스트, 실행 가능한 메시지 — 이 부분이 안정적입니다.

  • 추측하기를 거부하는 API — 애초에 이러한 예외를 던지게 하는 빠른 실패 가드입니다.
  • NextPDF 설계 철학 — 왜 “오류는 API 표면이다”가 일급 원칙인지에 대한 설명입니다.
  • 파이프라인 모델 — 문서가 엔진을 거치는 동안 이러한 실패가 어디에서 드러나고 어떻게 관찰되는지 설명합니다.
  • 코드 기반(증거 수준) — 주장이 엔진 자체 소스와 대조 확인되었고, 의역이 아니라 인용을 바탕으로 한 페이지입니다.
  • 컨텍스트 인식 예외ContextAwareExceptionInterface를 구현하고 getContext()를 노출하는 NextPDF 예외입니다. 이 메서드는 메시지 문자열을 파싱하지 않고도 로그나 APM 페이로드로 안전하게 직렬화할 수 있는, 프리미티브 진단 필드의 snake_case 맵을 반환합니다.
  • 명명된 생성자 — 특정 근본 원인, 그리고 종종 그 해결책과 결합된 메시지를 가진 예외를 만드는 정적 팩토리 메서드입니다(예: SignatureException::tsaUrlEmpty()).
  • PAdES — PDF Advanced Electronic Signatures, PDF 서명을 위한 ETSI 프로파일 제품군입니다. 처음 사용할 때 전체 이름을 밝히며, 서명 페이지에서 자세히 다룹니다.
  • TSA — Time-Stamping Authority, 상위 PAdES 프로파일에서 사용하는 RFC 3161 타임스탬프를 발급하는 신뢰할 수 있는 서비스입니다.