728x90
반응형

어제(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'}
  1. page_content
    • 문서의 본문 텍스트를 담습니다.
    • 예를 들어 PDF라면 각 페이지 텍스트, CSV라면 각 행(row)의 텍스트가 page_content가 됩니다.
  2. 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를 하나씩 다뤄보면서, 어떤 경우에 어떤 로더를 쓰면 좋은지 비교해 보겠습니다.

 

읽어주셔서 감사합니다.

728x90
반응형

+ Recent posts