RecursiveCharacterTextSplitter로 RAG 텍스트 분할하기
RAG 파이프라인 필수 도구, RecursiveCharacterTextSplitter 정리
RAG(Retrieval-Augmented Generation)를 구현하다 보면 항상 마주치는 문제가 있다. LLM에 넣을 수 있는 입력 길이는 한정돼 있는데, 실제로 다뤄야 하는 문서는 그보다 훨씬 길다는 것. 이럴 때 텍스트를 적당한 크기로 쪼개주는 게 텍스트 스플리터(Text Splitter)고, 랭체인에서 가장 널리 쓰이는 게 RecursiveCharacterTextSplitter다.
왜 그냥 자르면 안 될까
가장 단순한 방법은 텍스트를 500자, 1000자 단위로 기계적으로 자르는 것이다. 하지만 이렇게 하면 문장이나 문단 중간이 뚝뚝 잘려서 각 청크(chunk)만 봤을 때 문맥이 깨지는 경우가 많다. 임베딩 검색의 품질은 청크 하나하나가 얼마나 의미적으로 온전한 단위인지에 크게 좌우되기 때문에, 이건 꽤 치명적인 문제다.
동작 원리
핵심 아이디어는 "의미 단위를 최대한 지키면서, 정해진 길이 제한 안에서 재귀적으로 쪼갠다"는 것.
우선순위대로 시도하는 구분자:
\n\n— 문단 구분\n— 줄 구분" "— 단어 구분""— 글자 단위 (최후의 수단)
동작 흐름:
- 먼저 텍스트를
\n\n기준으로 나눠본다 - 나눈 결과가 여전히
chunk_size보다 크면 그 조각만 다시\n으로 나눈다 - 그래도 크면 공백으로, 그마저도 안 되면 글자 단위로 강제 분할
"문단 → 줄 → 단어 → 글자" 순으로 점점 잘게 재귀적으로 파고들면서 지정 크기 이하가 될 때까지 반복하는 방식. 그 결과 문단/문장 단위를 최대한 보존하면서도 청크 크기 제한은 확실히 지켜진다.
실습 코드
ch02_TEXT_SPLITTER.ipnyb
import urllib.request
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 실습을 위해 굉장히 긴 길이를 가진 텍스트를 다운로드 하고 길이를 출력
urllib.request.urlretrieve("https://sampleurl/sample.txt", filename="2025-07-09.txt")
withopen("sample.txt", encoding="utf-8")as f:
file = f.read()
print("텍스트 길이:",len(file))RecursiveCharacterTextSplitter()를 이용해 텍스트를 분할하는 text_splitter 객체 만들기
핵심 파라미터 2가지:
chunk_size: 청크 하나의 최대 길이chunk_overlap: 인접 청크끼리 겹치는 정도 (0을 지정하면 각 청크의 내용이 겹치지 않는다.)
text_splitter= RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0)이렇게 선언된 text_splitter로 파이썬 문자열을 분할할 때는 create_documents()를 사용한다.
texts= text_splitter.create_documents([text])
print("분할된 청크 수:",len(texts))분할된 청크의 형태
create_documents()로 분할한 결과는 문자열 리스트가 아니라 Document(page_content="...") 형태의 객체 리스트로 반환된다.
랭체인 실습에서 계속 마주치게 되는 형태라 기억해두면 좋다.
원문 텍스트에 접근하려면 각 청크 뒤에 .page_content를 붙이면 된다.
texts[1].page_content
texts[2].page_contentchunk_overlap이 실제로 하는 일
chunk_overlap=0일 때는 청크끼리 겹치지 않고 1번 청크 다음 내용이 그대로 2번 청크로 이어진다.
각 청크의 길이를 확인해보면 chunk_size=500을 넘지 않는 걸 볼 수 있다.
이번엔 chunk_overlap을 50으로 바꿔서 다시 분할해본다.
text_splitter= RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
texts= text_splitter.create_documents([text])
print("분할된 청크 수:",len(texts))chunk_overlap=50으로 설정하면 앞뒤 청크 사이에 약 50자 정도의 내용이 겹치도록 분할된다.
실제로 1번 청크의 마지막 부분과 2번 청크의 시작 부분을 비교해보면, 동일한 문장이 두 청크에 걸쳐 중복으로 포함돼 있는 걸 확인할 수 있다.
chunk_overlap이 0이었을 때보다 청크 수가 살짝 더 많아지는 것도 특징인데, 겹치는 구간만큼 텍스트가 더 잘게 나뉘기 때문이다.
한계: 길이 기준 분할의 문제점
RecursiveCharacterTextSplitter로 분할된 청크들을 살펴보면, 문맥이나 의미 단위로 나뉜 게 아니라 기본적으로 길이에 맞춰 분할된다는 걸 알 수 있다.
그래서 하나의 청크 안에 서로 다른 주제나 사건이 섞여 있다가 갑자기 끊기는 경우가 생긴다.
이런 방식은 분할 속도는 빠르지만, 내용적으로 완결되지 않은 청크가 만들어진다는 단점이 있다.
이런 청크가 그대로 RAG 파이프라인의 검색 입력으로 들어가면 검색 성능이 떨어지는 원인이 된다.
이 문제를 보완하기 위해 텍스트를 짧은 청크로 나눌 때 의미를 파악해서 문맥 단위로 분할하는 방식도 있는데, 랭체인에서도 이런 기능을 별도로 제공한다.
chunk_size는 어떻게 정할까
- 임베딩 모델의 토큰 제한 안에서 정해야 함
- 검색 정밀도 vs 문맥 보존: 청크가 작으면 검색은 정밀해지지만 문맥 부족, 크면 반대로 노이즈 섞임
- 문서 성격: 코드/논문/대화 로그마다 적정 크기가 다름
보통 300~1000자 사이에서 시작해 검색 품질 보면서 조정.
정리
RecursiveCharacterTextSplitter는 문단 → 줄 → 단어 → 글자 순으로 재귀적으로 텍스트를 쪼개면서 의미 단위를 최대한 보존하는 분할기chunk_size로 청크 최대 길이,chunk_overlap으로 겹침 정도 조절- 다만 길이 기준 분할이라 청크 중간에 문맥이 끊기는 한계가 있음
- RAG 검색 품질은 이 분할 단계에서 상당 부분 결정되므로 문서 특성에 맞게 튜닝 필요