Chunking이란 무엇이고 왜 중요한가?
RAG 시스템을 구축할 때 가장 먼저 맞닥뜨리는 고민은 문서를 어떻게 잘라서 인덱싱 해야 하는가?에 대한 부분입니다.
우리가 앞서 살펴봤던 RAG의 기본 흐름을 떠올려 볼까요?
먼저 외부 문서를 벡터로 변환해 인덱스에 저장하고, 질문이 들어오면 유사한 벡터를 검색해 LLM에 전달합니다.
그런데 문서 전체를 통째로 하나의 벡터로 만들면 어떻게 될까요? 100페이지짜리 기술 매뉴얼을 벡터 하나로 압축하면, 그 안에 담긴 수백 가지 정보가 뭉개져버립니다.
만약 '3번 포트 설정 방법'을 물어봤을 때 매뉴얼 전체가 검색되면 LLM은 어디서부터 답해야 할지 알 수 없습니다.
그래서 문서를 적당한 크기의 조각으로 나누는 과정인 Chunking이 필요합니다.
RAG 시스템에서 검색 품질을 좌우하는 핵심 요소 중 하나는 청크(chunk) 크기입니다. 실제로 RAG 논문에서도 이 설계가 중요한 결정으로 등장합니다.
Facebook AI Research 팀은 Wikipedia 전체를 100단어 단위로 분할해 약 2,100만 개의 청크를 생성했습니다.
단순해 보이는 설정같아 보이지만, 실제로는 검색 정밀도에 큰 영향을 미칩니다. 실무에서도 청크 크기를 조금만 잘못 설정해도 검색 결과가 눈에 띄게 달라지는 것을 쉽게 확인할 수 있습니다.
- 청크가 너무 크면: 검색 정밀도가 떨어집니다. 하나의 청크에 너무 많은 정보가 담겨 있어서, 정작 필요한 내용이 노이즈에 묻혀버립니다.
- 청크가 너무 작으면: 문맥이 끊깁니다. '이 약의 부작용은 다음과 같습니다.'라는 문장과 실제 부작용 목록이 다른 청크로 분리되면, 검색된 결과만으로는 의미를 파악하기 어렵습니다.
- 청크 경계가 잘못 잡히면: 문장 중간에서 잘리거나, 논리적으로 연결된 내용이 분리되어 LLM이 엉뚱한 답을 만들어냅니다.
결국 Chunking은 문서를 적당히 자르는 전처리 작업이 아니라 RAG 성능을 좌우하는 핵심 설계 결정입니다.
LlamaIndex에서는 이 청킹을 담당하는 컴포넌트를 Node Parser라고 부릅니다. 문서(Document)를 입력받아 노드(Node) 단위로 잘라주는 역할입니다.
💡 Node란?
LlamaIndex에서 Document를 청킹한 결과물입니다. 원본 문서의 텍스트 조각과 함께, 원본 문서의 메타데이터(파일명, 페이지 번호 등)를 그대로 상속받습니다. 예를 들어 report.pdf의 3페이지 내용을 담은 노드는 파일명과 페이지 번호 정보를 메타데이터로 갖게 됩니다. 이 노드들이 임베딩 모델을 통해 벡터로 변환되어 인덱스에 저장됩니다.
그렇다면 Node Parser는 어떤 방식으로 문서를 잘라낼까요? LlamaIndex는 목적에 따라 선택할 수 있는 여러 전략을 제공합니다. 각각 어떻게 다른지 하나씩 살펴보겠습니다.
첫 번째 전략 : 고정 크기로 자르기(TokenTextSplitter)
가장 단순하고 직관적인 방식입니다. 텍스트를 토큰 수를 기준으로 일정한 크기로 잘라냅니다. 여기서 토큰은 단어와 비슷한 개념으로 영어 기준으로 대략 1단어가 1~2토큰입니다.
from llama_index.core.node_parser import TokenTextSplitter
splitter = TokenTextSplitter(
chunk_size=512, # 청크 하나의 최대 토큰 수
chunk_overlap=50 # 인접 청크 간 겹치는 토큰 수
)
nodes = splitter.get_nodes_from_documents(documents)
여기서 주목할 파라미터는 chunk_overlap입니다. 이름 그대로 인접한 두 청크가 겹치는 구간을 설정합니다.
예를 들어 chunk_overlap=50이라면, 앞 청크의 마지막 50토큰이 다음 청크의 앞부분에도 반복해서 포함됩니다.
그럼 왜 겹치게 할까요? 문장이 청크 경계에서 잘리는 경우를 대비해서입니다. '부작용으로는 구역질, 두통, 그리고...'라는 문장이 청크 중간에서 잘렸다면 다음 청크에 앞 내용이 일부 반복되어야 '그리고....' 뒤에 이어지는 내용이 문맥과 함께 검색될 수 있습니다. overlap이 없으면 각 청크가 독립적인 섬처럼 되어버려 경계 근처의 내용은 항상 불완전하게 검색될 위험이 있습니다.
- 장점: 구현이 단순하고 동작을 예측하기 쉽습니다. 청크 크기가 고정되어 있어 LLM의 컨텍스트 윈도우를 초과할 걱정이 없고, 처리 속도도 빠릅니다.
- 단점: 의미 단위를 전혀 고려하지 않습니다. '이 약의 부작용은...' 같은 문장이 토큰 한도에 걸려 두 청크로 쪼개지면 두 청크 모두 불완전한 정보를 담게 됩니다. overlap으로 어느 정도 완화할 수 있지만 근본적인 해결책은 아닙니다.
이 전략은 텍스트 구조가 균일하고 내용이 고르게 분포된 문서, 또는 빠른 프로토타입이 필요할 때 시작점으로 쓰기 좋습니다.
두 번째 전략 : 문장 경계를 지키며 자르기(SentenceSplitter)
LlamaIndex의 기본 Node Parser입니다. 고정 크기 방식의 가장 큰 문제 즉 문장이 중간에 잘리는 문제를 해결합니다.
텍스트를 먼저 문장 단위로 분리한 뒤, 지정한 청크 크기에 맞게 문장들을 묶어나갑니다.
from llama_index.core.node_parser import SentenceSplitter
splitter = SentenceSplitter(
chunk_size=1024,
chunk_overlap=20
)
nodes = splitter.get_nodes_from_documents(documents)
동작 방식을 구체적으로 보면 먼저 마침표, 느낌표, 물음표 같은 문장 부호를 기준으로 텍스트 전체를 문장 단위로 쪼갭니다.
그런 다음 각 문장을 순서대로 청크에 쌓아가다가 다음 문장을 추가하면 chunk_size를 초과할 것 같으면 거기서 청크를 끊고 새 청크를 시작합니다. 이 덕분에 '이 약의 부작용은 구역질입니다.'라는 문장은 절대 중간에 잘리지 않습니다.
TokenTextSplitter와 파라미터 구성은 동일합니다. chunk_overlap으로 인접 청크가 겹치는 구간을 설정해 경계 근처 문맥이 끊기지 않도록 합니다.
- 장점: TokenTextSplitter보다 자연스럽고, 구현 복잡도는 비슷합니다. LlamaIndex의 기본값이라 별도 설정 없이도 사용되며, 대부분의 상황에서 무난하게 동작합니다.
- 단점: 문장이 잘리지 않는다고 해서 의미의 흐름까지 보장되는 건 아닙니다. 예를 들어 앞 문단 전체가 배경 설명이고, 다음 문단 첫 문장이 '따라서 이 방법은 권장되지 않습니다.'일 때, 배경 설명 없이 결론 문장만 검색되었을 때 LLM은 맥락을 잃고 엉뚱한 해석을 할 수 있습니다.
이 전략은 특별한 이유가 없다면 여기서 시작하는 게 좋습니다. 빠르게 파이프라인을 구성하고 결과를 확인해본 뒤, 품질이 부족하다면 이후 소개할 전략으로 넘어가는 흐름을 추천합니다.
세 번째 전략 : 정밀하게 검색하고, 풍부하게 전달하기( SentenceWindowNodeParser)
SentenceSplitter의 딜레마를 떠올려봅시다. 청크를 작게 만들면 검색은 정밀해지지만 문맥이 부족하고, 크게 만들면 문맥은 풍부하지만 검색 정밀도가 떨어집니다. SentenceWindowNodeParser는 이 두 가지를 동시에 해결하려는 방식입니다.
아이디어는 아래와 같습니다.
검색할 때는 문장 하나 단위로 정밀하게 찾고, LLM에 전달할 때는 그 문장의 앞뒤 문장들까지 함께 넘겨준다.
from llama_index.core.node_parser import SentenceWindowNodeParser
node_parser = SentenceWindowNodeParser.from_defaults(
window_size=3 # 앞뒤 각 3문장을 window로 저장
)
nodes = node_parser.get_nodes_from_documents(documents)
동작 방식을 단계별로 같이 살펴보시죠!
1단계 - 인덱싱
문서를 개별 문장 단위로 잘게 자릅니다. 이 때 각 노드(문장)에는 그 문장 자체뿐 아니라 앞 뒤 window_size개 문장들이 메타데이터로 함께 저장됩니다. window_size=3이라면 앞 3문장 + 현재 문장 + 뒤 3문장, 총 7문장이 하나의 노드에 담기는 셈이되죠.
단, 임베딩(벡터 변환)은 현재 문장만 대상으로 합니다.
2단계 - 검색
질문이 들어오면 문장 단위 임베딩으로 가장 관련성 높은 문장을 찾습니다. 청크가 작을수록 특정 정보를 정확히 찾아낼 확률이 높아집니다.
3단계 - LLM 전달
검색된 문장을 그대로 LLM에 넘기지 않습니다. MetadataReplacementNodePostProcessor가 해당 문장을 메타데이터에 저장된 window 문장 전체로 교체합니다. LLM은 문장 하나가 아니라 앞뒤 맥락이 포함된 풍부한 텍스트를 받게 됩니다.
예를 들어 '이 성분은 간 독성을 유발할 수 있습니다.'라는 문장이 검색되었다면, LLM에는 앞뒤 3문장씩 포함된 7문장짜리 구간이 전달됩니다. '어떤 약에서', '어떤 용량에서', '어떤 조건에서' 같은 맥락이 함께 오기 때문에 훨씬 정확한 답변이 가능해집니다.
- 장점: 검색 정밀도와 LLM 입력 품질을 동시에 확보할 수 있습니다. 기존 방식에서 둘 중 하나를 포기해야 했던 트레이드오프를 구조적으로 해결합니다.
- 단점: 문장마다 window 문장들을 메타데이터로 저장하기 때문에 저장 공간이 늘어납니다. 또한 MetadataReplacementNodePostProcessor를 retrieval 파이프라인에 별도로 연결해야 하므로 구현이 SentenceSplitter보다 복잡합니다.
이 전략은 긴 문서에서 특정 사실을 정확히 찾아야하고, 동시에 답변 품질도 높아야 하는 경우에 적합합니다.
의료, 법률, 기술 문서처럼 맥락이 중요한 도메인에서 특히 효과적이죠.
전략 비교 한눈에 보기
| 전략 | LlamaIndex 클래스 | 핵심 아이디어 | 장점 | 단점 |
| Fixed-size | TokenTextSplitter | 고정 토큰 수로 분할 | 단순, 빠름 | 문장 중간에서 잘릴 수 있음 |
| Sentence | SentenceSplitter | 문장 경계를 존중해 분할 | 자연스러움, 기본값 | 문맥 흐름 손실 가능 |
| Sentence Window | SentenceWindowNodeParser | 문장 단위 검색 + 주변 문맥 LLM 전달 | 검색 정밀도 + 문맥 풍부 | 저장 공간 증가 |
| Parent-Child | HierarchicalNodeParser | 계층적 다중 크기 분할 | 정밀도 + 문맥 동시 확보 | 구현 복잡도 높음 |
의미 단위로 자르기(Semantic Chunking)
위 네 가지와 별도로, 최근에는 Semantic Chunking도 주목받고 있습니다. 고정된 크기나 문장 경계가 아니라 임베딩 유사도를 기준으로 의미적으로 관련된 문장들을 묶어 청크를 만드는 방식입니다.
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.openai import OpenAIEmbedding
splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=OpenAIEmbedding()
)
동작 방식은 이렇습니다.
먼저 문서를 문장 단위로 분리합니다. 그런 다음 인접한 문장들을 각각 임베딩 벡터로 변환하고, 이웃한 문장들 사이의 코사인 유사도를 계산합니다. 유사도가 높으면 같은 주제를 다루고 있다고 판단해 같은 청크로 묶고, 유사도가 급격히 떨어지는 지점을 주제 전환으로 보고 청크를 끊습니다.
breakpoint_percentile_threshold=95는 유사도 하락이 전체 문장 쌍 중 하위 5%에 해당할 만큼 급격할 때 청크를 자른다.는 의미입니다. 값을 높일수록 청크가 더 잘게 나뉘고, 낮출수록 더 큰 단위로 묶입니다.
예를 들어 기술 문서에서 설치 방법을 설명하는 문단과 트러블슈팅을 설명하는 문단이 있다면 두 구간의 임베딩이 의미적으로 멀기 때문에 자연스럽게 다른 청크로 분리됩니다. 반면 설치 방법 내의 여러 단계들은 의미적으로 가까우므로 하나의 청크로 묶입니다. 문서 작성자가 의도한 논리적 단위가 그대로 청크로 이어지는 셈입니다.
- 장점: 의미 단위가 자연스럽게 보존되어 검색 품질이 높아집니다. 특히 하나의 문서 안에서 주제가 자주 전환되는 경우에는 다른 전략보다 훨씬 깔끔하게 청크가 나뉩니다.
단점: 청크 크기가 일정하지 않아 LLM 컨텍스트 윈도우를 초과할 위험이 있습니다. 무엇보다 인덱싱 단계에서 모든 문장을 임베딩해야 하기 때문에, 문서가 많을수록 비용과 시간이 크게 늘어납니다. 다른 전략들은 인덱싱 시 임베딩이 한 번만 필요하지만 Semantic Chunking은 청킹 자체를 위해 임베딩을 한 번 더 호출합니다.
이 방식은 주제 전환이 잦은 긴 문서, 예를 들어 여러 섹션으로 구성된 보고서나 논문에서 효과적입니다. 다만 비용과 처리 시간을 감수할 수 있는 상황인지 먼저 확인하는 것이 좋습니다.
어떤 전략을 선택해야 할까?
정답은 없지만 아래 기준으로 시작해볼 수 있습니다.
빠르게 프로토타입을 만들어야 한다면 SentenceSplitter(기본값)로 시작하세요. 답변 품질을 높여야 한다면 SentenceWindowNodeParser를, 계층 구조가 있는 전문 문서라면 HierarchicalNodeParser를 고려해보세요. 의미 단위 보존이 가장 중요하다면 SemanticSplitterNodeParser가 있지만, 비용과 복잡도를 감수해야 합니다.
결국 chunking 전략은 문서의 성격, 필요한 정밀도, 허용 가능한 비용에 따라 달라집니다. 실제로는 두세 가지 전략을 실험해보고 평가 지표로 비교하는 것이 가장 확실합니다.
'AI' 카테고리의 다른 글
| [바미] 구글도 키워드 검색을 버리지 않은 이유 (하이브리드 검색과 RAG-Fusion) (0) | 2026.03.29 |
|---|---|
| [바미] 내가 크롤링한 결과물이 쓰레기가 되는 이유 (2) | 2026.03.28 |
| [바미] AI도 구글 검색을 한다면 어떨까요? (0) | 2026.03.26 |
| [바미] 지식 증류(Knowledge Distillation)로 성능은 챙기고, 모델은 줄이기 (0) | 2026.02.22 |
| [바미] 코사인 유사도 한 번에 정리해보기 (0) | 2026.02.20 |