PDF 증분 업데이트를 사용하면 Delphi 애플리케이션이 변경된 객체만 덧붙여 문서를 수정할 수 있고, 원본 바이트는 하나도 건드리지 않습니다. losLab PDF Library는 이를 AppendToStream으로 구현하며, 이 메서드는 ISO 32000-1 §7.5.6이 정의한 증분 섹션만 기록하므로 2 GB 파일에서 북마크 하나를 고치는 편집이 전체 재작성 대신 킬로바이트 단위의 출력만으로 끝납니다. 서명된 문서를 서명을 무효화하지 않고 갱신할 수 있는 것도 같은 메커니즘 덕분입니다
이것이 해결하는 고통은 구체적입니다. 전체 저장은 파일 전체를 다시 씁니다. 모든 객체가 다시 직렬화되고, 모든 상호 참조 오프셋이 재계산되며, 출력물은 입력과 바이트 수준의 연관성이 전혀 없습니다. 40 KB짜리 인보이스라면 그래도 괜찮습니다. 하지만 문서 제목의 오타 하나만 고친 2 GB 스캔 아카이브라면, 20바이트를 바꾸려고 2기가바이트를 다시 쓰는 것은 터무니없습니다 — 게다가 그 파일에 전자 서명이 들어 있었다면 재작성이 방금 그 서명을 파괴한 것입니다
PDF를 저장하면 왜 전자 서명이 깨질까요?
PDF 전자 서명은 문서의 논리적 내용에 서명하는 것이 아니라 물리적 파일의 바이트 범위에 서명합니다. 서명 딕셔너리의 /ByteRange 항목은 암호학적 다이제스트가 덮는 파일 구간이 정확히 어디인지 기록합니다. 그 바이트를 다시 직렬화하는 저장 작업은 — 의미상 완전히 동일한 문서를 만들어 내는 저장이라 해도 — 다이제스트를 바꾸고, 모든 검증기가 서명이 깨졌다고 보고합니다. 이는 의도된 설계입니다. 서명은 어떤 추상적인 문서 모델이 아니라 서명자가 본 바이트를 보증하기 때문입니다
증분 업데이트는 PDF 명세가 마련해 둔 탈출구입니다. 증분 저장은 원본 %%EOF 뒤에 새 데이터를 덧붙일 뿐 서명된 바이트 범위를 절대 건드리지 않으므로, 기존 서명은 자신이 덮는 바이트에 대해 계속 유효하게 검증됩니다. 그러면 검증기는 덧붙여진 변경을 — 두 번째 서명, 폼 입력, 주석을 — 별도로 분류하고 그것이 허용된 수정인지 판단합니다. 다중 서명 워크플로는 모두 이에 기대고 있으며, 각 서명자는 직전 서명 위에 증분 섹션을 하나씩 얹습니다. 서명 파이프라인을 만들고 있다면 Delphi의 PAdES 서명과 검증을 다룬 글이 서명 바이트 범위와 증분 섹션이 어떻게 맞물리는지 자세히 설명합니다
ISO 32000-1 §7.5.6에서 증분 업데이트가 동작하는 방식
ISO 32000-1 §7.5.6은 이 모델을 세 가지 규칙으로 정의합니다. 첫째, 원본 파일 내용은 그대로 온전히 남습니다 — 단 한 바이트도 움직이지 않습니다. 둘째, 변경되거나 새로 만들어진 객체는 마지막 %%EOF 뒤에 덧붙여지며, 각각 이전과 같은 객체 번호를 그대로 지닙니다(변경된 객체는 단순히 옛 정의를 가리는 더 새로운 정의를 얻습니다). 셋째, 새 상호 참조 섹션과 트레일러가 덧붙여지고, 트레일러의 /Prev 항목이 이전 상호 참조 섹션의 바이트 오프셋을 가리켜 사슬을 이루며, 리더는 이 사슬을 최신에서 과거 방향으로 따라가며 각 객체를 가장 최근 정의로 해석합니다
이 구조에서 쓸모 있는 성질 두 가지가 따라 나옵니다. 업데이트 비용은 문서 크기가 아니라 변경된 양에 비례합니다 — 덧붙이기 비용은 수정된 객체의 크기에 작은 xref/트레일러 오버헤드를 더한 값입니다. 그리고 파일 자체가 자신의 버전 이력이 됩니다. 이전 리비전이 모두 물리적으로 남아 있으므로, 감사자는 파일을 앞쪽의 어떤 %%EOF에서든 잘라 내어 그 시점에 존재하던 문서를 그대로 복원할 수 있습니다. 각 개정 전에 문서가 어떤 모습이었는지 증명해야 하는 컴플라이언스 워크플로에서는 이 내장 감사 추적이 증분 저장을 택하는 결정적 논거가 되는 경우가 많습니다
AppendToStream으로 증분 업데이트 작성하기
losLab PDF Library는 증분 출력을 AppendToStream(AppendMode: Integer; OutStream: TStream): Integer로 노출하며, 성공하면 1을, 실패하면 0을 반환합니다. AppendMode 매개변수는 대상 스트림에 무엇이 들어갈지 선택합니다. 모드 0은 완전한 파일을 씁니다. 원본 소스 바이트를 먼저 스트림에 복사한 다음 증분 섹션을 덧붙입니다. 모드 1은 증분 섹션 자체만 — 즉 델타만 — 쓰고 소스 바이트는 통째로 건너뜁니다. 모드 2는 SetAppendInputFromString으로 등록한 호출자 제공 접두부를 먼저 쓴 다음, 그 위에 업데이트 섹션을 덧붙입니다
var
Doc: TPDFlib;
Delta: TMemoryStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('contract.pdf', '') <= 0 then
Exit;
// 작은 편집: 파일 전체 재작성을 유발해서는
// 안 되는 종류의 변경
Doc.SetInformation(3, 'Amended 2026-07-04'); // 키 3 = /Subject
Delta := TMemoryStream.Create;
try
// AppendMode = 1: 증분 섹션만 씁니다.
// 원본 바이트 + Delta = 완전하고 유효한 PDF.
if Doc.AppendToStream(1, Delta) = 1 then
Delta.SaveToFile('contract.delta.bin');
finally
Delta.Free;
end;
finally
Doc.Free;
end;
end;
시스템 설계 관점에서 흥미로운 것은 모드 1입니다. 델타가 자기 완결적이므로 원본과 독립적으로 배포할 수 있습니다. 리비전을 오브젝트 스토리지에 별도 블롭으로 저장하거나, 원격 사이트에 델타만 복제하거나, 기본 파일에 증분 사슬을 이어 붙여 임의의 리비전을 재구성할 수 있습니다. 재구성 규칙은 단순한 바이트 이어 붙이기입니다 — 원본 파일이 먼저, 그다음 각 델타가 순서대로 — 이것이 바로 §7.5.6이 증분 업데이트된 파일에 대해 규정하는 배치이기 때문입니다
라이브러리는 원본 파일을 복사하지 않고 어떻게 xref 오프셋을 계산할까요?
증분 섹션 안의 상호 참조 항목은 절대 바이트 오프셋을 담아야 합니다 — 델타의 시작이 아니라 완전한 파일의 시작에서 잰 위치여야 합니다. 이는 모드 1에 수수께끼를 던집니다. 작성기는 원본 바이트를 결코 내보내지 않으면서도, 기록하는 모든 오프셋은 그 바이트가 거기 있는 것처럼 굴어야 하기 때문입니다. losLab PDF Library는 직렬화기에 가상 좌표 공간을 제시하는 내부 스트림 어댑터 TPDFAppendSectionStream으로 이를 해결합니다. 이 어댑터는 원본 파일의 바이트 길이를 기준 오프셋으로 삼아 생성되고, 자신의 위치와 크기를 그 기준값에 지금까지 덧붙인 양을 더한 값으로 보고하며, 새로 쓰인 바이트만 호출자의 대상 스트림으로 전달합니다
그 결과 모드 1은 소스 문서의 사본을 결코 실체화하지 않습니다 — 디스크에도, 메모리에도 만들지 않습니다. 순진한 구현(전체 파일을 임시 버퍼에 쓴 다음 꼬리를 잘라 내는 방식)은 원본 PDF 전체의 일시적 사본을 떠안게 되는데, 기가바이트급 입력에서는 바로 그것이 증분 업데이트가 피하려고 존재하는 비용입니다. 이 오프셋 가상화 기법은 라이브러리의 다른 곳에서 쓰이는 바이트 참조 시프팅과 가까운 사촌입니다. 바이트 참조 시프팅을 이용한 빠른 PDF 병합에 관한 글은 같은 아이디어를 문서 결합에 적용한 사례를 보여 주고, 직접 파일 접근 방식의 대용량 PDF 병합과 분할 가이드는 RAM에 넉넉히 들어가지 않는 파일을 위한 주변 I/O 아키텍처를 다룹니다
SaveToStream으로 전체 저장 스트리밍하기
증분 출력은 스트리밍 이야기의 절반이고, 나머지 절반은 전체 저장에서 벌어지는 일입니다. losLab PDF Library의 SaveToStream은 문서 전체를 중간 AnsiString으로 만들어 두었다가 그 버퍼를 한 번에 써 내보내는 대신, 문서 직렬화기를 대상 스트림에 대고 직접 구동합니다. 예전 방식도 동작하기는 했지만, 전체 저장마다 출력의 완전한 두 번째 사본을 메모리에 일시적으로 들고 있어야 했습니다 — 10 MB에서는 무해하고, 500 MB에서는 고통스러우며, 32비트 프로세스에서 수 기가바이트 출력에는 넘을 수 없는 벽이었습니다. 직접 직렬화는 최대 메모리 사용량이 직렬화된 길이가 아니라 문서의 객체 구조를 따라가게 만듭니다
var
Doc: TPDFlib;
Output: TFileStream;
begin
Doc := TPDFlib.Create;
try
if Doc.LoadFromFile('archive.pdf', '') <= 0 then
Exit;
// ... 전체 재작성을 정당화하는 편집들 ...
Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
try
if Doc.SaveToStream(Output) = 0 then
Writeln('Save failed, error ', Doc.LastErrorCode);
finally
Output.Free;
end;
finally
Doc.Free;
end;
end;
공유 모드가 남긴 교훈: AppendToFile이 0을 반환했을 때
이 영역의 회귀 하나는 실패 양상이 널리 일반화되기 때문에 다시 이야기할 값어치가 있습니다. AppendToFile(FileName)은 디스크에 있는 기존 PDF에 증분 업데이트를 직접 덧붙입니다 — 파일을 읽어 들이고, 변경하고, 같은 경로에 덧붙이는 제자리 감사 추적 워크플로에 자연스러운 호출입니다. v3.71.2에서 바로 그 순서가 0을 반환하기 시작했습니다. 근본 원인은 작성기가 아니라 로더에 있었습니다. 대용량 문서의 온디맨드 읽기를 지원하려고 LoadFromFile은 문서 객체가 살아 있는 동안 소스 파일 핸들을 열어 둔 채 유지하는데, 그 핸들이 fmShareDenyWrite로 열려 있었습니다. 그래서 AppendToFile이 같은 파일을 쓰기용으로 다시 열려 하자 로더 자신의 공유 모드가 이를 거부했고, API는 한 바이트도 쓰지 못한 채 실패했습니다
수정은 로더의 공유 모드를 fmShareDenyNone으로 완화한 것이었고, 이것이 안전한 이유는 증분 덧붙이기의 성질 그 자체에 있습니다. 증분 덧붙이기는 파일 끝 뒤에만 바이트를 추가할 뿐, 리더의 장수명 핸들이 서비스하는 영역을 절대 다시 쓰지 않습니다. 이 라이브러리를 감싸는 사람이 — 혹은 비슷한 스트리밍 로더를 만드는 사람이 — 얻어 갈 일반적인 교훈은, 지연 방식으로 핸들을 붙들고 있는 리더와 같은 파일에 쓰는 작성기는 서로 긴장 관계에 있으며, 열 때 고르는 공유 모드는 구현 세부가 아니라 API 계약이라는 점입니다. 코드에서 AppendToFile이 0을 반환한다면, 프로세스 안의 다른 무언가가 여전히 제한적인 공유 모드로 대상 파일을 붙들고 있지 않은지 먼저 확인하십시오
솔직한 비용: 증분 업데이트가 잘못된 도구일 때
증분 업데이트는 파일 크기를 내주고 쓰기 효율을 얻는 거래이며, 그 거래가 언제나 유리하지는 않습니다. 각 리비전은 변경된 객체를 덧붙이는 한편 대체된 옛 정의는 파일에 그대로 남으므로, 수백 번 편집된 문서는 죽은 객체와 모든 리더가 따라 걸어야 하는 긴 /Prev 사슬을 쌓아 갑니다. 더 나쁜 것은 "삭제된" 내용이 사라지지 않는다는 점입니다. 리비전 5에서 지운 텍스트는 리비전 4의 바이트 안에 여전히 물리적으로 존재하며, 파일을 잘라 내는 누구든 복원할 수 있습니다. 따라서 편집 마스킹, 새니타이즈, 민감 정보의 제거는 전체 재작성을 요구합니다 — 마스킹을 증분으로 저장하는 것은 단계만 늘어난 데이터 유출입니다
전체 저장이 옳은 선택인 경우도 있습니다. 압축 정리가 목적일 때(쌓인 증분과 사용되지 않는 객체를 짜내는 경우), 암호화처럼 문서 전역 속성을 바꿀 때 — 재암호화는 모든 문자열과 스트림을 건드리므로 그 변경에는 "증분"이라 부를 것이 남지 않습니다 — 또는 편집 이력이 파일과 함께 따라다녀서는 안 되는 깨끗한 산출물을 만들 때입니다. 합리적인 규칙은 이렇습니다. 문서가 살아서 변하는 동안, 특히 서명이 붙은 뒤에는 AppendToStream이나 AppendToFile을 쓰고, 문서가 시스템을 떠나거나 이력을 평탄화해야 하는 생애 주기의 경계에서는 전체 SaveToStream 재작성을 쓰십시오
증분 업데이트, 가상 오프셋 델타 출력, 스트림 직접 직렬화는 모두 Delphi, C# 및 VB.NET용 표준 losLab PDF Library의 일부이며, 제품 페이지는 위에서 다룬 서명 및 대용량 파일 기능과 함께 전체 저장 및 덧붙이기 API 전반을 소개합니다