
결론부터 말씀드리면, RAG 청킹 전략은 문서 성격에 따라 답이 갈립니다. 정형화된 매뉴얼이나 로그성 텍스트는 고정 길이 분할로도 충분하지만, 논리 흐름이 중요한 기술 문서나 법률·의료 텍스트는 문장 경계나 시맨틱 분할을 써야 검색 품질이 눈에 띄게 달라집니다. 이 글에서는 세 방식이 실제로 어떤 조건에서 갈리는지, 그리고 각각의 트레이드오프를 코드와 함께 짚어보겠습니다.
RAG 파이프라인을 운영해본 분이라면 임베딩 모델이나 벡터DB를 바꿔도 검색 결과가 크게 안 나아지는 경험을 하셨을 겁니다. 원인은 대부분 청킹 단계에 있습니다. 청크 경계가 문장이나 문단 중간에서 잘리면, 임베딩 자체가 의미를 온전히 담지 못하기 때문입니다.
고정 길이 청킹, 언제 검색 품질을 떨어뜨릴까
고정 길이 청킹은 문자 수나 토큰 수를 기준으로 기계적으로 자르는 방식입니다. 구현이 가장 단순하고 처리 속도가 빨라서 초기 프로토타입 단계에서 많이 씁니다. 문제는 문장이나 표, 코드 블록 중간에서 경계가 끊길 확률이 높다는 점입니다.
예를 들어 500자 단위로 자르면 “이 함수는 널 값을 반환하지 않습니다”라는 문장이 앞 청크와 뒤 청크로 쪼개질 수 있습니다. 이 상태로 임베딩하면 두 청크 모두 원래 의미를 온전히 담지 못하고, 쿼리와의 코사인 유사도 계산에서 관련 청크가 순위 밖으로 밀리는 경우가 생깁니다.
다만 고정 길이 방식이 불리하기만 한 것은 아닙니다. 로그 파일, FAQ 목록처럼 문장 하나하나가 이미 독립적인 의미 단위인 텍스트에서는 굳이 문장 경계를 계산할 필요가 없어서 속도 이점이 그대로 살아납니다.
문장 경계 분할이 고정 길이보다 나은 이유

문장 경계 분할은 마침표, 물음표 같은 구두점이나 문단 구분을 기준으로 자르되, 목표 길이를 넘지 않는 선에서 문장 단위를 최대한 보존합니다. LangChain의 RecursiveCharacterTextSplitter가 대표적인데, 줄바꿈, 문장, 단어 순으로 분리 기준을 재귀적으로 적용해 의미 단위가 잘리는 빈도를 줄입니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_text(long_document)
print(len(chunks), "개 청크 생성")
print(chunks[0][:100])
이 코드는 문단(\n\n)을 먼저 시도하고, 없으면 줄바꿈, 그다음 문장 단위로 내려가면서 500자에 가장 가까운 지점에서 자릅니다. overlap을 50자 정도 줘서 앞뒤 청크가 문맥을 조금씩 공유하게 만드는데, 이 값이 너무 크면 저장 용량과 임베딩 비용이 늘고, 너무 작으면 경계 부근 정보 손실이 다시 생깁니다.
한국어 문서에서는 영어 대비 문장 경계 인식이 까다로운 편입니다. 마침표가 소수점이나 약어에도 쓰이고, 존댓말 어미로 문장이 끝나는 경우가 많아서 구두점 기반 분리기가 오작동하는 사례가 종종 나옵니다. 이럴 땐 KSS(Korean Sentence Splitter) 같은 한국어 전용 문장 분리 라이브러리를 전처리 단계에 끼워 넣는 방식이 실무에서 더 안정적입니다.
시맨틱 분할은 정말 검색 정확도를 높여줄까
시맨틱 분할은 문장 단위로 먼저 임베딩을 계산한 뒤, 인접 문장 간 코사인 유사도가 일정 임곗값 아래로 떨어지는 지점을 청크 경계로 삼는 방식입니다. langchain_experimental.text_splitter의 SemanticChunker가 이 방식을 구현하고 있으며, breakpoint_threshold_type 파라미터로 percentile, standard_deviation 등 경계 탐지 기준을 선택할 수 있습니다. 자세한 파라미터는 LangChain 공식 문서에서 확인할 수 있습니다.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
chunker = SemanticChunker(
OpenAIEmbeddings(model="text-embedding-3-small"),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=90
)
chunks = chunker.split_text(long_document)
이 방식의 장점은 주제가 바뀌는 지점을 자동으로 찾아준다는 데 있습니다. 예를 들어 한 문서 안에서 “설치 방법” 설명이 끝나고 “트러블슈팅” 설명이 시작되는 지점을 문자 수와 무관하게 잡아냅니다. 실제로 여러 사례에서 시맨틱 분할이 고정 길이 대비 검색 재현율(recall)을 끌어올린다고 보고되지만, 문서 종류와 임곗값 설정에 따라 편차가 커서 일괄적인 수치로 단정하기는 어렵습니다.
트레이드오프도 분명합니다. 모든 문장을 미리 임베딩해야 하므로 처리 비용과 시간이 고정 길이 대비 몇 배로 늘어나고, 문서가 수만 건 단위라면 이 비용이 무시하기 어려운 수준이 됩니다. 임곗값을 잘못 잡으면 오히려 청크가 지나치게 잘게 쪼개지거나, 반대로 하나의 거대한 청크로 뭉쳐버리는 경우도 생깁니다.

세 전략을 정리하면 언제 무엇을 골라야 할까
세 방식의 차이를 조건별로 정리하면 다음과 같습니다.
| 구분 | 고정 길이 | 문장 경계 | 시맨틱 분할 |
|---|---|---|---|
| 처리 속도 | 가장 빠름 | 중간 | 가장 느림 (임베딩 선행 필요) |
| 구현 난이도 | 낮음 | 낮음~중간 | 중간~높음 |
| 문맥 보존 | 낮음 | 중간~높음 | 높음 (주제 경계까지 반영) |
| 적합한 문서 | 로그, FAQ, 정형 데이터 | 일반 기술 문서, 블로그 | 논문, 법률·정책 문서, 긴 리포트 |
| 추가 비용 | 없음 | 없음 | 임베딩 API 호출 비용 |
이 표는 절대적 우열이 아니라 조건부 선택 기준으로 봐야 합니다. 예를 들어 청크 크기를 300자로 작게 잡으면 검색 정밀도(precision)는 오르는 경향이 있지만, 문맥이 부족해 LLM이 답변을 생성할 때 근거가 빈약해지는 문제가 같이 발생합니다. 반대로 1,000자 이상으로 크게 잡으면 문맥은 풍부해지지만 관련 없는 내용까지 섞여 들어가 재현율이 떨어질 수 있습니다.
청크를 잘게 쪼갤수록 검색이 정확해지나요?
꼭 그렇지는 않습니다. 청크가 너무 작으면 임베딩이 담을 수 있는 정보량 자체가 줄어들어서, 쿼리와 표면적으로 단어가 겹치는 청크만 상위에 노출되고 정작 맥락상 필요한 청크는 밀려나는 역효과가 생깁니다. 임베딩이라는 개념 자체는 텍스트를 고차원 벡터 공간에 사상해 의미적 유사도를 계산하는 기법인데, 관련 배경은 위키백과 자연어 처리 문서에서 개념적으로 확인할 수 있습니다.
실무에서는 청크 크기 하나를 정답처럼 고정하기보다, 청크 크기와 overlap을 2~3가지 조합으로 바꿔가며 실제 쿼리 세트로 검색 재현율을 비교하는 과정이 필요합니다. 이 튜닝 과정 없이 커뮤니티에서 본 숫자를 그대로 옮기면, 정작 자신의 문서 특성과 안 맞는 경우가 흔합니다.
결국 청킹 전략을 고르는 기준은 문서의 구조적 특성과 운영 예산입니다. 소규모 문서 집합이고 정확도가 중요하다면 시맨틱 분할부터 테스트해보시고, 문서량이 많고 비용이 부담이라면 문장 경계 분할에 overlap을 적절히 조합하는 방식으로 시작하시는 편이 현실적입니다. 다음 단계로는 지금 쓰고 계신 문서 샘플 10~20개를 골라 세 방식으로 각각 청킹한 뒤, 실제 검색 쿼리 몇 개를 넣어 상위 청크의 관련성을 직접 비교해보시길 권해드립니다.
