매번 같은 바이트: 재현 가능한 PDF
Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3
한눈에 보기
섹션 제목: “한눈에 보기”같은 입력으로 PDF를 두 번 빌드하면 같은 파일을 기대하게 됩니다. 대부분의 PDF 라이브러리는 그것을 약속할 수 없습니다 — 다시 빌드해서 diff 해 보면, 바이트가 어긋납니다. NextPDF는 움직이는 두 가지를 고정할 수 있어서, 같은 입력이 매번 같은 바이트를 만들어 냅니다.
이것이 중요한 이유
섹션 제목: “이것이 중요한 이유”바이트 동일 출력은 허영심을 채우는 지표가 아닙니다. 그것은 팀이 실제로 원하는 세 가지 밑에 깔린 토대입니다.
첫째는 캐싱입니다. 빌드가 그 입력의 순수 함수라면, 그 출력 해시는 캐시 키입니다. 같은 입력, 같은 해시이므로, 작업을 건너뛰고 저장된 파일을 제공합니다. 바이트가 떠돌면 해시가 떠돌고, 캐시는 결코 적중하지 않습니다.
둘째는 변조 증거입니다. 자신이 배포한 바로 그 파일을 다시 생성할 수 있는 파이프라인은, 나중에 보관된 문서가 변경되지 않았음을 증명할 수 있습니다. 다시 빌드하고, 둘을 해시하고, 비교합니다. 임베디드 시계 때문에 단 한 바이트라도 다르면, 그 증거는 사라지고 여러분은 “날 믿어”로 돌아갑니다.
셋째는 신뢰할 수 있는 CI입니다. 골든 파일 테스트는 알려진 양호 출력을 기록하고, 변경이 그것을 바꾸면 실패합니다. 그 신호는 변하지 않은 엔진이 변하지 않은 파일을 재현할 때에만 의미가 있습니다. 매 실행이 타임스탬프에서 달라지면, 골든 파일은 잡음이고, 팀은 빨간 빌드를 무시하는 법을 배웁니다 — 테스팅에서 가장 비싼 습관입니다.
짧게 요약하면
섹션 제목: “짧게 요약하면”NextPDF의 결정적 프로파일에서, 동일한 빌드 사이에서 그렇지 않으면 어긋날 엔진 통제
필드 두 가지는 날짜와 /ID입니다. 이는 파이프라인의 나머지가 이미 안정적이라고
가정합니다 — 같은 입력, 그리고 스스로 변하지 않는 직렬화입니다(이에 관해서는 아래에서
더 다룹니다):
- 임베디드 날짜. 문서 정보 딕셔너리는
CreationDate와ModDate를 나르고(Spec: ISO 32000-2, §14.3.3ISO 32000-2 §14.3.3), XMP 메타데이터가 그것을 비춥니다. 빌드 시점에 “지금”을 포착하면 매 재빌드가 달라집니다. - 파일 식별자.
/ID배열은 파일을 식별하는 한 쌍의 바이트 문자열로(Spec: ISO 32000-2, §14.4ISO 32000-2 §14.4), 트레일러 딕셔너리에 저장됩니다(Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5). 라이브러리는 보통 그것을 현재 시각 더하기 임의의 바이트로부터 도출하므로, 설계상 매 실행마다 다릅니다.
둘 다 고정하면 — 고정된 타임스탬프와 /ID를 위한 고정된 시드 — 출력은 그 콘텐츠의
결정적 함수가 됩니다. 콘텐츠를 그대로 두면 파일은 바이트 대 바이트로 동일합니다. 이는
Reproducible Builds 프로젝트가 컴파일된 소프트웨어를 위해 확립한 것과 같은 규율을,
문서 계층에 적용한 것입니다.
NextPDF가 이를 다루는 방식
섹션 제목: “NextPDF가 이를 다루는 방식”NextPDF에서 결정성은 테스트 꼼수가 아니라 구성 객체입니다. 엔진은 NextPDF\Core
네임스페이스에 DeterministicSettings 값 객체를 노출합니다. 그것은 final readonly,
불변이며, 위에서 명명한 시계 및 무작위 파생 드리프트 원천 정확히 두 가지를 고정합니다.
날짜와 /ID입니다. 그것들을 고정하면 가장 흔한 드리프트 원천 두 가지가 제거되지만,
그것만으로 바이트 동일 출력이 보장되지는 않습니다. 엔진의 다른 직렬화 동작 — 객체
순서, 폰트 서브셋팅, 압축 설정 — 또한 출력이 재현되려면 결정적이어야 하며, NextPDF는
설계상 그것들을 안정적으로 유지합니다.
그 생성자는 두 개의 인자를 받습니다.
public function __construct( public DateTimeImmutable $timestamp, public string $fileIdSeed,) { // ...}$timestamp는 모든 날짜 필드 — CreationDate, ModDate, 그리고 그 XMP 미러 — 에
기록되는 단일 고정 순간입니다. DateTimeImmutable 하나를 넘기면 문서는 벽시계에게
지금 몇 시인지 묻기를 멈춥니다. $fileIdSeed는 트레일러 /ID를 고정하는 입력입니다.
32자리 16진수 문자열입니다. 같은 시드를 주면 엔진은 시계와 무작위 원천을 표본 추출하는
대신 같은 파일 식별자를 도출합니다.
이 객체는 자신의 입력을 검증합니다. 시드는 정확히 32자리 16진수 문자여야 하며, 그 밖의
어떤 것이든 다르게 보이는 /ID를 조용히 만들어 내는 대신 생성 시점에
InvalidConfigException으로 거부됩니다. 이는 엔진의 나머지가 취하는 것과 같은
추측-거부 자세입니다 — 모호한 입력은 바이트를 조용히 바꾸는 대신 요란하게 실패합니다.
둘 다 고정하면, 레시피는 Reproducible Builds 프로젝트가 익숙하게 만든 바로 그것입니다. 다시 빌드하고, diff 하면, diff 는 비어 있습니다.
- Fix the inputsThe same content, fonts, and settings that produced the original document.
- Pin the timestampOne DateTimeImmutable feeds CreationDate, ModDate, and the XMP dates — no wall clock.
- Pin the /ID seedA 32-character hex seed derives the trailer /ID instead of a clock-plus-random value.
- BuildThe output is now a pure function of content; the two moving parts are held still.
- Rebuild and diffRegenerate from the same inputs and compare bytes — an empty diff is the proof.
실제 예시
섹션 제목: “실제 예시”작고 완전한 형태입니다. 설정은 한 번 구성되어 재사용되므로, 같은 프로그램을 두 번 실행하면 같은 파일을 내보냅니다.
<?php
declare(strict_types=1);
use NextPDF\Core\DeterministicSettings;use NextPDF\Exception\InvalidConfigException;
// One fixed instant for every date field — never the wall clock.$timestamp = new DateTimeImmutable('2026-01-01T00:00:00+00:00');
// A 32-character hex seed pins the trailer /ID. Same seed, same /ID.$fileIdSeed = '0123456789abcdef0123456789abcdef';
try { $deterministic = new DeterministicSettings( timestamp: $timestamp, fileIdSeed: $fileIdSeed, );} catch (InvalidConfigException $e) { // A malformed seed (not exactly 32 hex chars) is refused here, // before any document is built — not silently coerced. error_log($e->getMessage());
throw $e;}
// Hand $deterministic to the document configuration. With both moving// parts pinned, building the same content twice yields identical bytes://// sha256(build_one) === sha256(build_two)시드는 비밀이 아니라 여러분이 통제하는 빌드 입력입니다. 그것을 나머지 빌드 구성 옆에 저장하십시오. 요점은 그것이 고정되어 있어서, 그것이 만들어 내는 파일 식별자도 고정된다는 것입니다.
흔한 오해
섹션 제목: “흔한 오해”첫 번째 함정은 “타임스탬프를 제거했으니, 이제 내 빌드는 재현 가능해.”입니다. 대개는
그렇지 않은데, /ID 배열이 두 원천 중 더 조용한 쪽이기 때문입니다. 날짜는 메타데이터
패널에서 보이고 기억하기 쉽지만, 트레일러 /ID는 대부분의 독자에게 보이지 않으며 매
실행마다 시계와 무작위 원천으로부터 다시 생성됩니다. 날짜만 고정하는 빌드는 여전히 매번
다른 파일을 만들어 냅니다. 둘 다 붙들어 두어야 합니다.
두 번째 함정은 결정성을 그 자체로 보안 기능으로 취급하는 것입니다. 고정된 /ID는
파일을 재현 가능하게 만듭니다. 그것이 파일을 서명된 것으로 만들지는 않으며, 그것
자체로 두 빌드가 일치함을 증명하지도 않습니다. 바이트 대 바이트 비교나 해시가 빌드의
일치를 증명합니다. /ID를 고정하는 것은 단지 거짓 차이의 한 원천을 제거할 뿐입니다.
그리고 그 둘 중 어느 것도 제삼자가 파일을 보증한다는 것을 증명하지는 않습니다.
재현성과 서명은 대체재가 아니라 상호 보완적인 계층입니다.
한계와 경계
섹션 제목: “한계와 경계”결정성은 엔진 자신의 움직이는 부분을 고정합니다. 그것은 여러분의 입력을 고정하지
않습니다. 콘텐츠가 살아 있는 타임스탬프를 임베드하거나, 디스크에서 바뀐 폰트를
끌어오거나, 현재 날짜에 의존하는 값을 렌더링하면, 입력이 바뀌었기 때문에 출력이
바뀝니다 — 그리고 그것은 올바릅니다. DeterministicSettings는 여러분의 비결정성이
아니라 엔진의 비결정성을 제거합니다. 재현 가능한 빌드는 여전히 재현 가능한 입력을
요구합니다.
| Edition | Availability |
|---|---|
| Core | 완전 지원. |
| Pro | Not in this edition |
| Enterprise | Not in this edition |
관련 문서
섹션 제목: “관련 문서”- 골든 파일 테스팅 — 바이트 동일 출력에 의존하는 CI 기법, 그리고 왜 결정적 엔진이 그것의 전제 조건인지.
- 증분 업데이트 — PDF가 어떻게 덧붙이기로
자라는지, 그리고 파일을 그 이전 버전에 연관시키는 데
/ID배열이 다시 어디서 중요한지. - 메타데이터와 XMP 패킷 — 임베디드 날짜가 사는 곳, 그리고 XMP 패킷이 문서 정보 딕셔너리를 어떻게 비추는지.
- PDF 파일의 해부 — 트레일러, 상호 참조
테이블, 그리고
/ID배열이 파일 구조에서 자리하는 곳.
용어집
섹션 제목: “용어집”- 바이트 동일(byte-identical) — 정확히, 바이트 대 바이트로 일치하는 두 파일입니다. “같다”의 가장 강한 형태이며, 해시나 diff 가 검증할 수 있는 것입니다.
/ID(파일 식별자) — PDF와 그 버전을 식별하는 두 바이트 문자열의 배열입니다(ISO 32000-2 §14.4). 트레일러 딕셔너리에 저장됩니다(§7.5.5). 보통 시계 더하기 무작위 바이트로부터 도출되며, 그것이 매 고정되지 않은 빌드마다 그것이 바뀌는 이유입니다.- 문서 정보 딕셔너리(document information dictionary) —
CreationDate와ModDate를 나르는 구조입니다(ISO 32000-2 §14.3.3). 결정적 빌드가 고정해야 하는 두 비결정성 원천 중 하나입니다. - 골든 파일(golden file) — 테스트가 대조하는, 기록된 알려진 양호 출력입니다. 변하지 않은 엔진이 변하지 않은 파일을 재현할 때에만 의미가 있습니다.
- 재현 가능한 빌드(reproducible build) — 그 출력이 그 입력의 결정적 함수인 빌드이므로, 같은 입력으로 다시 빌드하면 같은 바이트를 내놓습니다. 이 용어는 컴파일된 소프트웨어를 위한 Reproducible Builds 프로젝트에서 왔습니다.