PDF는 렌더링 결과만 보는 순간 놓치는 게 많습니다.
**xref(교차 참조)**로 객체 위치를 찾고, **obj/stream**을 통해 실제 데이터를 디코딩하기 때문입니다.
내부 구조를 알면 추출 품질·성능·보안을 동시에 개선할 수 있어요.
오늘 다룰 주제는 흔히 “그냥 PDF”라고 뭉뚱그려지는 파일 포맷입니다. 그런데 실제로 실무에서는 PDF가 **데이터 파이프라인의 병목**이 되거나, **보안 사고(매크로/스크립트 유사, 취약 파싱)**의 시작점이 되거나, **불필요한 바이트 낭비**로 비용을 터뜨리는 경우가 많아요. 그래서 이번 글에서는 PDF의 내부 요소—특히 **xref(교차 참조 테이블)**, **obj(객체)**, **stream**—가 어떻게 파일 동작(로드/파싱/렌더링/검증)에 영향을 주는지 깊게 보겠습니다.
## 왜 PDF 내부를 알아야 할까요?
PDF는 “문서”이지만, 컴퓨터 관점에서는 “약속된 그래프(객체 모델) + 바이트 덩어리(스트림)”입니다. 즉, PDF의 렌더링/추출/검증은 결국 다음을 계속 수행합니다.
- **obj 번호**로 객체를 찾아감
- 객체의 사전(Dictionary)에서 필터(예: **FlateDecode**)를 확인하고 스트림을 디코딩
- 텍스트/폰트/이미지/XObject를 해석
- 마지막으로 최종 렌더링에 필요한 리소스를 구성
이 과정에서 가장 먼저 부딪히는 것이 바로 **xref**입니다. xref 없이는 대부분의 파서는 “어디서 무엇을 읽어야 하는지”를 모릅니다.
## Deep Dive: xref/obj/stream은 PDF 처리를 어떻게 좌우하나요?
첨부된 텍스트(=PDF의 덤프)에는 대표적으로 이런 단서가 보입니다.
- `%PDF-1.3`
- `xref ...`
- `n 00000 n ...` 같은 오프셋 테이블
- `obj << ... >> endobj`
- `stream ... endstream`
- `Filter /FlateDecode` 같은 디코딩 힌트
### 1) xref: “객체 번호 → 파일 오프셋” 인덱스
xref는 `xref 0 126`처럼 시작하며, 이어서 각 obj가 파일에서 어디에 위치하는지(바이트 오프셋)를 제공합니다. 파서 입장에서는 이 테이블을 먼저 읽고:
1. obj 번호 i를 결정
2. xref에서 i의 오프셋을 추출
3. 그 위치로 가서 해당 객체(obj)를 파싱
…을 반복합니다.
메모리/성능 측면에서 중요한 포인트는 여기예요.
- xref가 정확하면 랜덤 액세스가 가능해지고, 일부 작업이 빨라집니다.
- xref가 손상되거나(또는 악의적으로 조작되어) 오프셋이 어긋나면 파서는 **재시도/스캔**에 들어가며 느려지고, 취약 파싱 가능성이 커집니다.
### 2) obj 객체: 문서의 “노드”
PDF의 핵심은 `obj`입니다. 예시 형태는:
```pdf
126 0 obj << /Linearized 1 /O 128 /H [ 1432 639 ] /L 133687 ... >> endobj
```
이런 식으로 obj는 “사전”으로 구성되고, 필요하면 뒤에 **stream**을 붙입니다.
실무적으로 객체가 곧 추출/렌더링 로직의 출발점이라서, 파서 구현 시 다음을 명확히 해야 합니다.
- 객체 사전에서 **/Type** 확인 (예: /Page, /Font, /XObject)
- 간접 참조 패턴(`N 0 R`) 처리
- stream이 있으면 Length/Filter로 정확히 디코딩
### 3) stream: 압축된 본문(콘텐츠/폰트/이미지 데이터)
텍스트 안에서도 `stream H�*T... endstream` 같은 구간이 반복됩니다. 예를 들어:
- `<< /Filter /FlateDecode /Length 126 ... >> stream ... endstream`
- `<< /Filter /CCITTFaxDecode /DecodeParms ... >> stream ... endstream`
즉, “콘텐츠는 객체이며, 데이터의 실제 바이트는 stream”입니다. PDF 로더가 하는 일의 상당 부분은 다음과 같습니다.
- stream 바이트를 가져옴(Length 기준)
- Filter 체인을 적용(예: **FlateDecode**)
- 그 결과를 해석
폰트의 경우 `/FontDescriptor`, `/ToUnicode`, `/Encoding` 같은 부속 정보까지 같이 엮이기 때문에, 추출 품질(텍스트 복원률)과 직결됩니다.
### 4) Linearized PDF: 스트리밍 렌더링을 위한 배치
샘플 어딘가에 이런 구문이 보입니다.
- `/Linearized 1`
- `/O ... /H [ ... ] /L ...`
이건 **Linearized PDF(“웹 스트리밍을 위한 PDF”)**로, 파일의 앞부분만으로도 일부 렌더링이 가능하게 구조를 배치합니다. 즉, 서버가 PDF를 점진적으로 제공할 때:
- xref/obj의 위치가 앞쪽으로 최적화되어 있어야 하고
- 파서가 “초기 렌더링”을 위해 필요한 최소 객체를 빨리 찾아야 합니다.
### 가상의 파서 파이프라인(요점만)
실무에서 “PDF 추출 서비스”를 만든다면, 대략 이런 순서를 갖습니다.
```ts
// pseudocode
const buf = await readPdfBytes();
const xref = parseXref(buf); // obj->offset
const trailer = parseTrailer(buf, xref);
const root = deref(trailer.Root);
for (const page of enumeratePages(root)) {
const contentStreams = getPageContents(page);
for (const s of contentStreams) {
const bytes = sliceStream(buf, s.length, s.offset);
const decoded = applyFilters(bytes, s.filters); // FlateDecode, etc
renderCommands(decoded); // or extractText(decoded)
}
}
```
여기서 **parseXref()가 실패하면 전체 서비스 로직이 흔들릴 수밖에 없습니다.**
## Pros & Cons: PDF를 안다는 장점과, 조심해야 할 Trade-off
### Pros
- **신뢰성**: xref 손상/조작 케이스를 감지하고 더 안전한 fallback(보수적 스캔) 전략을 설계할 수 있습니다.
- **성능 최적화**: Linearized PDF/스트림 Length를 활용해 필요한 부분만 읽도록 I/O 비용을 줄일 수 있어요.
- **텍스트 품질 향상**: `/ToUnicode` 및 폰트 매핑을 확인하면 “찌꺼기 같은 문자” 대신 정확한 복원이 가능해집니다.
### Cons (Trade-off)
- **구현 복잡도 증가**: PDF는 객체 그래프와 필터 체인이 복잡해서 파서가 커집니다.
- **취약점 표면 확대**: 내부 포맷을 직접 파싱하면, 작은 실수(Length 신뢰, 디코딩 순서, 경계 체크)로 보안 버그가 생길 수 있어요.
- **호환성 비용**: 버전(PDF-1.3, 1.7 등), 다양한 폰트/이미지/필터 조합을 모두 고려해야 합니다.
그래서 많은 팀이 “직접 파서 작성” 대신, 검증된 라이브러리(예: 상용/오픈소스)를 사용하되, **사전 단계에서 xref/Length 범위 검증 같은 방어 로직**만 추가하는 쪽을 선택하곤 합니다.
## Author's Opinion: 시니어 입장에서의 한 줄 평과 전망
PDF 내부를 직접 파고드는 건 고급기술이지만, 더 중요한 건 “파서를 만들기 위한 지식”이라기보다 **운영에서 안전성과 비용을 통제하기 위한 통찰**이라고 봐요.
앞으로도 문서 포맷은 계속 늘어날 거고(스캔본 PDF, 복합 폰트, 다양한 필터 체인), 결국 개발자는 “문서가 데이터다”라는 관점으로 내부 구조를 읽을 수 있어야 합니다.
## 마지막으로: 개발자로서 던져야 할 질문
- 여러분의 시스템은 PDF를 **신뢰**하고 있나요, 아니면 **검증**하고 있나요?
- xref가 깨졌을 때 라이브러리가 느려지는 구간을 측정해봤나요?
- 텍스트 추출 품질이 나쁘다면, 그 원인이 **문서 구조(폰트/ToUnicode)** 문제인지 **인코딩/디코딩(FlateDecode)** 문제인지 분리했나요?
PDF의 xref/obj/stream을 이해하면, 문서 처리 파이프라인을 “블랙박스”에서 “제어 가능한 시스템”으로 바꿀 수 있습니다. 오늘 글이 그 첫 걸음이 되길 바랍니다.
#PDF #Parsing #Security #Performance #Text-Extraction
'개발자 이야기 > IT 뉴스' 카테고리의 다른 글
| 아카이빙을 소프트웨어로: **아날로그 콘서트 테이프**의 디지털 파이프라인 (0) | 2026.04.13 |
|---|---|
| 크롬의 수직탭, 생산성 전환점 (1) | 2026.04.12 |
| 라이브 액티비티, 방송을 바꾸다 (0) | 2026.04.12 |
| PDF 필드의 정체: 객체 스트림(Object Stream)·FlateDecode·xref를 통해 본 구조적 해부 (0) | 2026.04.12 |
| LLM 에이전트가 집에 들어올 때: 스마트 스피커를 넘는 **Mabu**의 보안·프라이버리 모델 (0) | 2026.04.12 |
댓글