1편에서는 PDF를 문서 파일이라기보다 객체 그래프와 렌더링 명령의 조합으로 보는 관점을 정리했다. Header, Body, xref, trailer가 파일의 큰 골격을 만들고, Catalog와 Pages 트리가 페이지 구조를 만든다. Page 객체는 Contents와 Resources를 통해 실제 렌더링에 필요한 명령과 자원을 연결한다.
2편에서는 그 구조를 바탕으로 PDF 편집기를 만들 때 어떤 철학으로 범위를 잡아야 하는지 정리한다. 구현 세부 코드보다 먼저 정해야 하는 것은 목표다. PDF 편집기를 워드프로세서처럼 만들 것인지, 페이지 조립 도구로 만들 것인지, 객체 그래프 재작성기로 만들 것인지에 따라 설계가 완전히 달라진다.
내가 만든 Edit2me의 초기 목표는 명확하다. PDF 내부 텍스트를 자유롭게 고치는 편집기가 아니라, 여러 PDF를 병합하고 페이지 순서를 바꾸고 필요한 페이지만 새 PDF로 조립하는 도구다. 이 목표는 기능을 줄이기 위한 선택이 아니라, PDF라는 포맷의 구조를 정직하게 받아들이는 선택이다.

PDF 편집기는 파일 업로드, 페이지 선택, 조립, 출력으로 이어지는 워크플로우로 보는 편이 명확하다. 출처: ConvertAll
2편의 구성
2편은 다음 순서로 정리한다.
- PDF 편집기를 만들 때 가장 먼저 버려야 하는 가정
- 페이지 단위 편집을 1차 목표로 잡은 이유
- 원본 보존과 새 문서 재작성 사이의 선택
- 객체 그래프 복사와 참조 재매핑의 중요성
- 텍스트 편집을 초기 범위에서 제외한 이유
- UI보다 문서 모델을 먼저 설계해야 하는 이유
- RAG 문서 파싱 경험이 PDF 편집 설계로 이어지는 지점
- Edit2me가 우선 고려한 기능과 의도적으로 미룬 기능
이 글의 핵심은 구현 철학이다. 어떤 라이브러리를 쓰는지보다, 어떤 문제를 풀지 않는다고 결정했는지가 더 중요하다. PDF 편집기는 기능을 무작정 늘릴수록 빠르게 불안정해진다. 그래서 처음부터 처리 범위를 구조적으로 제한해야 한다.
PDF 편집기를 만들 때 가장 먼저 버려야 하는 가정
PDF 편집기를 만들 때 가장 위험한 가정은 PDF가 일반적인 문서 편집 포맷이라는 생각이다. PDF는 페이지 위에 텍스트와 이미지를 안정적으로 표시하기 위해 설계된 고정 레이아웃 문서 형식이다. PDF Reference 1.7에서 페이지 콘텐츠는 그래픽 연산자, 텍스트 연산자, 이미지 XObject, 리소스 dictionary를 통해 렌더링되는 구조로 설명된다.
이 구조에서는 사용자가 보는 문장과 파일 안의 문자열이 1대1로 대응하지 않는다. 화면에는 한 줄의 문장처럼 보이지만 내부적으로는 여러 개의 Tj, TJ 연산자로 나뉘어 있을 수 있다. 폰트 인코딩과 ToUnicode CMap에 따라 실제 유니코드 추출 결과도 달라진다. 이미지 기반 PDF라면 텍스트는 애초에 존재하지 않을 수도 있다.
그래서 PDF 편집기를 워드프로세서처럼 설계하면 초반부터 난도가 급격히 올라간다. 글자 하나를 바꾸는 작업이 단순 문자열 치환이 아니라 폰트, 인코딩, stream 압축, 좌표, 줄바꿈, 자간, 리소스 참조를 함께 다루는 문제가 된다.
내가 Edit2me의 초기 범위를 페이지 단위 조작으로 잡은 이유가 여기에 있다. 페이지 병합, 순서 변경, 선택 추출은 PDF의 가장 안정적인 축인 Pages 트리를 중심으로 처리할 수 있다. 콘텐츠 stream 내부를 해석하고 재작성하는 작업보다 예측 가능성이 높다.
핵심 철학 1: PDF를 편집하지 말고 조립한다
Edit2me의 첫 번째 철학은 PDF를 직접 편집하지 않고 조립한다는 것이다. 여기서 말하는 조립은 기존 PDF 안의 Page 객체와 그 의존 객체들을 새 문서 구조로 옮기고, 새 Catalog, Pages 트리, xref, trailer를 만들어 쓰는 방식이다.
PDF 병합을 파일 바이트 단위의 이어 붙이기로 처리할 수는 없다. 각 PDF는 자기만의 객체 번호 공간을 가지고 있고, trailer와 xref도 독립적으로 존재한다. 두 파일을 단순히 이어 붙이면 객체 번호가 충돌하고, Root Catalog도 하나로 정리되지 않는다.
따라서 병합은 다음과 같은 절차로 이해해야 한다.
입력 PDF 파싱
페이지 목록 추출
페이지 선택 및 순서 결정
페이지가 참조하는 객체 그래프 수집
새 객체 번호 할당
간접 참조 재작성
새 Pages 트리 생성
새 xref와 trailer 작성
이 방식은 기존 파일을 부분적으로 고치는 접근보다 단순하고 명확하다. 원본 PDF를 열어서 필요한 페이지를 읽고, 결과물은 완전히 새 PDF로 생성한다. 원본은 입력 데이터이고, 출력 PDF는 새로 조립된 산출물이다.
이 관점은 오류 처리에도 유리하다. 원본 파일을 직접 수정하지 않으므로 실패해도 원본이 손상되지 않는다. 결과 PDF 생성 중 문제가 생기면 출력을 폐기하면 된다. 편집 도구에서 원본 보존은 기능이 아니라 기본 전제다.
핵심 철학 2: 페이지는 UI 단위이자 내부 모델 단위다
사용자 입장에서 PDF 편집의 가장 자연스러운 단위는 페이지다. 여러 PDF를 올리고, 썸네일을 보고, 페이지를 끌어 옮기고, 일부 페이지를 제거한 뒤 저장한다. 온라인 PDF 편집기에서 불편했던 작업도 대부분 이 페이지 단위 흐름이 매끄럽지 않아서 발생한다.
내부 모델도 이 흐름을 따라야 한다. UI에서 보이는 페이지 하나는 내부적으로 다음 정보를 가진 모델로 표현할 수 있다.
DocumentRef
PageIndex
OriginalPageObjectRef
InheritedPageAttributes
ResourceRefs
ContentRefs
AnnotationRefs
Rotation
MediaBox
CropBox
이 모델의 핵심은 페이지를 단순 이미지처럼 보이게 만들되, 내부적으로는 원본 PDF의 객체 참조를 유지하는 것이다. 사용자가 보는 것은 썸네일과 페이지 번호지만, 시스템이 보존해야 하는 것은 Page 객체와 그 Page가 참조하는 객체 그래프다.
이 설계는 UI와 PDF 엔진을 분리하는 데도 중요하다. 프론트엔드는 페이지 목록의 순서를 바꾸는 역할을 하고, 백엔드는 그 순서에 맞춰 PDF 객체를 재조립한다. UI가 PDF 내부 객체 번호를 직접 이해할 필요는 없다. 반대로 PDF 엔진은 드래그앤드롭 UI를 알 필요가 없다.

PDF의 Page, Contents, Resources도 참조로 연결된 객체 그래프로 다루는 편이 안전하다. 출처: Site24x7
핵심 철학 3: 원본 객체 번호를 믿지 않는다
PDF 병합에서 가장 중요한 구현 원칙은 원본 객체 번호를 믿지 않는 것이다. 서로 다른 PDF는 같은 객체 번호를 자유롭게 사용할 수 있다. 파일 A의 10 0 obj와 파일 B의 10 0 obj는 완전히 다른 객체다.
새 PDF를 만들 때는 입력 객체를 새 객체 번호로 재매핑해야 한다.
(file A, 10 0) -> new 15 0
(file B, 10 0) -> new 42 0
문제는 객체 번호만 바꾸는 것으로 끝나지 않는다는 점이다. 객체 내부에는 다른 객체를 가리키는 간접 참조가 들어 있다.
<<
/Type /Page
/Resources 10 0 R
/Contents 11 0 R
>>
이 Page 객체를 새 문서로 옮기면 /Resources와 /Contents가 가리키는 대상도 새 객체 번호로 바뀌어야 한다. 즉 객체 복사는 깊은 복사에 가깝고, 복사 과정에서 모든 indirect reference를 새 번호 체계로 재작성해야 한다.
이 원칙을 지키지 않으면 결과 PDF는 겉으로는 생성되더라도 일부 페이지에서 폰트가 깨지거나 이미지가 사라지거나 뷰어가 특정 객체를 찾지 못하는 문제가 생긴다. PDF 편집기의 신뢰성은 결국 참조 재매핑의 정확도에 달려 있다.
핵심 철학 4: Pages 트리는 flatten한 뒤 다시 만든다
PDF의 Pages 구조는 트리다. 단순한 PDF에서는 /Kids 배열에 Page 객체가 직접 들어 있지만, 페이지가 많은 문서에서는 중간 Pages 노드가 여러 단계로 구성될 수 있다.
페이지 순서 변경을 이 트리 위에서 직접 처리하려고 하면 구현이 복잡해진다. 특정 Page를 다른 위치로 옮길 때 상위 /Count 값을 갱신해야 하고, 여러 레벨의 /Kids 배열을 동시에 다뤄야 한다.
초기 구현에서는 Pages 트리를 한 번 flatten하는 방식이 더 안전하다.
Pages tree -> [Page 1, Page 2, Page 3, ...]
사용자 조작 -> [Page 3, Page 1, Page 2, ...]
새 Pages tree -> 새 /Kids 배열과 /Count 생성
이 방식은 원본 트리의 최적화 구조를 보존하지 않을 수 있다. 하지만 초기 PDF 편집기의 목표가 페이지 병합과 재배치라면, 완전한 원본 트리 보존보다 결과의 명확성과 안정성이 더 중요하다. 새 Pages 트리를 단순한 구조로 만들면 /Parent, /Kids, /Count를 일관되게 계산할 수 있다.
PDF 1.5 이후에는 object stream과 xref stream이 등장한다. qpdf 문서는 object stream과 xref stream이 PDF 1.5에서 도입된 구조라고 설명한다. 입력 PDF가 이런 구조를 사용하더라도 출력 PDF를 반드시 같은 방식으로 쓸 필요는 없다. 파서는 다양한 입력을 읽을 수 있어야 하지만, writer는 처음에는 단순하고 검증하기 쉬운 형태로 쓰는 편이 낫다.
핵심 철학 5: 콘텐츠 stream 내부 편집은 마지막 단계로 미룬다
PDF에서 가장 매력적으로 보이는 기능은 텍스트 수정이다. 사용자는 PDF 위의 글자를 클릭하고 고치고 싶어 한다. 하지만 기술적으로는 이 기능이 가장 늦게 들어가야 한다.
콘텐츠 stream 내부 편집은 다음 문제를 한꺼번에 건드린다.
- stream 압축 해제와 재압축
/Length갱신BT,ET사이 텍스트 객체 파싱Tj,TJ,Td,Tm,Tf연산자 해석- 폰트 Encoding과 ToUnicode CMap 처리
- 문자열 길이 변경에 따른 레이아웃 변화
- subset font에 없는 glyph 추가 문제
- 기존 텍스트 위에 덮어쓰기와 실제 삭제의 차이
특히 글자 수가 바뀌면 레이아웃 문제가 발생한다. PDF는 HTML처럼 자동으로 줄을 밀어내지 않는다. 기존 위치에 새 glyph를 그릴 뿐이다. 더 긴 문자열을 넣으면 옆 글자와 겹칠 수 있고, 짧은 문자열을 넣으면 기존 글자 흔적을 지우는 처리가 필요하다.
그래서 Edit2me의 초기 철학은 텍스트를 수정하지 않는 것이다. 페이지를 조립하는 기능이 충분히 안정화된 뒤, 텍스트 편집은 별도의 계층으로 다루는 편이 맞다. 텍스트 편집이 필요하다면 기존 콘텐츠를 의미적으로 수정하는 방식과, 기존 콘텐츠 위에 새 레이어를 덧그리는 방식도 구분해야 한다.
핵심 철학 6: 파서는 관대하게, writer는 엄격하게
현실의 PDF는 표준만으로 설명되지 않는 경우가 많다. 생성 도구마다 조금씩 다른 구조를 만들고, 일부 PDF는 손상되어 있어도 뷰어에서 열린다. incremental update가 여러 번 쌓인 파일도 있고, 암호화된 파일도 있고, xref가 깨졌지만 복구 가능한 파일도 있다.
이런 입력을 다루려면 파서는 가능한 한 관대해야 한다. 전통적인 xref table, xref stream, object stream, incremental update를 읽을 수 있어야 하고, stream filter도 처리해야 한다. PDF의 보존 형식과 변형은 Library of Congress의 PDF 설명에서도 여러 하위 유형과 사용 맥락으로 정리된다.
반대로 writer는 엄격해야 한다. 결과 PDF는 예측 가능한 구조로 써야 한다. 새 객체 번호는 충돌하지 않아야 하고, xref offset은 정확해야 하며, trailer의 /Root와 /Size도 일관되어야 한다. 입력은 복잡할 수 있지만 출력은 단순해야 한다.
이 원칙은 문서 처리 시스템에서 자주 쓰는 전략과도 맞닿아 있다.
입력 계층: 다양한 현실 문서를 최대한 수용
정규화 계층: 내부 모델로 변환
출력 계층: 예측 가능한 형식으로 생성
PDF 편집기도 같은 구조를 가져야 한다. 입력 PDF의 복잡성을 UI와 writer까지 그대로 끌고 가면 전체 시스템이 불안정해진다. 중간에 내부 모델을 두고, 모든 조작을 그 모델 위에서 수행해야 한다.

입력 PDF를 내부 모델로 정규화한 뒤 예측 가능한 출력으로 쓰는 구조가 핵심이다. 출처: InfoQ
RAG 문서 파싱 경험이 설계에 준 영향
AI Agent의 RAG 파이프라인을 만들면서 PDF를 많이 다뤘다. Contextifier는 문서를 chunk로 나누기 위한 라이브러리이고, PDF 파싱 과정에서 텍스트 순서, 좌표, 표, 이미지, 페이지 경계 문제가 계속 등장한다.
RAG에서 PDF를 다룰 때 중요한 것은 사람이 보는 페이지와 모델이 읽는 텍스트 사이의 간극이다. 화면에는 자연스러운 문단처럼 보이지만 추출된 텍스트는 순서가 틀어질 수 있다. 표는 행과 열이 무너지고, 머리말과 꼬리말이 본문 사이에 섞이며, 스캔본은 OCR 없이는 텍스트가 나오지 않는다.
이 경험은 PDF 편집기를 설계할 때도 그대로 이어진다. PDF는 보이는 결과가 안정적인 대신 내부 의미 구조가 약할 수 있다. 따라서 편집기도 의미 구조를 과도하게 추정하면 안 된다. 페이지 단위 조작처럼 PDF가 비교적 명확하게 제공하는 구조를 우선 사용해야 한다.
문서 파싱에서는 렌더링 결과를 읽기 순서로 복원한다. PDF 편집에서는 렌더링 결과를 깨지 않도록 객체 그래프를 다시 조립한다. 방향은 다르지만 핵심은 같다. PDF를 단순 파일이 아니라 구조화된 객체 그래프로 다뤄야 한다.
UI 설계의 기준: 사용자는 페이지를 조작하고, 시스템은 객체를 조작한다
PDF 편집기의 UI는 단순해야 한다. 사용자가 원하는 작업은 대체로 다음 네 가지다.
- PDF 파일을 올린다.
- 각 PDF의 페이지를 확인한다.
- 페이지 순서를 바꾸거나 일부 페이지를 제거한다.
- 결과 PDF를 내려받는다.
이 흐름에서 사용자는 xref, trailer, object stream, Resources를 알 필요가 없다. 복잡한 내부 구조는 모두 시스템 내부로 들어가야 한다. 사용자가 페이지를 옮기면 시스템은 내부적으로 Page 모델의 순서를 바꾸고, 저장 시점에 객체 그래프를 재작성한다.
이때 중요한 것은 UI 상태와 PDF 상태를 분리하는 것이다. UI 상태는 사용자가 선택한 페이지 순서와 삭제 여부를 담는다. PDF 상태는 원본 문서, Page 객체 참조, 의존 객체 그래프, 새 객체 번호 매핑을 담는다.
UI State
- document list
- selected pages
- page order
- removed pages
PDF Engine State
- parsed objects
- page references
- dependency graph
- object id mapping
- output writer state
이 분리는 구현을 단순하게 만든다. UI에서 되돌리기, 재정렬, 선택 해제를 하더라도 원본 PDF 객체를 매번 수정할 필요가 없다. 최종 저장 시점에만 사용자의 페이지 배열을 입력으로 받아 새 PDF를 생성하면 된다.
무엇을 고려했는가
Edit2me의 초기 설계에서 고려한 항목은 다음과 같다.
첫째, 원본 보존이다. 원본 PDF는 절대 직접 수정하지 않는다. 모든 작업은 새 출력 PDF를 만드는 방식으로 처리한다. 이 방식은 안전하고, 실패 시 복구가 단순하다.
둘째, 페이지 단위 안정성이다. 초기 기능은 병합, 분할, 순서 변경에 집중한다. 이 기능들은 PDF 내부의 논리 문단을 추정하지 않고도 구현할 수 있다.
셋째, 객체 그래프 기반 복사다. Page 객체만 복사하지 않고, Page가 참조하는 Contents, Resources, Annots 등 의존 객체를 함께 추적한다. 간접 참조는 새 객체 번호 체계에 맞춰 재작성한다.
넷째, 입력과 출력의 비대칭성이다. 입력은 다양한 PDF 구조를 받을 수 있어야 하지만, 출력은 가능한 한 단순하고 검증 가능한 구조로 만든다.
다섯째, UI와 엔진의 분리다. 사용자가 조작하는 단위는 페이지이지만, 엔진이 처리하는 단위는 객체 그래프다. 이 둘을 직접 섞지 않는다.
여섯째, 기능 확장의 순서다. 텍스트 편집, OCR, AcroForm 보존, 전자서명 보존, 주석 고급 편집은 초기 범위에서 제외한다. 먼저 페이지 조립이 안정적으로 동작해야 한다.
무엇을 고려하지 않기로 했는가
좋은 설계는 무엇을 하지 않을지 명확히 정하는 데서 시작한다. Edit2me의 초기 버전에서 의도적으로 미룬 항목은 다음과 같다.
- PDF 내부 텍스트를 직접 수정하는 기능
- 기존 문단의 자동 리플로우
- 폰트 subset에 새 glyph를 삽입하는 기능
- 전자서명 보존
- 암호화 PDF 편집
- 손상 PDF 복구
- AcroForm의 완전한 의미 보존
- annotation의 고급 편집
- OCR 기반 텍스트 레이어 생성
- PDF/A, PDF/X 같은 목적별 표준 완전 대응
이 항목들은 중요하지 않아서 빠지는 것이 아니다. 각각이 독립적인 프로젝트가 될 정도로 범위가 크기 때문에 초기 목표와 분리한다. 특히 전자서명은 incremental update와 변경 무결성 문제까지 연결된다. 암호화 PDF는 권한, 복호화, 재암호화 정책이 필요하다. OCR은 이미지 처리와 텍스트 레이어 삽입 문제로 확장된다.
초기 버전의 목표는 더 좁고 명확하다. 사용자가 여러 PDF를 올리고, 페이지를 원하는 순서로 조립하고, 결과물을 안정적으로 내려받는 것이다.
아키텍처 관점에서 본 Edit2me
Edit2me는 크게 네 계층으로 나눌 수 있다.
Upload Layer
Parse Layer
Page Model Layer
Write Layer
Upload Layer는 입력 파일을 받는다. 이 계층은 파일 크기, MIME type, 저장 위치, 임시 파일 수명 같은 운영 문제를 담당한다.
Parse Layer는 PDF를 읽고 객체 테이블을 구성한다. xref를 읽고, trailer에서 Root를 찾고, Pages 트리를 순회해 Page 목록을 만든다. stream filter와 object stream 처리는 이 계층의 책임이다.
Page Model Layer는 UI와 엔진 사이의 중간 표현이다. 사용자가 보는 페이지 목록, 원본 문서 참조, 원본 Page 객체 참조, 회전, 박스 정보, 의존 객체 목록을 관리한다.
Write Layer는 최종 페이지 배열을 입력으로 받아 새 PDF를 생성한다. 새 객체 번호를 할당하고, 참조를 재작성하고, Catalog와 Pages 트리를 만들고, xref와 trailer를 쓴다.
이 구조에서 가장 중요한 계층은 Page Model Layer다. 이 계층이 없으면 UI 조작과 PDF 내부 구조가 직접 연결된다. 그러면 작은 UI 변경도 PDF 객체 수정으로 이어지고, 구현이 복잡해진다. 반대로 Page Model을 잘 잡으면 UI는 페이지 배열만 다루고, writer는 그 배열을 PDF로 직렬화하기만 하면 된다.
구현 우선순위
초기 구현 우선순위는 다음처럼 잡는 것이 합리적이다.
- 단일 PDF 파싱
- Pages 트리 flatten
- 페이지 목록 모델 생성
- 선택한 페이지로 새 PDF 생성
- 객체 번호 재매핑
- 여러 PDF 병합
- 페이지 순서 변경
- 썸네일 또는 미리보기 연결
- annotation 기본 보존
- 예외 케이스 확장
이 순서는 기능이 아니라 위험도를 기준으로 정리한 것이다. 단일 PDF에서 페이지를 읽고 새 PDF로 다시 쓰는 기능이 안정화되어야 여러 PDF 병합으로 넘어갈 수 있다. 여러 PDF 병합이 되려면 객체 번호 재매핑이 반드시 필요하다. UI 미리보기는 그 이후에 붙이는 편이 안전하다.
소프트웨어 설계에서 가장 흔한 실패는 눈에 보이는 UI부터 만드는 것이다. PDF 편집기에서는 특히 위험하다. UI는 그럴듯하게 만들 수 있지만, 저장 버튼을 눌렀을 때 올바른 PDF가 생성되지 않으면 전체 도구의 의미가 사라진다. 따라서 엔진의 신뢰성을 먼저 확보해야 한다.
실패할 수 있는 지점
PDF 편집기는 여러 지점에서 실패할 수 있다.
첫째, xref를 잘못 읽을 수 있다. 전통적인 xref table만 가정하면 xref stream을 사용하는 PDF에서 실패한다.
둘째, object stream 내부 객체를 놓칠 수 있다. 객체가 파일 본문에 직접 드러나지 않으면 단순 정규식 파싱으로는 찾기 어렵다.
셋째, inherited page attribute를 놓칠 수 있다. /Resources, /MediaBox, /CropBox, /Rotate는 Page 객체에 직접 없고 상위 Pages 노드에서 상속될 수 있다.
넷째, resource dependency를 덜 복사할 수 있다. Page의 /Resources가 폰트, 이미지, 색상 공간, ExtGState, Pattern 같은 객체를 참조하면 그 하위 객체까지 따라가야 한다.
다섯째, annotation과 form을 놓칠 수 있다. 단순 병합에서는 겉보기 페이지가 살아 있어도 링크나 입력 필드가 사라질 수 있다.
여섯째, stream filter를 제대로 처리하지 못할 수 있다. FlateDecode 외에도 DCTDecode, JPXDecode, LZWDecode 등 다양한 filter가 존재한다.
이 실패 지점은 모두 같은 결론으로 이어진다. PDF 편집기는 문자열 편집기가 아니라 객체 그래프 처리기여야 한다.
정리
PDF 편집기를 만드는 핵심 철학은 PDF를 워드프로세서 문서처럼 다루지 않는 것이다. PDF는 고정 레이아웃 렌더링 결과에 가깝고, 내부적으로는 객체 그래프와 콘텐츠 stream으로 구성된다. 따라서 초기 목표는 텍스트 편집이 아니라 페이지 조립으로 잡는 편이 안정적이다.
Edit2me의 방향은 원본 PDF를 직접 수정하지 않고, 필요한 페이지와 의존 객체를 새 문서로 조립하는 것이다. 페이지는 사용자에게 보이는 UI 단위이면서 내부 모델의 중심 단위가 된다. 여러 PDF를 합칠 때는 객체 번호를 새로 할당하고, 모든 indirect reference를 재작성해야 한다. Pages 트리는 flatten한 뒤 새로 만드는 편이 초기 구현에서 더 명확하다.
이 설계는 RAG 문서 파싱 경험과도 연결된다. PDF는 사람이 보는 결과와 기계가 읽는 구조 사이에 간극이 있다. 파싱에서는 그 간극을 읽기 순서와 의미 단위로 복원해야 하고, 편집에서는 그 간극을 깨지 않도록 객체 그래프를 보존하며 재조립해야 한다.
3편에서는 이 철학을 실제 구현 흐름으로 옮긴다. 단일 PDF 파싱, 페이지 목록 추출, 객체 번호 재매핑, 새 Pages 트리 생성, xref와 trailer 작성까지 이어지는 구현 단계를 코드 중심으로 정리한다.