문서 청킹(Chunking) 전략 - 검색 품질을 좌우하는 핵심
지난 글에서 pgvector에 문서를 저장하고 검색하는 실습을 진행했습니다. 그런데 실제 문서는 그때처럼 짧은 한 문장이 아니라, 보통 페이지 단위, 문단 단위로 길이가 제각각입니다. 이번 글에서는 긴 문서를 어떻게 쪼개야 검색 품질이 좋아지는지, 청킹(Chunking) 전략을 정리합니다.
1. 왜 문서를 그대로 임베딩하면 안 되는가
문서 하나(예: 10페이지짜리 사내 규정집) 전체를 통째로 임베딩하면 두 가지 문제가 생깁니다.
- 임베딩 모델에는 입력 가능한 토큰 수 제한이 있어서, 긴 문서는 애초에 한 번에 넣을 수 없습니다.
- 설령 넣을 수 있다 해도, 문서 하나의 벡터는 "그 문서 전체의 평균적인 의미"로 뭉뚱그려지기 때문에, 문서 안의 특정 조항 하나를 묻는 질문과는 유사도가 낮게 나옵니다.
질문: "재택근무는 주 몇 회까지 가능해?"
문서 전체(10페이지, 연차/재택/출장/복지 등 다 포함)를 하나로 임베딩
→ 질문과의 유사도가 흐릿하게 나옴 (문서 전체 주제가 섞여 있어서)그래서 문서를 검색에 적합한 작은 단위, 즉 청크(Chunk)로 쪼개서 각각 임베딩하는 과정이 필요합니다.
2. 청크 크기는 어떻게 정하는가
청크가 너무 작으면 문맥이 잘려서 의미가 애매해지고, 너무 크면 다시 앞서 말한 "뭉뚱그려짐" 문제가 재발합니다.
| 청크 크기 | 문제점 |
|---|---|
| 너무 작음 (예: 50자) | 문장이 중간에 끊겨 문맥 손실, 검색은 잘 되지만 답변 근거로 쓰기엔 정보 부족 |
| 적절함 (예: 300~500자) | 하나의 완결된 의미 단위를 유지하면서 검색 정확도도 확보 |
| 너무 큼 (예: 2000자 이상) | 여러 주제가 섞여 유사도가 흐릿해짐, 불필요한 내용까지 함께 딸려옴 |
실무에서는 보통 300~500 토큰 사이에서 시작해서, 실제 검색 결과 품질을 보며 조정하는 경우가 많습니다.
3. 고정 크기 분할 (Fixed-size Chunking)
가장 단순한 방식은 글자 수(또는 토큰 수) 기준으로 일정하게 자르는 것입니다.
def fixed_size_chunk(text, chunk_size=500, overlap=50):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
구현이 간단하다는 장점이 있지만, 문장이나 문단 중간에서 뚝 잘릴 수 있다는 단점이 있습니다. 예를 들어 "입사 1년 이상 직원은 연 15" 까지만 잘리고 "일이 부여됩니다"가 다음 청크로 넘어가면, 두 청크 모두 온전한 의미를 갖지 못합니다.
4. Overlap(중첩)을 두는 이유
위 코드에서 overlap=50처럼, 청크끼리 일부 구간을 겹치게 자르는 것이 일반적입니다.
청크1: [-------- 0 ~ 500자 --------]
청크2: [-------- 450 ~ 950자 --------]
↑ 50자 구간이 겹침경계에 걸린 문장이 한쪽 청크에서만 잘려도, 겹치는 다른 청크에는 온전한 형태로 남아있을 확률이 높아집니다. 검색 시 두 청크 중 하나만 걸려도 문맥 손실 없이 답변에 활용할 수 있습니다.
5. 문단/문장 기준 분할 (Recursive Chunking)
고정 크기로 무 자르듯 나누는 대신, 문단이나 문장 경계를 우선적으로 존중하면서 크기를 맞추는 방식도 널리 쓰입니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_text(document_text)
separators에 지정된 구분자(문단 → 줄바꿈 → 문장 → 단어) 순서대로 우선 시도하면서, 가능한 한 의미 단위가 깨지지 않는 지점에서 잘라줍니다. 고정 크기 분할보다 문맥 보존이 잘 되어서, 실무에서 가장 널리 쓰이는 방식입니다.
6. 문서 구조를 활용한 분할 (Structure-aware Chunking)
마크다운 문서나 HTML처럼 구조(제목, 섹션)가 명확한 문서라면, 그 구조를 기준으로 나누는 것이 가장 정확합니다.
def split_by_heading(markdown_text):
sections = []
current_section = []
for line in markdown_text.split("\n"):
if line.startswith("## "):
if current_section:
sections.append("\n".join(current_section))
current_section = [line]
else:
current_section.append(line)
if current_section:
sections.append("\n".join(current_section))
return sections
## 제목 단위로 나누면, 각 청크가 "하나의 주제"를 온전히 담게 되어 검색 정확도가 크게 올라갑니다. 사내 위키나 매뉴얼처럼 구조가 잘 잡힌 문서라면 이 방식을 우선 고려하는 것이 좋습니다.
7. 청킹 전략 비교
| 방식 | 장점 | 단점 |
|---|---|---|
| 고정 크기 분할 | 구현이 매우 간단 | 문맥이 중간에 잘릴 수 있음 |
| 문단/문장 기준 분할 | 문맥 보존이 상대적으로 우수 | 문서에 따라 청크 크기 편차 발생 |
| 구조 기반 분할 | 의미 단위 보존이 가장 정확 | 구조가 없는 문서(일반 텍스트)에는 적용 어려움 |
8. 정리
- 문서를 통째로 임베딩하면 토큰 제한에 걸리거나, 의미가 뭉뚱그려져 검색 정확도가 떨어집니다.
- 청크 크기는 너무 작지도 크지도 않게, 보통 300~500 토큰 사이에서 시작해 조정합니다.
- Overlap을 두면 경계에서 문맥이 잘리는 문제를 완화할 수 있고, 문서 구조가 명확하다면 구조 기반 분할이 가장 정확도가 높습니다.
이렇게 청킹까지 마치면 임베딩, 벡터 DB 저장, 검색까지 RAG의 핵심 요소가 모두 갖춰집니다. 다음 글에서는 이 조각들을 하나로 이어서, LangChain으로 처음부터 끝까지 RAG 파이프라인을 직접 구축해보겠습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 10 - 검색 정확도를 높이는 방법 (0) | 2026.07.08 |
|---|---|
| STEP 9 - LangChain으로 RAG 파이프라인 처음부터 구축하기 (0) | 2026.07.08 |
| STEP 7 - pgvector 실습 (0) | 2026.07.07 |
| STEP 6 - 벡터 DB 선택하기 (0) | 2026.07.06 |
| STEP 5 - 벡터 DB의 필요성 (0) | 2026.07.06 |