어제(27일차)까지는 메모리를 중심으로 대화형 AI의 맥락 관리 방법을 살펴보았습니다.
오늘은 RAG의 또 다른 핵심 요소인 Document Loader를 다룹니다.
1. Document Loader란?
RAG 파이프라인을 떠올려 보면, 보통 다음 흐름으로 진행됩니다.
Document Loader → Text Splitter → Embeddings → Vector Store → Retriever → LLM
이 중 Document Loader는 가장 첫 단계로,
👉 “외부 문서를 LangChain이 이해할 수 있는 Document 객체로 불러오는 역할”을 합니다.
- 입력 소스 다양성 지원
- TXT, PDF, CSV, Markdown 같은 파일
- Notion, Google Docs, Slack 같은 SaaS 플랫폼
- API / DB 연결까지 가능
- LangChain 표준 객체(Document)로 변환
- Document는 page_content(본문 텍스트)와 metadata(출처/페이지 번호 등)를 담습니다.
- 즉, 다양한 데이터 소스를 일관된 형태로 불러오는 추상화 계층이 바로 Document Loader입니다.
언제 중요한가?
- RAG 프로젝트에서 데이터 소스가 다양할 때
- “내가 가진 문서를 모델이 읽고 답변해주길 원할 때”
- 데이터 전처리(요약, 파싱, 메타데이터 추가)를 위해 텍스트를 구조화된 단위로 불러올 때
2. Document 객체란?
LangChain에서 Document Loader는 단순히 텍스트만 불러오는 것이 아니라, 이를 Document 객체라는 표준 형태로 반환합니다.
Document는 크게 두 가지 속성을 가집니다.
from langchain_core.documents import Document
doc = Document(
page_content="이 문서는 Document 객체를 설명하는 예시입니다.",
metadata={"source": "example.txt", "page": 1}
)
doc.__dict__
출력 결과:
{'id': None,
'metadata': {'source': 'example.txt', 'page': 1},
'page_content': '이 문서는 Document 객체를 설명하는 예시입니다.',
'type': 'Document'}
- page_content
- 문서의 본문 텍스트를 담습니다.
- 예를 들어 PDF라면 각 페이지 텍스트, CSV라면 각 행(row)의 텍스트가 page_content가 됩니다.
- meta_data
- 문서에 대한 부가 정보(메타데이터)를 담습니다.
- 파일명, 페이지 번호, URL, 작성일자 등 출처 정보를 넣을 수 있습니다.
- 이 정보는 검색 시 “이 답변은 어떤 문서의 몇 페이지에서 나왔는지”를 알려주는 근거로 사용됩니다.
- 사용하는 모듈에 따라 다양한 구성으로 이루어져 있습니다.
metadata는 직접 추가도 가능합니다.
# 메타데이터 추가
doc.metadata["source"] = "TeddyNote"
doc.metadata["page"] = 2
doc.metadata["author"] = "Teddy"
doc.metadata
>> {'source': 'TeddyNote', 'page': 2, 'author': 'Teddy'}
3. Document Loader
RAG 파이프라인에서 Document Loader는 가장 첫 단계로, 외부 데이터를 불러와서 LangChain 표준 형식(Document)으로 변환하는 역할을 합니다.
쉽게 말해, “데이터 입력 게이트웨이” 입니다.
주요 Loader
- PyPDFLoader: PDF 파일을 로드하는 로더입니다.
- CSVLoader: CSV 파일을 로드하는 로더입니다.
- UnstructuredHTMLLoader: HTML 파일을 로드하는 로더입니다.
- JSONLoader: JSON 파일을 로드하는 로더입니다.
- TextLoader: 텍스트 파일을 로드하는 로더입니다.
- DirectoryLoader: 디렉토리를 로드하는 로더입니다.
PyPDFLoader 빠른 실습
의존성
pip install pypdf langchain-community
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("sample.pdf")
PyPDFLoader는 페이지 단위로 텍스트를 뽑아 Document 리스트로 반환하며,
각 Document.metadata에는 보통 아래 같은 정보가 자동 포함됩니다.
- source: 파일 경로
- page: 페이지 번호(0부터 시작)
- (버전에 따라 total_pages, pdf_miner 관련 필드 등 추가될 수 있음)
1) load() — 한 번에 전부 읽기
가장 기본. PDF 전체를 읽어 Document 리스트로 반환합니다.
페이지별 Document가 생성됩니다.
docs = loader.load()
print(len(docs), docs[0].page_content[:200])
print(docs[0].metadata) # {'source': 'sample.pdf', 'page': 0, ...}
언제?
- 파일이 크지 않고, 한번에 메모리로 올려도 무리 없을 때
- 후속 단계(분할/임베딩)를 별도로 처리할 계획일 때
2) load_and_split(splitter) — 로드 + 즉시 청크 분할
읽자마자 Text Splitter로 바로 청크를 잘라 반환합니다.
대부분의 RAG에서는 이 방식이 실전적입니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=120
)
docs = loader.load_and_split(text_splitter=splitter)
print(len(docs), docs[0].page_content[:200])
print(docs[0].metadata) # {'source': 'sample.pdf', 'page': 0, 'start_index': ...}
포인트
- chunk_size/overlap은 임베딩/검색 성능에 큰 영향 → 적절히 튜닝
- 페이지 메타데이터는 청크로 전파되므로 근거 추적에 유리
3) lazy_load() — 지연 로드(제너레이터)
한 번에 다 읽지 않고 이터레이터(제너레이터)로 Document를 순차 반환합니다.
대용량 PDF에 유리(메모리 절약).
for i, doc in enumerate(loader.lazy_load()):
print(i, doc.metadata, doc.page_content[:120])
if i >= 2:
break # 예시로 처음 3개 페이지만
언제?
- 수백/수천 페이지 PDF
- 스트리밍 파이프라인(읽으면서 바로 임베딩/인덱싱)
4) aload() — 비동기 로드 (async/await)
비동기 컨텍스트에서 한 번에 전체를 읽되, I/O를 await로 처리합니다.
웹 서버/배치 파이프라인에서 다른 작업과 병렬로 I/O를 겹치는 데 유용.
import asyncio
async def main():
docs = await loader.aload()
print(len(docs), docs[0].metadata, docs[0].page_content[:200])
asyncio.run(main())
참고
- 반환값은 load()와 동일하게 리스트입니다(한 번에 메모리 상주).
- “대용량 + 메모리 절약”이 목적이면 lazy_load()를,
- “비동기 I/O”가 목적이면 aload()를 선택하세요.
어떤 걸 언제 쓰면 될까?
| 메서드 | 반환 | 메모리 사용 | 추천 상황 |
| load() | List[Document] | 전체 상주 | 작은/중간 크기 PDF, 후처리 따로 |
| load_and_split() | List[Document] (청크) | 전체 상주 | 실전 RAG(바로 청크로 임베딩/색인) |
| lazy_load() | Iterator[Document] | 최소 | 대용량 PDF, 스트리밍 파이프라인 |
| aload() | List[Document] | 전체 상주 | 비동기 환경(I/O 겹치기) |
실무 팁
- 텍스트 추출 품질(스캔본이라면 UnstructuredPDFLoader, pytesseract 등 OCR 연동을 고려)
- PDF가 스캔본이면 OCR이 필요합니다. PyPDFLoader는 텍스트 레이어가 있어야 제대로 추출합니다.
- 메타데이터 보강
- metadata에 title, section, doc_id 등을 추가로 넣어두면 나중에 근거 링크/표시가 좋아집니다.
- 청크 튜닝
- 질의 유형(사실 질의/요약/코드 설명)에 따라 chunk_size/overlap을 조정하세요.
마무리
오늘은 Document Loader의 개념과 역할을 살펴보고, 그중에서도 가장 많이 사용되는 PyPDFLoader를 중심으로 다양한 메서드(load(), load_and_split(), lazy_load(), aload())를 정리했습니다.
핵심은 외부 문서를 LangChain의 Document 객체로 표준화하여 RAG 파이프라인의 출발점으로 삼는다는 점이었습니다.
특히 PDF와 같이 구조가 있는 데이터를 다룰 때는, 상황에 맞게 한 번에 로드(load()), 분할 후 로드(load_and_split()), 지연 로드(lazy_load()), 비동기 로드(aload()) 중 적합한 방식을 선택하는 것이 중요합니다.
- 작은 문서라면 → load()
- 바로 청크 단위로 색인하려면 → load_and_split()
- 수백/수천 페이지 같은 대용량이라면 → lazy_load()
- 비동기 서버/파이프라인 환경이라면 → aload()
👉 결국 Document Loader는 RAG의 입구 역할을 하며, 어떤 소스에서 데이터를 가져오든 LangChain의 표준 형식으로 일관되게 다룰 수 있게 해줍니다.
다음 시간에는 PDF와 CSV, Text, HTML, JSON, DirectoryLoader 등 다양한 Document Loader를 하나씩 다뤄보면서, 어떤 경우에 어떤 로더를 쓰면 좋은지 비교해 보겠습니다.
읽어주셔서 감사합니다.
