PDF 문서를 RAG용 벡터 데이터로 변환하는 청킹 전략과 파이썬 구현 가이드
레이아웃 인식 추출, 마크다운 계층 분할, 표 보존과 메타데이터 주입 기법

핵심 요약
- PDF는 시각적 배치를 위한 포맷이므로 단순 텍스트 추출보다 레이아웃을 인식해 마크다운으로 변환하는 전처리가 선행되어야 한다.
- 고정 글자 수 청킹 대신 문단, 섹션 헤더(#, ##), 표 단위의 의미론적(Semantic) 청킹을 적용해야 문맥 손실을 방지한다.
- 표(Table) 데이터는 셀 단위로 쪼개지 않고 단일 청크로 유지하거나 마크다운/HTML 표 포맷으로 직렬화해야 검색 신뢰도가 보장된다.
- 문서명, 페이지 번호, 상위 섹션 제목을 청크 메타데이터로 함께 인덱싱해야 LLM의 출처 인용(Citation) 정확도가 올라간다.
검색 증강 생성(RAG) 시스템의 성능을 좌우하는 가장 중요한 요소는 언어 모델의 파라미터 크기가 아니라, 지식 저장소에 색인된 텍스트 청크(Chunk)의 품질이다. 기업 보고서, 규정 문서, 기술 매뉴얼 등 기업 데이터의 80% 이상이 PDF 형식으로 보관되어 있지만, PDF는 태생적으로 인공지능 분석이 가장 까다로운 파일 형식으로 꼽힌다.
PDF는 컴퓨터가 데이터의 의미 구조(Semantic structure)를 파악하도록 만들어진 파일이 아니라, 종이에 인쇄할 시각적 좌표(X, Y 좌표 기반 텍스트와 벡터 그래픽)를 보존하는 포맷이기 때문이다. RAG의 개념과 필요성 및 하이브리드 검색과 리랭킹 기법에서 언급했듯이, 텍스트가 깨지거나 표가 분절된 채 임베딩되면 아무리 뛰어난 검색 알고리즘을 적용해도 올바른 답변을 생성할 수 없다. 이 글에서는 PDF 문서를 고품질 RAG 청크로 변환하는 실무 절차와 파이썬 구현 코드를 다룬다.
PDF 텍스트 추출이 RAG 검색 품질을 결정하는 이유
PDF에서 단순히 텍스트 레이어를 긁어내면 다단(Multi-column) 문서의 줄이 엉키고, 머리글(Header)과 쪽 번호(Footer)가 문맥 중간에 끼어들며, 표(Table)의 행과 열이 무작위 문자열로 풀려버린다.
이러한 지저분한 텍스트에 고정 글자 수(예: 500자 단위) 청킹을 적용하면, 하나의 문장이 두 청크로 쪼개지거나 표의 상단 헤더와 하단 데이터가 분리된다. 결과적으로 벡터 임베딩 코사인 유사도 계산 시 질문과 연관된 의미 벡터를 생성하지 못해 검색 단계에서 탈락하게 된다. 따라서 청킹 전 단계에서 문서를 마크다운이나 구조화된 객체로 변환하는 레이아웃 분석이 선행되어야 한다.
3대 PDF 청킹 방식의 성능 및 구조 보존 비교표
문서 특성에 따라 선택할 수 있는 3가지 주요 청킹 방식의 엔지니어링 특성은 다음과 같다.
| 청킹 방식 | 작동 원리 | 표·목록 보존율 | 문맥 연속성 | 권장 적용 시나리오 |
|---|---|---|---|---|
| 고정 크기 청킹 (Fixed-size) | 고정된 토큰/문자 수 단위로 절단 | 매우 낮음 (표 깨짐 발생) | 낮음 (문장 절단 빈번) | 균일한 평문 소설, 뉴스 기사 |
| 재귀적 분할 (Recursive) | 단락(\n\n) → 문장(\n) → 공백 순으로 계층 분할 | 보통 | 보통 | 일반 기술 블로그, FAQ 문서 |
| 문서 구조 인식 청킹 (Layout-aware) | 헤더(#), 표, 단락 객체를 인식해 의미 단위 분할 | 매우 높음 (마크다운 표 유지) | 매우 높음 | 다단 논문, 금융 보고서, API 명세서 |
PDF에서 고품질 RAG 청크를 생성하는 4단계 파이프라인
PDF 문서를 벡터 DB에 적재 가능한 최적의 청크로 가공하는 4단계 절차는 다음과 같다.
- 레이아웃 인식 파싱 (PDF to Markdown): 단순 텍스트 추출기 대신 문서의 제목 계층(#, ##), 본문 단락, 표, 캡션을 구분해 마크다운으로 변환한다.
- 반복 불필요 요소 제거 (Deduplication & Cleaning): 모든 페이지 상단과 하단에 반복되는 보고서 제목, 면책고지, 쪽 번호를 정규식으로 필터링한다.
- 마크다운 계층 기반 분할 (Markdown Header Splitting): H1, H2, H3 제목 기호를 기준으로 문맥을 분할하여 각 섹션의 독립적인 논리 단위를 유지한다.
- 메타데이터 주입 및 벡터 DB 적재: 분할된 청크마다 원본 파일명, 페이지 번호, 상위 헤더 경로(Breadcrumb)를 메타데이터로 첨부한다.
파이썬 기반 마크다운 헤더 및 메타데이터 보존 청킹 스크립트
아래는 LangChain의 MarkdownHeaderTextSplitter와 재귀적 분할기를 결합하여, 문서 구조를 보존하면서도 임베딩 토큰 한도를 넘지 않도록 안전하게 분할하는 파이썬 예제 코드다.
# scripts/chunk_pdf_markdown.py
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
from typing import List, Dict, Any
def process_structured_document(markdown_text: str, source_filename: str) -> List[Dict[str, Any]]:
# 1. 분할 기준이 될 마크다운 헤더 계층 정의
headers_to_split_on = [
("#", "Header_1"),
("##", "Header_2"),
("###", "Header_3"),
]
# 2. 헤더 기반 1차 구조적 분할
markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False
)
header_splits = markdown_splitter.split_text(markdown_text)
# 3. 개별 섹션이 너무 길 경우를 대비한 2차 재귀 분할기
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=80,
separators=["\n\n", "\n", ". ", " "]
)
final_chunks = text_splitter.split_documents(header_splits)
# 4. 파일명 및 검색용 메타데이터 강화
enriched_chunks = []
for idx, chunk in enumerate(final_chunks):
metadata = chunk.metadata.copy()
metadata.update({
"source": source_filename,
"chunk_index": idx,
"char_length": len(chunk.page_content)
})
enriched_chunks.append({
"content": chunk.page_content,
"metadata": metadata
})
return enriched_chunks
# 샘플 실행
if __name__ == "__main__":
sample_doc = """
# 결제 시스템 아키텍처
## 개요
이 문서는 분산 결제 시스템의 안정성 설계를 다룹니다.
## 트랜잭션 격리 수준
| 격리 수준 | 발생 가능한 이상 현상 | 권장 여부 |
| --- | --- | --- |
| Read Committed | Non-repeatable Read | 기본 설정 |
| Serializable | 성능 저하 (락 경합) | 특수 트랜잭션 한정 |
위 표에서 보듯이 대부분의 정산 작업은 Read Committed로 충분합니다.
"""
chunks = process_structured_document(sample_doc, "payment_arch.pdf")
print(f"생성된 청크 수: {len(chunks)}")
for c in chunks:
print(f"[{c['metadata']}] -> {c['content'][:50]}...")
다중 컬럼 및 복잡한 표 추출 시의 기술적 주의점
첫째, 표(Table) 데이터의 원형을 보존해야 한다. 표를 일반 텍스트로 풀어헤치면 컬럼 간의 연관 관계가 사라진다. 파이프라인에서 표를 추출할 때는 반드시 Markdown Table이나 HTML <table> 형태로 직렬화하고, 청크 경계에서 표의 중간 줄이 잘려나가지 않도록 분할 예외 규칙을 두어야 한다.
둘째, 청크 메타데이터에 섹션 경로를 남겨야 한다. 하위 단락만 떼어놓으면 "이 값이 무엇을 의미하는지" 주어가 소실된다. 상위 헤더 이름(예: Header_1: 결제 시스템 > Header_2: 트랜잭션 격리 수준)을 메타데이터로 함께 보관하면, LLM이 컨텍스트를 해석할 때 완벽한 맥락을 파악할 수 있다.
셋째, 임베딩 모델의 유효 컨텍스트 창을 고려해야 한다. 일반적인 임베딩 모델(OpenAI text-embedding-3-small, BGE-M3 등)은 512~8192 토큰을 지원하지만, 1,000자 이상의 너무 긴 청크는 벡터의 밀도가 희석되어 특정 키워드에 대한 유사도 점수가 낮아질 수 있다. 실무에서는 400~800자 범위의 정밀 청크를 유지하는 것이 검색 정확도에 가장 유리하다.
PDF 데이터의 구조적 청킹을 체계화함으로써, RAG 시스템의 환각을 줄이고 사용자 질의에 대해 정확한 출처와 근거를 제시하는 신뢰도 높은 검색 파이프라인을 완성할 수 있다.
초보자가 자주 실수하는 포인트
- 표가 다수 포함된 2단(Two-column) 레이아웃 PDF를 일반 문자열로 읽어 줄과 단락 순서가 뒤섞이는 경우
- 페이지 상하단 머리글(Header)과 바닥글(Footer)을 제거하지 않고 청크마다 중복 삽입하여 임베딩 품질을 떨어뜨리는 경우
- 청크 분할 시 겹침 영역(Chunk Overlap)을 전혀 두지 않아 문장 중간이 잘려 단어 의미가 단절되는 경우
체크리스트
- PDF 파서가 2단 레이아웃과 표 경계를 올바르게 인식하고 있는가
- 청크마다 소스 파일명, 페이지 번호, 섹션 계층 메타데이터가 첨부되었는가
- 임베딩 모델의 최대 토큰 한도(Context limit)를 넘지 않는 청크 크기를 설정했는가
- 표나 코드 블록의 중간 행이 청크 경계에서 단절되지 않도록 방어 로직이 적용되었는가
자주 묻는 질문
청크 크기(Chunk Size)와 오버랩(Overlap)의 권장 기본값은 얼마인가요?
일반적인 텍스트 RAG에서는 500~1,000자(약 200~400 토큰) 크기에 10~20% 수준(50~100자)의 오버랩을 기본 출발점으로 설정합니다. 의미 단위가 중요한 기술 명세서나 규정 문서는 단락 또는 섹션 헤더 단위의 분할이 훨씬 유리합니다.
스캔된 이미지형 PDF는 어떻게 처리해야 하나요?
텍스트 레이어가 없는 스캔본은 Tesseract, AWS Textract, 또는 VLM(Vision-Language Model) 기반의 OCR 엔진을 통과시켜 텍스트와 좌표를 추출한 뒤 청킹 파이프라인으로 연결해야 합니다.
표(Table) 데이터를 가장 효과적으로 청킹하는 방법은 무엇인가요?
표를 일반 텍스트로 풀어쓰기보다 마크다운 표 또는 HTML 태그 형태로 변환하여 하나의 독립된 노드로 관리하고, 상단에 표의 캡션(제목)과 요약 설명을 주석으로 달아 임베딩하는 방식이 가장 우수한 검색 재현율을 보입니다.
참고 자료 · 검증 기준
- LangChain Documentation — Text Splitters and Document Loaders
- LlamaIndex Documentation — Node Parsers and Chunking
- Unstructured.io Documentation — PDF Partitioning Strategies
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.