개발 slecs

대용량 PDF 추출 파이프라인을 청크 분할·병합 구조로 개선하고 빠른

목차

대용량 PDF 처리 파이프라인을 chunk+merge 방식으로 전면 재설계하고, 빠른 미리보기를 위해 Haiku 모델을 별도 경로로 붙였다.


왜 이 작업이 필요했나

PDF 기반 데이터 추출 기능을 운영하다 보면 반드시 벽에 부딪히는 시점이 온다. 작은 문서는 문제없이 돌아가다가, 어느 순간 페이지 수가 수십~수백 장짜리 문서가 들어오면 API 타임아웃이 터지거나 토큰 한도를 초과해버린다. 이번 작업의 발단도 거기서 시작됐다.

단순히 문서 전체를 통째로 모델에 던지는 방식은 소규모 PoC 단계에서는 괜찮다. 그런데 실제 서비스 환경에서는 문서 크기가 예측 불가능하고, 모델마다 컨텍스트 윈도우 상한이 다르며, 응답 지연이 UX에 직접적인 영향을 준다. 팀 내에서도 "작은 문서는 빠른데 큰 문서는 왜 이렇게 느리냐"는 피드백이 쌓이고 있었고, 더 이상 미룰 수 없는 구조 개선 타이밍이었다.


작업 내용: chunk+merge 파이프라인 + Haiku 미리보기

이번에 건드린 파일은 크게 네 곳이다.

파일 역할 이번 변경
src/lib/pdf.ts PDF 파싱 / 페이지 분리 문서를 chunk 단위로 분할하는 로직 추가
src/lib/anthropic.ts LLM 호출 추상화 모델 선택 분기 (Haiku / 기존 모델) 처리
src/lib/extraction-job.ts 추출 작업 상태 관리 chunk 단위 작업 + 최종 merge 단계 오케스트레이션
src/app/api/extract/route.ts API 엔드포인트 미리보기 요청과 전체 추출 요청 분기 처리

핵심 흐름은 이렇다.

[대용량 PDF 입력]
       
  pdf.ts: 페이지 단위 chunk 분할
       
  extraction-job.ts: chunk 별 병렬/순차 추출 작업 스케줄링
       
  anthropic.ts: 각 chunk → LLM 호출
       
  extraction-job.ts: 결과 merge → 최종 구조화 데이터
       
  route.ts: 클라이언트 응답

미리보기 경로는 별도로 분기된다. 전체 문서를 다 처리하기 전에 사용자에게 "대략 이런 결과가 나올 것 같다"는 피드백을 주기 위해 첫 번째 chunk 혹은 요약 수준의 처리를 Haiku 모델로 빠르게 돌린다. Haiku는 속도가 빠르고 비용이 낮은 대신 정밀도가 다소 떨어지는데, 미리보기 용도라면 그 트레이드오프가 충분히 납득된다.

// anthropic.ts 내 모델 선택 분기 (패턴 예시)
function selectModel(mode: 'preview' | 'full') {
  if (mode === 'preview') {
    return 'claude-haiku-*'; // 빠른 응답 우선
  }
  return 'claude-sonnet-*'; // 정밀도 우선
}

chunk+merge 패턴에서 항상 신경 써야 하는 건 merge 단계의 일관성이다. 각 chunk에서 나온 결과를 단순히 이어 붙이면 경계 부분에서 중복이 생기거나 맥락이 끊기는 문제가 있다. 이 부분은 extraction-job.ts에서 merge 오케스트레이션을 담당하게 했는데, chunk 간 겹치는 컨텍스트를 어느 정도 허용하면서 최종 결합 시 중복을 정리하는 방향으로 설계했다.


회고: 이 패턴을 선택한 이유와 남은 숙제

chunk+merge 외에도 선택지는 있었다.

  • 스트리밍 처리: 결과를 실시간으로 흘려보내는 방식. 구현 복잡도가 높고 merge 로직을 클라이언트까지 내려야 한다.
  • 문서 압축/요약 후 단일 호출: 정보 손실 위험이 있고, 구조화 데이터 추출 정밀도가 떨어진다.
  • chunk+merge (채택): 구현이 명확하고, 실패한 chunk만 재처리하는 재시도 전략도 붙이기 쉽다.

팀 내에서 논의할 때 "완벽한 정밀도 vs. 빠른 사용자 피드백" 사이의 균형이 주요 쟁점이었다. 결국 전체 추출은 정밀도 우선, 미리보기는 속도 우선으로 두 트랙을 병렬로 가져가는 게 현실적인 답이었다.

남은 숙제가 없는 건 아니다. chunk 크기를 지금은 고정값으로 잘라내고 있는데, 문서의 구조(섹션, 헤더 경계)를 인식해서 의미 단위로 분할하면 merge 품질이 훨씬 올라갈 것이다. 그리고 대용량 문서 처리 시 작업 상태를 어떻게 persist하고, 중간에 실패했을 때 어디서부터 재개할지 — 이 부분은 extraction-job.ts가 더 성숙해져야 제대로 풀린다.

이번 작업을 통해 다시 확인한 건, 파이프라인 설계는 "평균 케이스"가 아니라 "극단적인 입력"을 기준으로 짜야 한다는 점이다. 작은 문서만 보고 설계하면 언제나 나중에 뜯어고치게 된다.

다음


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.