본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 25. · 3 Views
문서 청킹 전략 완벽 가이드
RAG 시스템의 핵심인 문서 청킹 전략을 초급 개발자도 쉽게 이해할 수 있도록 실무 사례와 함께 설명합니다. 고정 크기부터 의미 기반 청킹까지, 최적의 청킹 전략을 찾는 방법을 배워봅니다.
목차
- Chunk Size와 Overlap 결정
- 고정 크기 vs 의미 기반 청킹
- Sentence Splitting
- Recursive Chunking
- 실습: 다양한 청킹 전략 실험
- 실습: 최적 Chunk Size 찾기
1. Chunk Size와 Overlap 결정
신입 개발자 김개발 씨가 처음으로 RAG 시스템을 구축하게 되었습니다. "문서를 벡터 DB에 넣으면 된다"는 건 알겠는데, 도대체 문서를 얼마나 작게 나눠야 할까요?
선배 박시니어 씨가 말합니다. "청킹이 RAG의 80%를 결정하지."
Chunk Size는 문서를 나눌 때 각 조각의 크기를 의미하고, Overlap은 조각들 사이의 중복 영역을 의미합니다. 마치 책을 여러 장으로 나눌 때, 각 장의 마지막 문단을 다음 장의 첫 문단으로도 포함시키는 것과 같습니다.
이 두 가지 파라미터가 검색 성능을 크게 좌우합니다.
다음 코드를 살펴봅시다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 기본적인 청킹 설정
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 각 청크의 최대 길이
chunk_overlap=50, # 청크 간 중복 영역
length_function=len, # 길이 측정 함수
separators=["\n\n", "\n", " ", ""] # 분리 우선순위
)
# 문서 청킹 실행
chunks = text_splitter.split_text(document_text)
print(f"총 {len(chunks)}개의 청크 생성됨")
김개발 씨는 입사 2개월 차 주니어 개발자입니다. 오늘 팀장님께 첫 AI 프로젝트를 받았습니다.
"사내 문서 검색 챗봇을 만들어봐요." 설레는 마음으로 LangChain 문서를 읽기 시작했지만, 첫 장부터 난관에 부딪혔습니다. "chunk_size를 얼마로 설정해야 하지?" 문서에는 친절하게도 기본값만 적혀 있을 뿐, 왜 그 값을 선택해야 하는지는 나와 있지 않았습니다.
선배 박시니어 씨가 커피를 한 잔 들고 다가왔습니다. "청킹 때문에 고민이구나.
이건 RAG 시스템의 가장 중요한 결정 중 하나야." 청킹이란 무엇일까요? 쉽게 비유하자면, 청킹은 마치 백과사전을 여러 권으로 나누는 작업과 같습니다.
너무 크게 나누면 원하는 정보를 찾기 어렵고, 너무 작게 나누면 문맥을 잃어버립니다. 딱 적당한 크기로 나눠야 나중에 필요한 정보를 빠르게 찾을 수 있습니다.
Chunk Size는 각 조각의 크기를 결정합니다. 일반적으로 문자 수나 토큰 수로 측정하며, 보통 200에서 1000 사이의 값을 사용합니다.
너무 작으면 문맥이 부족하고, 너무 크면 관련 없는 정보까지 포함됩니다. Overlap은 왜 필요할까요?
문서를 딱딱 자르면 중요한 정보가 두 조각으로 나뉠 수 있습니다. 예를 들어 "파이썬의 리스트 컴프리헨션은 매우 강력합니다"라는 문장이 "파이썬의 리스트 컴프리헨션은"과 "매우 강력합니다"로 나뉜다면, 어느 조각도 완전한 의미를 전달하지 못합니다.
Overlap을 설정하면 이런 문제를 해결할 수 있습니다. 각 청크가 이전 청크의 마지막 부분을 일부 포함하게 되어, 문맥의 연속성이 유지됩니다.
일반적으로 chunk_size의 10-20% 정도를 overlap으로 설정합니다. 박시니어 씨가 화이트보드에 그림을 그리며 설명합니다.
"청크가 너무 작으면 검색 정확도는 높지만 문맥이 부족해. 반대로 너무 크면 관련 없는 정보까지 섞여서 LLM이 혼란스러워하지." 실제 현업에서는 어떻게 결정할까요?
대부분의 경우 실험을 통해 최적값을 찾습니다. 문서의 특성에 따라 다르기 때문입니다.
기술 문서라면 500-800자가 적당하고, 대화형 로그라면 200-300자가 좋을 수 있습니다. 김개발 씨가 질문합니다.
"그럼 모든 청크를 같은 크기로 만들어야 하나요?" "좋은 질문이야. 꼭 그렇지는 않아.
의미 단위로 나누는 방법도 있어. 하지만 처음에는 고정 크기로 시작하는 게 좋아." 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 RecursiveCharacterTextSplitter를 임포트합니다. 이것은 LangChain에서 제공하는 가장 범용적인 텍스트 분할 도구입니다.
chunk_size=500은 각 청크가 최대 500자가 되도록 설정합니다. chunk_overlap=50은 각 청크가 이전 청크의 마지막 50자를 포함하도록 합니다.
separators 파라미터가 중요합니다. 이것은 문서를 나눌 때 어떤 구분자를 우선적으로 사용할지 정합니다.
먼저 두 줄 바꿈으로 나누고, 안 되면 한 줄 바꿈, 그것도 안 되면 공백, 마지막으로 문자 단위로 나눕니다. 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 chunk_size를 임베딩 모델의 최대 토큰 길이와 혼동하는 것입니다. 예를 들어 OpenAI의 임베딩 모델은 8191 토큰까지 처리할 수 있지만, 청크 크기를 그렇게 크게 설정하면 검색 정확도가 떨어집니다.
임베딩 모델의 한계가 아니라 검색 품질을 기준으로 정해야 합니다. 또 다른 실수는 overlap을 너무 크게 설정하는 것입니다.
overlap이 chunk_size의 50%를 넘어가면 중복이 과도해져서 저장 공간 낭비와 검색 속도 저하를 초래합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 실험이 중요하군요!" Chunk Size와 Overlap을 적절히 설정하면 RAG 시스템의 성능이 크게 향상됩니다.
처음에는 chunk_size=500, chunk_overlap=50 정도로 시작해서, 점진적으로 최적값을 찾아가세요.
실전 팁
💡 - 처음에는 chunk_size=500, overlap=50으로 시작하고 점진적으로 조정하세요
- overlap은 chunk_size의 10-20%가 적당합니다
- 문서 특성에 따라 다르므로 반드시 실험을 통해 검증하세요
2. 고정 크기 vs 의미 기반 청킹
김개발 씨가 청킹 코드를 작성하고 있는데, 이상한 현상을 발견했습니다. 어떤 청크는 완벽한 문단을 포함하는데, 어떤 청크는 문장 중간에서 뚝 끊깁니다.
"이거 뭔가 이상한데..." 선배에게 물으니 "고정 크기 청킹의 한계"라고 합니다.
고정 크기 청킹은 문자 수나 토큰 수 기준으로 기계적으로 자르는 방식이고, 의미 기반 청킹은 문단, 문장, 주제 등 의미 단위로 나누는 방식입니다. 마치 피자를 자를 때 눈대중으로 8등분 하는 것과 토핑 배치를 고려해서 자르는 것의 차이입니다.
각각 장단점이 있어서 상황에 맞게 선택해야 합니다.
다음 코드를 살펴봅시다.
from langchain.text_splitter import CharacterTextSplitter
from semantic_text_splitter import TextSplitter
# 방법 1: 고정 크기 청킹 (빠르지만 문맥 무시)
fixed_splitter = CharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separator="\n"
)
fixed_chunks = fixed_splitter.split_text(text)
# 방법 2: 의미 기반 청킹 (느리지만 문맥 보존)
semantic_splitter = TextSplitter(max_characters=500)
semantic_chunks = semantic_splitter.chunks(text)
print(f"고정 크기: {len(fixed_chunks)}개")
print(f"의미 기반: {len(semantic_chunks)}개")
김개발 씨가 첫 번째 프로토타입을 완성했습니다. 고정 크기 청킹을 사용해서 사내 문서 1000개를 처리했습니다.
빠르게 동작했고, 벡터 DB에도 잘 저장되었습니다. "와, 생각보다 쉽네!" 하지만 실제로 검색을 해보니 이상한 결과가 나왔습니다.
"파이썬 설치 방법"을 검색했더니 중간에 잘린 문장이 반환되었습니다. "파이썬을 설치하려면 먼저"까지만 있고 뒷부분은 다른 청크에 있었습니다.
박시니어 씨가 화면을 보더니 웃으며 말합니다. "고정 크기 청킹의 전형적인 문제야.
문맥을 고려하지 않고 무작정 자르니까 이런 일이 생기지." 고정 크기 청킹은 가장 간단하고 빠른 방법입니다. 문자 수나 토큰 수를 세면서 기계적으로 잘라냅니다.
구현이 쉽고 속도가 빠르며, 모든 청크가 비슷한 크기를 가집니다. 하지만 치명적인 단점이 있습니다.
문장이나 문단 중간에서도 가차없이 잘립니다. 예를 들어 "머신러닝 모델을 학습시키려면 먼저 데이터를 전처리해야 합니다.
전처리 과정에서는..."이라는 문장이 있다면, 딱 500자에서 잘려서 "머신러닝 모델을 학습시키려면 먼저 데이터를 전"까지만 들어갈 수 있습니다. 의미 기반 청킹은 이런 문제를 해결합니다.
문장, 문단, 심지어 주제까지 고려해서 의미 있는 단위로 나눕니다. 마치 신문 기사를 단락별로 읽는 것처럼, 각 청크가 완전한 의미를 가지게 됩니다.
검색 품질이 훨씬 좋아지지만, 처리 속도가 느리고 청크 크기가 불균일합니다. 김개발 씨가 궁금해합니다.
"그럼 무조건 의미 기반이 좋은 거 아닌가요?" 박시니어 씨가 고개를 젓습니다. "상황에 따라 다르지.
문서가 정형화되어 있고 속도가 중요하면 고정 크기도 괜찮아. 반대로 복잡한 기술 문서나 법률 문서라면 의미 기반이 낫지." 실제 현업에서는 어떻게 선택할까요?
대부분의 서비스는 하이브리드 접근을 사용합니다. 먼저 문단이나 섹션으로 나누고, 각 조각이 너무 크면 다시 고정 크기로 자르는 방식입니다.
이렇게 하면 두 방법의 장점을 모두 얻을 수 있습니다. 위의 코드를 살펴보겠습니다.
CharacterTextSplitter는 고정 크기 청킹을 수행합니다. separator="\n"으로 설정하면 줄바꿈을 우선적으로 고려하지만, chunk_size를 초과하면 가차없이 자릅니다.
빠르고 예측 가능하지만 문맥 손실이 있습니다. semantic_text_splitter는 의미 기반 청킹을 수행합니다.
문장 경계를 인식하고, 가능한 한 완전한 문장 단위로 나눕니다. max_characters는 최대 크기 제한이며, 이를 초과하지 않는 선에서 의미 단위를 유지합니다.
두 방법의 결과를 비교하면 청크 개수가 다를 수 있습니다. 고정 크기는 정확히 계산 가능하지만, 의미 기반은 문서 구조에 따라 달라집니다.
주의할 점이 있습니다. 의미 기반 청킹을 사용할 때, 임베딩 모델의 토큰 제한을 반드시 확인해야 합니다.
어떤 청크는 1000자가 넘어갈 수 있는데, 이것이 모델의 한계를 초과하면 에러가 발생합니다. max_characters를 보수적으로 설정하세요.
또한 의미 기반 청킹은 언어에 따라 성능이 다릅니다. 영어는 문장 구분이 명확하지만, 한국어는 문장 끝 판단이 애매한 경우가 많습니다.
한국어 문서라면 추가적인 전처리가 필요할 수 있습니다. 김개발 씨는 두 방법을 모두 테스트해보기로 했습니다.
"일단 두 가지 버전을 만들어서 A/B 테스트를 해봐야겠어요." 박시니어 씨가 엄지를 치켜세웁니다. "좋은 생각이야.
데이터가 답을 알려줄 거야." 고정 크기와 의미 기반 청킹 중 무엇을 선택할지는 여러분의 상황에 달려 있습니다. 속도와 품질 사이의 트레이드오프를 이해하고, 실험을 통해 최선의 선택을 하세요.
실전 팁
💡 - 처음에는 고정 크기로 시작해서 빠르게 프로토타입을 만드세요
- 품질이 중요하면 의미 기반으로 전환하되, 처리 시간 증가를 고려하세요
- 하이브리드 접근(문단 분리 + 크기 제한)이 실전에서 가장 효과적입니다
3. Sentence Splitting
김개발 씨의 RAG 시스템이 또 이상한 답변을 내놓았습니다. "API 인증 방법은 JWT를 사용하"라는 문장 중간까지만 검색되어서, LLM이 엉뚱한 답을 생성했습니다.
"문장 단위로 자르면 안 되나?" 검색해보니 Sentence Splitting이라는 기법이 있었습니다.
Sentence Splitting은 문서를 문장 단위로 정확하게 나누는 기법입니다. 마치 칼질을 할 때 재료의 결을 따라 자르는 것처럼, 문장의 자연스러운 경계를 따라 분리합니다.
단순히 마침표로 자르는 것이 아니라, 약어, 숫자, 인용문 등을 고려해서 지능적으로 처리합니다.
다음 코드를 살펴봅시다.
import re
from typing import List
def smart_sentence_split(text: str) -> List[str]:
"""문장을 지능적으로 분리하는 함수"""
# 약어 보호: Dr., Mr., etc.를 임시 토큰으로 치환
text = re.sub(r'(Dr|Mr|Mrs|Ms|Prof)\\.', r'\1<DOT>', text)
# 숫자 뒤 마침표 보호: 1.5, 3.14 등
text = re.sub(r'(\d+)\.(\d+)', r'\1<DOT>\2', text)
# 문장 분리: 마침표, 느낌표, 물음표 기준
sentences = re.split(r'(?<=[.!?])\s+', text)
# 임시 토큰 복원
sentences = [s.replace('<DOT>', '.') for s in sentences]
return [s.strip() for s in sentences if s.strip()]
김개발 씨가 회의실에서 머리를 싸매고 있었습니다. 테스트 케이스 100개 중 30개가 이상한 답변을 내놓았습니다.
문제를 분석해보니 대부분 문장이 중간에 잘린 경우였습니다. "Dr.
Kim의 연구에 따르면..."이라는 문장이 "Dr."에서 잘려서 "Dr"와 "Kim의 연구에 따르면..."으로 나뉘었습니다. 당연히 의미가 망가졌습니다.
박시니어 씨가 코드를 보더니 말합니다. "split('.') 이렇게 단순하게 자르면 안 돼.
Sentence Splitting을 제대로 구현해야 해." Sentence Splitting은 생각보다 복잡한 작업입니다. 단순히 마침표만 찾으면 되는 것 같지만, 실제로는 수많은 예외 상황이 있습니다.
"Dr. Smith가 3.14를 발견했다."라는 문장에는 마침표가 3개나 있지만, 실제 문장 끝은 맨 마지막입니다.
영어의 경우 약어가 큰 문제입니다. Dr., Mr., Mrs., Inc., Ltd.
등 수백 개의 약어가 있습니다. 이것들을 모두 구분해야 정확한 문장 분리가 가능합니다.
한국어는 또 다른 문제가 있습니다. 마침표 대신 느낌표와 물음표도 문장 끝을 나타낼 수 있고, 때로는 온점 없이도 문장이 끝납니다.
"안녕하세요 저는 김개발입니다 잘 부탁드립니다"처럼요. 김개발 씨가 질문합니다.
"그럼 라이브러리를 쓰는 게 낫지 않나요?" 박시니어 씨가 고개를 끄덕입니다. "맞아.
NLTK나 spaCy 같은 라이브러리가 이미 훌륭하게 구현되어 있어. 하지만 원리는 알아야 하니까 직접 만들어보는 거야." 실제 현업에서는 어떻게 처리할까요?
대부분의 프로덕션 시스템은 전문 라이브러리를 사용합니다. spaCy의 경우 머신러닝 모델을 사용해서 문장 경계를 탐지하므로 정확도가 매우 높습니다.
하지만 속도가 느리고 모델 로딩에 메모리가 필요합니다. 간단한 문서라면 정규식 기반 방법도 충분합니다.
위 코드처럼 주요 예외 상황만 처리하면 90% 이상의 정확도를 얻을 수 있습니다. 위의 코드를 자세히 살펴보겠습니다.
먼저 약어를 보호합니다. Dr., Mr.
같은 패턴을 찾아서 마침표를 <DOT>이라는 임시 토큰으로 치환합니다. 이렇게 하면 나중에 문장을 분리할 때 이 마침표들이 무시됩니다.
다음으로 숫자 사이의 마침표도 보호합니다. 3.14나 1.5 같은 소수점을 문장 끝으로 오인하면 안 되기 때문입니다.
이제 진짜 문장 분리를 수행합니다. (?<=[.!?])\s+ 정규식은 "마침표, 느낌표, 물음표 뒤에 공백이 오는 지점"을 찾습니다.
(?<=...)는 긍정 후방탐색으로, 패턴을 찾되 결과에 포함시키지 않습니다. 마지막으로 임시 토큰을 원래대로 복원합니다.
<DOT>을 다시 마침표로 바꿔줍니다. 주의할 점이 있습니다.
이 방법은 영어에 최적화되어 있습니다. 한국어 문서를 처리한다면 KoNLPy나 kiwipiepy 같은 한국어 특화 라이브러리를 사용하세요.
정규식만으로는 한국어의 복잡한 문장 구조를 완벽하게 처리하기 어렵습니다. 또한 문장이 너무 짧거나 너무 길 수도 있습니다.
"네."나 "좋습니다." 같은 한 단어 문장은 문맥이 부족하고, 반대로 100단어가 넘는 긴 문장은 너무 많은 정보를 담고 있습니다. 문장 분리 후에 길이 검증을 추가하세요.
김개발 씨는 코드를 수정해서 다시 테스트했습니다. 이번에는 95개가 정확한 답변을 내놓았습니다.
"와, 5개만 틀렸어요!" 박시니어 씨가 웃으며 말합니다. "100% 완벽은 없어.
95%면 훌륭한 거야. 나머지는 케이스별로 예외 처리를 추가하면 돼." Sentence Splitting을 제대로 구현하면 RAG 시스템의 품질이 크게 향상됩니다.
문장이 완전한 의미를 가지므로, LLM이 더 정확한 답변을 생성할 수 있습니다.
실전 팁
💡 - 간단한 문서는 정규식으로 충분하지만, 복잡한 문서는 spaCy나 NLTK를 사용하세요
- 약어 사전을 미리 만들어두면 정확도가 높아집니다
- 한국어는 전용 라이브러리(KoNLPy, kiwipiepy)를 사용하세요
4. Recursive Chunking
김개발 씨가 대용량 기술 문서를 처리하다가 또 문제에 부딪혔습니다. 어떤 섹션은 10줄인데 어떤 섹션은 1000줄입니다.
고정 크기로 자르면 큰 섹션이 여러 조각으로 쪼개지고, 의미 기반으로 자르면 너무 큰 청크가 생깁니다. "둘 다 쓸 수 있는 방법은 없나?" 박시니어 씨가 화이트보드에 트리 그림을 그리며 "Recursive Chunking"을 설명합니다.
Recursive Chunking은 여러 단계의 구분자를 순차적으로 적용해서 문서를 나누는 방법입니다. 마치 나무를 베어서 토막 내고, 토막을 쪼개고, 쪼갠 조각을 다시 자르는 것처럼, 큰 단위부터 작은 단위까지 재귀적으로 분할합니다.
이렇게 하면 문서 구조를 최대한 보존하면서도 적절한 크기의 청크를 얻을 수 있습니다.
다음 코드를 살펴봅시다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 계층적 구분자 정의: 큰 단위부터 작은 단위로
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100,
separators=[
"\n\n\n", # 섹션 구분 (빈 줄 3개)
"\n\n", # 문단 구분 (빈 줄 2개)
"\n", # 줄바꿈
". ", # 문장 끝
" ", # 단어 구분
"" # 문자 단위 (최후의 수단)
]
)
chunks = text_splitter.split_text(long_document)
for i, chunk in enumerate(chunks):
print(f"청크 {i}: {len(chunk)}자, 시작='{chunk[:30]}...'")
김개발 씨는 회사의 500페이지짜리 API 문서를 RAG 시스템에 넣어야 했습니다. 문서는 잘 구조화되어 있었습니다.
대제목, 중제목, 소제목, 문단, 문장이 명확하게 구분되어 있었습니다. 하지만 문제가 있었습니다.
고정 크기 청킹을 사용하면 "인증" 섹션의 중요한 내용이 5개 조각으로 쪼개졌습니다. 각 조각만으로는 완전한 의미를 파악하기 어려웠습니다.
의미 기반 청킹을 시도했더니 이번에는 어떤 청크가 3000자가 넘어갔습니다. 임베딩 모델의 한계를 초과했습니다.
박시니어 씨가 상황을 듣더니 말합니다. "전형적인 문제야.
Recursive Chunking으로 해결할 수 있어." Recursive Chunking은 분할 정복 전략을 사용합니다. 먼저 가장 큰 단위의 구분자로 문서를 나눕니다.
예를 들어 "\n\n\n"으로 섹션을 구분합니다. 각 섹션의 크기를 확인해서, chunk_size보다 작으면 그대로 둡니다.
하지만 섹션이 너무 크다면? 다음 단계의 구분자를 적용합니다.
"\n\n"으로 문단을 구분합니다. 여전히 크다면?
"\n"으로 줄을 구분합니다. 이런 식으로 재귀적으로 내려갑니다.
마지막까지 크기가 맞지 않으면 어떻게 할까요? 최후의 수단으로 문자 단위로 자릅니다.
하지만 대부분의 경우 그 전에 적절한 크기를 찾게 됩니다. 이 방법의 장점은 문서 구조를 최대한 보존한다는 것입니다.
잘 작성된 기술 문서는 계층적 구조를 가지고 있습니다. 섹션, 문단, 문장이 논리적으로 연결되어 있습니다.
Recursive Chunking은 이 구조를 존중합니다. 가능하면 섹션 단위로, 안 되면 문단 단위로, 최후에는 문장 단위로 나눕니다.
김개발 씨가 화이트보드의 트리 그림을 보며 고개를 끄덕입니다. "아, 그래서 Recursive구나.
위에서 아래로 재귀적으로 내려가는 거네요." 박시니어 씨가 웃습니다. "정확해.
컴퓨터 과학의 많은 문제가 재귀로 해결되는 것처럼, 문서 분할도 마찬가지야." 실제 현업에서는 어떻게 활용할까요? 대부분의 RAG 시스템이 RecursiveCharacterTextSplitter를 기본값으로 사용합니다.
LangChain, LlamaIndex 등 주요 프레임워크가 모두 이 방식을 추천합니다. 범용성이 높고, 대부분의 문서 타입에서 좋은 성능을 보이기 때문입니다.
위의 코드를 단계별로 살펴보겠습니다. separators 리스트가 핵심입니다.
첫 번째 요소인 "\n\n\n"은 섹션 구분자입니다. 많은 마크다운 문서나 기술 문서가 섹션 사이에 빈 줄을 여러 개 넣습니다.
두 번째 "\n\n"은 문단 구분자입니다. 일반적인 문단 사이의 빈 줄 하나를 의미합니다.
세 번째 "\n"은 단순 줄바꿈입니다. ".
"는 문장 끝을 나타냅니다. 마침표 뒤 공백이 있는 패턴을 찾습니다.
마지막 " "는 단어 구분, ""는 문자 단위 분할입니다. splitter는 이 순서대로 시도합니다.
먼저 "\n\n\n"으로 나누고, 각 조각이 1000자 이하인지 확인합니다. 넘으면 "\n\n"으로 다시 나눕니다.
이 과정을 재귀적으로 반복합니다. 주의할 점이 있습니다.
separators의 순서가 매우 중요합니다. 큰 단위를 먼저, 작은 단위를 나중에 배치해야 합니다.
순서를 바꾸면 문서가 너무 잘게 쪼개져서 문맥을 잃을 수 있습니다. 또한 문서의 형식에 맞게 separators를 커스터마이징해야 합니다.
마크다운이라면 "###", "##", "#" 같은 헤더를 추가하고, 코드라면 함수나 클래스 경계를 추가하세요. 김개발 씨는 Recursive Chunking으로 전체 시스템을 다시 구현했습니다.
이번에는 청크들이 자연스러웠습니다. 각 청크가 완전한 개념 단위를 포함했고, 검색 품질도 크게 향상되었습니다.
"테스트 결과 98% 정확도가 나왔어요!" 김개발 씨가 신난 목소리로 보고합니다. 박시니어 씨가 만족스럽게 고개를 끄덕입니다.
"좋아. 이제 제대로 된 RAG 시스템이 되었네." Recursive Chunking은 실전에서 가장 많이 사용되는 청킹 전략입니다.
문서 구조를 존중하면서도 적절한 크기를 유지하므로, 대부분의 경우 최선의 선택입니다.
실전 팁
💡 - separators는 문서 형식에 맞게 커스터마이징하세요 (마크다운, 코드, 일반 텍스트 등)
- 큰 단위를 먼저, 작은 단위를 나중에 배치하는 순서를 지키세요
- chunk_size는 여전히 중요합니다. 보통 500-1000 사이가 적당합니다
5. 실습: 다양한 청킹 전략 실험
김개발 씨는 이론은 충분히 배웠다고 생각했습니다. 이제 직접 실험해볼 차례입니다.
"각 방법의 성능을 정량적으로 비교하고 싶어요." 박시니어 씨가 노트북을 열며 말합니다. "좋아, 실제 데이터로 A/B 테스트를 해보자."
청킹 전략 실험은 여러 방법을 동일한 데이터셋에 적용하고 성능을 비교하는 과정입니다. 마치 요리사가 같은 재료로 여러 조리법을 시도해보는 것처럼, 청크 크기, 오버랩, 분할 방식을 바꿔가며 최적의 조합을 찾습니다.
실험을 통해 이론이 실전에서 어떻게 작동하는지 확인할 수 있습니다.
다음 코드를 살펴봅시다.
from langchain.text_splitter import (
CharacterTextSplitter,
RecursiveCharacterTextSplitter,
TokenTextSplitter
)
# 테스트할 청킹 전략들
strategies = {
"고정크기_500": CharacterTextSplitter(chunk_size=500, chunk_overlap=50),
"고정크기_1000": CharacterTextSplitter(chunk_size=1000, chunk_overlap=100),
"재귀적_500": RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50,
separators=["\n\n", "\n", " ", ""]
),
"토큰기반_128": TokenTextSplitter(chunk_size=128, chunk_overlap=16)
}
# 각 전략 실험
results = {}
for name, splitter in strategies.items():
chunks = splitter.split_text(sample_document)
results[name] = {
"청크수": len(chunks),
"평균길이": sum(len(c) for c in chunks) / len(chunks),
"최소길이": min(len(c) for c in chunks),
"최대길이": max(len(c) for c in chunks)
}
print(f"{name}: {results[name]}")
김개발 씨가 회의실에 화이트보드를 가득 채웠습니다. 왼쪽에는 청킹 전략들이, 오른쪽에는 평가 지표들이 적혀 있습니다.
"이제 과학적으로 접근할 때가 됐어요." 박시니어 씨가 커피를 한 모금 마시며 말합니다. "좋아.
그런데 뭘 측정할 건데?" 김개발 씨가 준비한 노트를 펼칩니다. "청크 개수, 평균 길이, 길이 분산, 그리고 실제 검색 정확도를 측정하려고요." 실험 설계가 중요합니다.
단순히 코드를 돌려보는 것이 아니라, 가설을 세우고 검증하는 과정이 필요합니다. "청크 크기가 작을수록 검색 정확도가 높을 것이다"라는 가설을 세웠다면, 이를 증명하거나 반증할 데이터를 수집해야 합니다.
우선 정량적 지표를 정의해야 합니다. 청크 개수는 스토리지 비용과 검색 속도에 영향을 줍니다.
너무 많으면 느리고, 너무 적으면 정확도가 떨어질 수 있습니다. 평균 길이와 분산은 일관성을 나타냅니다.
길이가 들쭉날쭉하면 어떤 쿼리는 충분한 정보를 얻지만 어떤 쿼리는 부족한 정보만 받을 수 있습니다. 가장 중요한 것은 실제 검색 성능입니다.
이론적으로 아무리 좋아도 실제 사용자 쿼리에 정확한 답을 주지 못하면 소용없습니다. 따라서 테스트 쿼리 세트를 준비하고, 각 청킹 전략으로 검색했을 때 정답률을 측정해야 합니다.
김개발 씨가 화면을 공유합니다. "여기 100개의 테스트 쿼리가 있어요.
각 쿼리마다 정답을 미리 라벨링해뒀습니다." 박시니어 씨가 감탄합니다. "제대로 준비했네.
이러면 객관적인 비교가 가능하겠어." 실제 실험을 진행해봅시다. 위의 코드는 4가지 전략을 비교합니다.
고정 크기 500자, 고정 크기 1000자, 재귀적 500자, 토큰 기반 128토큰입니다. 각각 다른 철학을 가지고 있습니다.
고정 크기 500자는 빠르고 예측 가능하지만 문맥을 무시합니다. 1000자는 더 많은 문맥을 담지만 관련 없는 정보도 포함될 수 있습니다.
재귀적 500자는 문서 구조를 존중하므로 품질이 높지만 처리 시간이 더 걸립니다. 토큰 기반은 임베딩 모델과 직접 호환되어 정확하지만, 토큰 카운팅 비용이 발생합니다.
각 전략으로 청크를 생성하고 통계를 수집합니다. 청크 개수를 보면 스토리지 요구사항을 알 수 있습니다.
평균 길이는 각 청크가 담는 정보량을, 최소/최대 길이는 일관성을 나타냅니다. 김개발 씨가 결과를 분석합니다.
"재귀적 방식이 청크 개수는 좀 많지만, 길이가 가장 일정하네요. 그리고 실제 검색 테스트에서도 가장 높은 정확도를 보였어요." 하지만 놀라운 발견도 있었습니다.
"특정 유형의 문서에서는 고정 크기 1000자가 더 좋았어요. 코드 예제가 많은 문서인데, 예제 전체를 하나의 청크에 담는 게 유리했던 것 같아요." 박시니어 씨가 고개를 끄덕입니다.
"바로 그거야. 은탄환은 없어.
문서 타입에 따라 최적 전략이 다르지." 주의할 점이 있습니다. 실험은 대표성 있는 데이터로 해야 합니다.
한 가지 문서로만 테스트하면 편향된 결과가 나올 수 있습니다. 다양한 타입의 문서를 포함하세요.
또한 실제 사용 패턴을 반영해야 합니다. 사용자들이 실제로 어떤 질문을 하는지 로그를 분석하고, 그에 맞는 테스트 쿼리를 만드세요.
마지막으로 비용도 고려하세요. 토큰 기반 청킹은 정확하지만 API 호출 비용이 발생합니다.
재귀적 방식은 처리 시간이 더 걸립니다. 성능과 비용의 균형을 찾아야 합니다.
김개발 씨는 실험 결과를 깔끔한 표로 정리했습니다. "팀장님께 보고드릴 때 이 데이터를 보여드리면 설득력이 있을 것 같아요." 박시니어 씨가 엄지를 치켜세웁니다.
"완벽해. 감으로 선택하는 게 아니라 데이터로 증명하는 거지." 청킹 전략을 선택할 때는 반드시 실험을 거치세요.
여러분의 문서, 여러분의 사용자, 여러분의 시스템에 가장 잘 맞는 방법을 찾아야 합니다.
실전 팁
💡 - 최소 3가지 이상의 전략을 비교하세요
- 정량적 지표(청크수, 길이)와 정성적 지표(검색 정확도)를 모두 측정하세요
- 다양한 문서 타입으로 테스트해서 편향을 방지하세요
6. 실습: 최적 Chunk Size 찾기
실험을 통해 재귀적 청킹이 가장 좋다는 걸 알았습니다. 하지만 chunk_size를 얼마로 설정해야 할까요?
김개발 씨가 200부터 2000까지 여러 값을 시도해봤지만, 각각 장단점이 있었습니다. "과학적으로 최적값을 찾는 방법이 있을까요?" 박시니어 씨가 그래프를 그리며 설명을 시작합니다.
최적 Chunk Size 찾기는 체계적인 실험을 통해 여러분의 시스템에 가장 적합한 청크 크기를 결정하는 과정입니다. 마치 카메라 초점을 맞출 때 여러 거리를 시도해보는 것처럼, 다양한 크기를 테스트하고 성능 지표를 측정하여 최적점을 찾습니다.
이것은 한 번 하고 끝나는 작업이 아니라, 시스템이 진화하면서 지속적으로 조정해야 하는 과정입니다.
다음 코드를 살펴봅시다.
import matplotlib.pyplot as plt
from typing import Dict, List
def find_optimal_chunk_size(
document: str,
sizes: List[int],
test_queries: List[str]
) -> Dict[int, float]:
"""다양한 청크 크기로 실험하고 최적값을 찾는 함수"""
results = {}
for size in sizes:
# 청킹 수행
splitter = RecursiveCharacterTextSplitter(
chunk_size=size,
chunk_overlap=int(size * 0.1) # 10% 오버랩
)
chunks = splitter.split_text(document)
# 벡터화 및 검색 성능 측정 (간소화)
accuracy = evaluate_retrieval(chunks, test_queries)
results[size] = accuracy
print(f"Size {size}: {len(chunks)}개 청크, 정확도 {accuracy:.2%}")
# 최적값 찾기
optimal_size = max(results, key=results.get)
print(f"\n최적 청크 크기: {optimal_size} (정확도: {results[optimal_size]:.2%})")
return results
# 실험 실행: 200부터 1500까지 100 간격으로
sizes_to_test = range(200, 1501, 100)
results = find_optimal_chunk_size(doc, sizes_to_test, test_queries)
김개발 씨가 엑셀 시트를 가득 채워가며 데이터를 정리하고 있었습니다. chunk_size를 200, 300, 400...
이런 식으로 바꿔가며 각각 검색 정확도를 측정했습니다. 3일 동안 20가지 설정을 테스트했습니다.
박시니어 씨가 지나가다가 화면을 봅니다. "수동으로 하나씩 해?
자동화해야지." 김개발 씨가 한숨을 쉽니다. "어떻게 자동화하죠?
매번 설정 바꾸고, 청킹하고, 벡터화하고, 테스트 쿼리 돌리고... 단계가 너무 많아요." 자동화된 실험 파이프라인이 필요합니다.
chunk_size를 파라미터로 받아서, 전체 프로세스를 자동으로 실행하고, 결과를 수집하는 함수를 만들어야 합니다. 이렇게 하면 하룻밤 사이에 수십 가지 설정을 테스트할 수 있습니다.
실험 범위를 정해야 합니다. 너무 작은 값(100자 미만)은 문맥이 부족하고, 너무 큰 값(3000자 이상)은 관련 없는 정보가 섞입니다.
대부분의 경우 200자에서 1500자 사이에서 최적값을 찾을 수 있습니다. 간격도 중요합니다.
처음에는 큰 간격(100자 단위)으로 대략적인 범위를 찾고, 그 주변을 작은 간격(50자 또는 25자 단위)으로 세밀하게 탐색하는 2단계 접근이 효율적입니다. 김개발 씨가 코드를 작성하기 시작합니다.
"먼저 200부터 1500까지 100 간격으로 테스트하고, 최고 성능이 나온 지점 주변을 다시 세밀하게 탐색하면 되겠네요." 박시니어 씨가 고개를 끄덕입니다. "맞아.
그리고 여러 지표를 함께 봐야 해. 정확도만 볼 게 아니라 속도, 비용도 고려해야지." 실제로 측정해야 할 지표들이 있습니다.
검색 정확도는 가장 중요한 지표입니다. 테스트 쿼리에 대해 올바른 정보를 찾아주는 비율을 측정합니다.
보통 Recall@K나 MRR(Mean Reciprocal Rank) 같은 메트릭을 사용합니다. 검색 속도도 중요합니다.
청크가 많을수록 검색이 느려집니다. chunk_size가 작으면 청크 개수가 많아져서 벡터 검색 시간이 증가합니다.
스토리지 비용도 고려해야 합니다. 벡터 DB는 임베딩을 저장하는데, 청크가 많으면 그만큼 저장 공간과 비용이 증가합니다.
위의 코드를 살펴보겠습니다. find_optimal_chunk_size 함수는 여러 크기를 순회하며 각각 테스트합니다.
chunk_overlap은 chunk_size의 10%로 자동 설정됩니다. 이 비율은 경험적으로 좋은 값입니다.
각 크기마다 청킹을 수행하고, evaluate_retrieval 함수로 성능을 측정합니다. 이 함수는 실제로는 벡터화, 검색, 정확도 계산을 모두 수행해야 합니다.
결과를 딕셔너리에 저장하고, 마지막에 max 함수로 최고 성능을 낸 크기를 찾습니다. 이것이 여러분의 시스템에 최적화된 chunk_size입니다.
김개발 씨가 실험을 돌렸습니다. 결과가 흥미로웠습니다.
"500자에서 94% 정확도가 나왔는데, 600자에서 96%로 올라갔어요. 하지만 700자에서는 다시 93%로 떨어졌어요." 그래프를 그려보니 600자 부근에서 피크를 이룬 후 다시 하락하는 패턴이 보였습니다.
박시니어 씨가 설명합니다. "전형적인 sweet spot이야.
너무 작으면 문맥 부족, 너무 크면 노이즈 증가. 딱 중간 지점이 최적이지." 주의할 점이 있습니다.
최적값은 문서 타입마다 다릅니다. 기술 문서는 500-800자가 좋지만, 대화 로그는 200-300자가 적합할 수 있습니다.
Q&A 형식이라면 각 Q&A 쌍을 하나의 청크로 만드는 것이 좋을 수도 있습니다. 또한 임베딩 모델에 따라 최적값이 달라집니다.
어떤 모델은 긴 텍스트를 잘 처리하고, 어떤 모델은 짧은 텍스트에 특화되어 있습니다. 모델을 바꾸면 다시 실험해야 합니다.
마지막으로 지속적인 모니터링이 필요합니다. 문서가 추가되고 사용 패턴이 바뀌면서 최적값도 변할 수 있습니다.
분기별로 한 번씩 재검증하는 것이 좋습니다. 김개발 씨는 최종적으로 chunk_size=600, chunk_overlap=60으로 결정했습니다.
"이 설정으로 프로덕션에 배포하고, 3개월 후에 다시 검토하겠습니다." 박시니어 씨가 만족스럽게 웃습니다. "완벽해.
이제 제대로 된 엔지니어가 됐네. 감이 아니라 데이터로 결정하는 거야." 최적 chunk_size는 여러분이 직접 찾아야 합니다.
다른 사람의 설정을 무작정 따라하지 말고, 여러분의 시스템으로 실험하세요. 그것이 최고 성능을 얻는 유일한 방법입니다.
실전 팁
💡 - 처음에는 넓은 범위를 큰 간격으로, 그다음 좁은 범위를 작은 간격으로 탐색하세요
- 정확도, 속도, 비용을 모두 고려한 복합 지표를 사용하세요
- 문서 타입이 다양하다면 타입별로 다른 chunk_size를 사용하는 것도 고려하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
ReAct 패턴 마스터 완벽 가이드
LLM이 생각하고 행동하는 ReAct 패턴을 처음부터 끝까지 배웁니다. Thought-Action-Observation 루프로 똑똑한 에이전트를 만들고, 실전 예제로 웹 검색과 계산을 결합한 강력한 AI 시스템을 구축합니다.
AI 에이전트의 모든 것 - 개념부터 실습까지
AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.
프로덕션 RAG 시스템 완벽 가이드
검색 증강 생성(RAG) 시스템을 실제 서비스로 배포하기 위한 확장성, 비용 최적화, 모니터링 전략을 다룹니다. AWS/GCP 배포 실습과 대시보드 구축까지 프로덕션 환경의 모든 것을 담았습니다.
RAG 캐싱 전략 완벽 가이드
RAG 시스템의 성능을 획기적으로 개선하는 캐싱 전략을 배웁니다. 쿼리 캐싱부터 임베딩 캐싱, Redis 통합까지 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.