포스트

RAG 청킹 전략 비교: 문서를 길이로 자를까, 의미로 자를까?

RAG 청킹 전략 비교: 문서를 길이로 자를까, 의미로 자를까?

RAG를 정리하면서 검색 파이프라인 전체 구조RAG가 학습이 아니라 검색이라는 점까지는 짚었는데, 그때마다 청킹은 “문서를 검색 가능한 단위로 나누는 과정” 정도로 뭉뚱그리고 넘어갔다. 마침 『AI 에이전트 개발 완벽 입문』의 RAG·청킹 부분을 다시 보다가, 정작 청킹 방식에 따라 검색 결과가 어떻게 달라지는지는 제대로 짚고 넘어간 적이 없다는 걸 깨달았다. 그래서 오늘은 책 내용을 참고 삼아 길이 기반 청킹과 의미 기반 청킹이 실제로 어떻게 다른지, 그리고 서비스에 적용한다면 어떤 기준으로 골라야 하는지를 직접 정리해봤다.

핵심은 하나다. RAG의 검색 품질은 임베딩 모델이나 벡터 데이터베이스뿐 아니라, 문서를 어떤 단위로 나누었는지에도 크게 영향을 받는다.

1. RAG에서 청킹이 필요한 이유

가장 단순한 방법은 문서 하나를 통째로 임베딩하는 것이다. 그런데 실제로 해보면 금방 문제가 드러난다.

문서 하나에 여러 주제가 들어 있으면(예: 하나의 TIL 글 안에 원인 분석, 해결 방법, 트레이드오프 정리가 함께 있는 경우), 이 내용이 전부 하나의 벡터로 뭉뚱그려 저장된다. 벡터 하나가 “이 문서는 대략 이런 내용을 담고 있다”는 평균적인 위치만 나타내다 보니, 사용자가 그중 한 부분(예: “해결 방법이 뭐였지?”)만 정확히 찾고 싶어도 임베딩은 그 부분만 콕 집어 표현하지 못한다. 결과적으로 질문과 관련 없는 부분까지 통째로 검색되거나, 반대로 관련 있는 문서인데도 다른 주제에 의해 벡터가 희석되어 검색 순위에서 밀리는 일이 생긴다.

그렇다고 문서를 무작정 잘게 쪼개는 것도 답은 아니다. 한두 문장 단위로 지나치게 잘게 나누면 검색은 정확한 조각을 찾아오더라도, 그 조각만으로는 질문에 답하는 데 필요한 앞뒤 맥락이 빠져 있는 경우가 많다. “그 이유는”으로 시작하는 문장만 검색되고, 정작 “그것”이 무엇을 가리키는지는 다른 청크에 있는 식이다.

청크 크기에 따른 문제 비교

청크가 너무 작으면 문맥이 여러 조각으로 끊기고, 너무 크면 관련 없는 내용까지 함께 딸려 온다.

정리하면 청킹의 목적은 문서를 단순히 일정한 크기로 잘라내는 작업이 아니라, 검색 가능한 단위로 나누면서 동시에 답변에 필요한 문맥을 최대한 보존하는 과정이다. 이 균형을 어떻게 잡느냐에 따라 접근 방식이 크게 갈린다.

2. 길이 기반 청킹: 정해진 크기로 잘라내기

가장 널리 쓰이는 방식은 LangChain의 RecursiveCharacterTextSplitter로 대표되는 길이 기반 청킹이다.

동작 방식은 생각보다 단순하다. chunk_size로 목표 크기를 정해두고, 이 크기를 넘지 않는 선에서 텍스트를 나눈다. 이때 아무 위치에서나 자르는 게 아니라 구분자를 우선순위대로 시도한다. 문단 구분(\n\n)으로 먼저 나눠보고, 그래도 청크가 너무 크면 줄바꿈(\n)으로, 그다음은 공백으로 내려가며 자른다. 즉 “가능하면 문서의 자연스러운 구조(문단, 줄, 단어)를 유지하면서” 크기를 맞추는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=50,
    chunk_overlap=10,
)

text = """RAG는 문서를 검색해 답변 근거로 활용하는 구조다. 문서를 통째로 임베딩하면 여러 주제가 섞여 검색이 어려워진다.

그래서 문서를 청크 단위로 나눈다. 청크가 너무 작으면 문맥이 끊기고, 너무 크면 주제가 섞인다.

적절한 청크 크기를 잡는 것이 중요하다."""

chunks = splitter.split_text(text)
for i, chunk in enumerate(chunks):
    print(i, chunk)

이 코드를 실제로 돌려보면 아래와 같이 잘린다.

1
2
3
4
5
0: RAG는 문서를 검색해 답변 근거로 활용하는 구조다. 문서를 통째로 임베딩하면 여러 주제가
1: 여러 주제가 섞여 검색이 어려워진다.
2: 그래서 문서를 청크 단위로 나눈다. 청크가 너무 작으면 문맥이 끊기고, 너무 크면 주제가
3: 너무 크면 주제가 섞인다.
4: 적절한 청크 크기를 잡는 것이 중요하다.

첫 문단은 “구조다.”에서 끊기지 못하고 “여러 주제가” 앞에서 잘렸다. chunk_size를 넘기지 않으려다 보니 문장 중간에서 끊어진 것이다. 대신 chunk_overlap으로 지정한 만큼(“여러 주제가”)이 다음 청크 앞에 다시 붙어서, 문맥이 완전히 잘려나가지는 않는다.

이 방식의 장점은 명확하다. 구현이 단순하고, 같은 입력이면 항상 같은 결과가 나오기 때문에 결과를 예측하기 쉽다. 별도의 모델 호출 없이 문자열 처리만으로 동작하니 전처리 비용도 낮다. 그래서 초기 RAG나 MVP를 만들 때 가장 먼저 시도해보기 좋은 방식이다.

한계도 뚜렷하다. 구분자 우선순위만으로는 “의미상 하나로 묶여야 하는 내용”과 “크기 제한”이 항상 맞아떨어지지 않는다. 위 예시처럼 하나의 문장이 두 청크로 쪼개질 수 있고, 반대로 서로 다른 주제를 다루는 문단이 크기 제한 안에 우연히 함께 들어가 하나의 청크에 섞이는 경우도 있다. 결국 이 방식은 글자 수 기준으로는 정확하지만, 의미 단위 기준으로는 정확하다고 보장하지 않는 접근이다.

3. 의미 기반 청킹: 주제가 바뀌는 지점을 찾아 자르기

SemanticChunker는 접근 자체가 다르다. 크기를 먼저 정하고 자르는 게 아니라, 문장 사이의 의미가 얼마나 이어지는지를 보고 자를 위치를 찾는다.

동작 방식은 이렇다. 먼저 문서를 문장 단위로 분리한다. 그다음 각 문장을 임베딩하고, 인접한 문장끼리 임베딩 유사도를 계산한다. 유사도가 완만하게 이어지다가 특정 지점에서 크게 떨어지면(설정한 임계값을 벗어나면), 그 지점을 주제가 전환되는 경계로 판단하고 그곳에서 청크를 나눈다.

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

chunker = SemanticChunker(
    embeddings=embeddings,
    breakpoint_threshold_type="percentile",
)

docs = chunker.create_documents([long_text])
for doc in docs:
    print(doc.page_content)

SemanticChunkerlangchain-experimental 패키지에 포함되어 있다(pip install langchain-experimental langchain-openai). 다만 이 패키지는 현재 유지보수 축소(sunset) 상태로 안내되고 있어서, 실제 도입 전에는 대체 구현이나 최신 상태를 한 번 더 확인하는 게 안전하다. 코드에는 OPENAI_API_KEY 같은 실제 인증 정보를 직접 넣지 않고 환경 변수로 관리한다.

이 방식의 장점은 하나의 의미 단위를 청크 경계 때문에 억지로 쪼개지 않는다는 점이다. 크기 제한이 아니라 의미 흐름을 기준으로 자르기 때문에, 보고서나 논문, 설명이 길게 이어지는 문서처럼 주제 흐름 자체가 중요한 문서에서 유리할 수 있다.

다만 이걸 “더 똑똑한 방식이니 항상 좋다”고 정리하기는 어렵다. 문장마다 임베딩을 계산해야 하므로 문서가 길어질수록 임베딩 호출 횟수와 처리 시간, 비용이 함께 늘어난다. 또한 어떤 임베딩 모델을 쓰는지, 임계값(breakpoint_threshold_type, breakpoint_threshold_amount)을 어떻게 잡는지에 따라 같은 문서도 다르게 잘릴 수 있어서, 길이 기반 청킹보다 결과를 예측하기가 어렵다. 결국 의미 기반 청킹은 검색 품질을 끌어올릴 수 있는 대신, 처리 비용과 예측 가능성을 일부 내주는 트레이드오프에 가깝다.

4. chunk_overlap은 또 다른 청킹 방식이 아니다

chunk_overlap을 청킹 전략의 세 번째 선택지처럼 다루는 설명을 종종 보는데, 정확히는 그렇지 않다. 오버랩은 길이 기반이든 의미 기반이든 상관없이, 청크 경계에서 문맥이 끊기는 문제를 줄이기 위한 보조 설정이다.

오버랩이 없으면 어떤 문장이나 설명이 딱 청크 경계에서 잘려, 그 청크만 봤을 때는 앞의 근거나 뒤의 결론이 빠진 채로 검색될 수 있다. 이걸 줄이기 위해 이전 청크의 끝부분을 다음 청크의 앞부분에 일부 중복시키는 것이 chunk_overlap이다.

문제는 이 값을 무작정 키운다고 좋아지지는 않는다는 점이다. 오버랩이 지나치게 크면 인접한 청크끼리 내용이 상당 부분 겹치게 되고, 그만큼 중복된 내용이 그대로 임베딩되어 벡터 DB에 쌓인다. 검색 결과에도 사실상 같은 내용의 청크가 여러 개 반복해서 올라올 수 있고, 이는 벡터 저장 공간을 불필요하게 늘릴 뿐 아니라 정작 봐야 할 다른 근거의 자리를 차지하는 검색 노이즈로 이어진다.

chunk_sizechunk_overlap에 정답 숫자가 있는 건 아니다. 문서의 구조가 짧은 단락 위주인지 긴 서술형인지, 그리고 실제 사용자 질문이 한 문단 안에서 끝나는지 여러 문단에 걸쳐 있는지를 보고 값을 조정해야 한다. 결국 이 값들은 코드를 짜기 전에 정하는 상수가 아니라, 검색 결과를 보면서 맞춰가는 튜닝 대상에 가깝다.

5. 두 방식 비교

기준길이 기반 청킹의미 기반 청킹
분할 기준글자/토큰 수(chunk_size)인접 문장 간 임베딩 유사도 변화
구현 복잡도낮음상대적으로 높음
처리 비용낮음(문자열 처리만 필요)높음(문장별 임베딩 호출 필요)
결과 예측 가능성높음(같은 입력이면 항상 동일)낮음(모델·임계값에 따라 달라짐)
문맥 보존경계에서 끊길 수 있음의미 단위 보존에 유리
적합한 문서 유형구조가 일정한 일반 문서, 짧은 기록보고서, 논문, 서술형 설명 문서
초기 MVP 적용 적합성적합상대적으로 부적합

길이 기반 청킹과 의미 기반 청킹 비교

길이 기반 청킹은 문서를 비슷한 크기로 자르는 대신 문장이 경계에서 끊길 수 있고, 의미 기반 청킹은 주제 경계에 맞춰 크기가 들쭉날쭉해지는 대신 같은 주제의 문장을 하나의 청크로 묶는다.

표로 정리하면 결국 선택은 “무엇을 더 아낄 것인가”의 문제다. 길이 기반 청킹은 구현과 운영 비용을 아끼는 대신 의미 단위 보존을 어느 정도 포기하고, 의미 기반 청킹은 의미 단위를 최대한 지키는 대신 처리 비용과 예측 가능성을 내준다. 둘 중 하나가 항상 우월한 게 아니라, 문서 특성과 서비스가 감당할 수 있는 비용에 따라 유리한 쪽이 달라진다.

6. 실제 서비스에서는 무엇을 선택할까

백엔드 개발자 입장에서 이 선택은 “더 정교한 방식”을 찾는 문제가 아니라 “지금 단계에서 감당 가능한 방식”을 찾는 문제에 가깝다.

초기 MVP라면 RecursiveCharacterTextSplitter처럼 단순하고 예측 가능한 방식으로 시작하는 편이 현실적이다. 대신 시작할 때부터 검색 품질을 판단할 기준 질문 몇 개와 그에 맞는 테스트 문서를 미리 준비해두는 게 중요하다. 기준이 없으면 청킹을 바꿔도 좋아졌는지 나빠졌는지 감으로만 판단하게 된다.

서비스를 운영하면서는 실제 검색이 실패한 사례를 계속 모아야 한다. 사용자 질문에 필요한 근거가 청크 경계 때문에 반쪽만 검색되거나, 하나의 청크에 서로 다른 주제가 섞여 있어 관련 없는 내용까지 함께 딸려 오는 경우가 반복적으로 확인된다면, 그때 청킹 전략을 개선하는 방향으로 가는 게 순서에 맞다.

이때도 모든 문서에 하나의 청킹 방식을 강제할 필요는 없다. Markdown, HTML, API 문서처럼 제목과 섹션 구조가 명확한 문서는 크기 기준보다 그 구조(헤더, 섹션)를 우선 활용해 나누는 편이 자연스럽다. 반대로 논문이나 보고서, 서술형으로 길게 이어지는 문서는 의미 기반 청킹을 검토해볼 만하다. 실제로는 구조 기반으로 큰 단위를 먼저 나누고, 그 안에서 다시 길이 기반으로 세부 크기를 맞추는 식으로 두 방식을 함께 적용하는 경우도 많다.

한 가지 더 고려할 부분은 운영 비용이다. 청킹 방식을 바꾸면 기존에 저장해둔 벡터를 그대로 쓸 수 없는 경우가 많다. 청크 경계 자체가 달라지므로 원본 문서를 다시 읽어 새 청크를 만들고, 그 청크들을 다시 임베딩해서 벡터 DB에 새로 넣어야 한다. 문서량이 많아질수록 이 재생성 작업 자체가 비용과 시간이 드는 작업이 된다.

그래서 결론은 이렇다. 처음부터 복잡한 의미 기반 청킹을 도입하기보다, 단순한 방식으로 기준선을 만들어두고 실제 검색 평가 결과를 바탕으로 필요한 부분만 개선해나가는 접근이 유지보수와 비용 측면에서 더 현실적이다.

7. 내가 이해한 점

이전에는 RAG의 품질이 대체로 임베딩 모델을 얼마나 잘 고르는지, 벡터 데이터베이스를 어떻게 구성하는지에 달려 있다고 생각했다. 그런데 이번에 청킹만 따로 파고들어 보니, 문서를 어떤 단위로 나누는지가 검색 결과에 훨씬 직접적으로 영향을 준다는 걸 알게 됐다. 아무리 좋은 임베딩 모델을 쓰더라도, 애초에 청크 안에 여러 주제가 섞여 있으면 그 벡터가 정확한 검색 기준점이 되기 어렵다.

의미 기반 청킹을 살펴보면서 인상 깊었던 부분은, 이 방식이 항상 정답은 아니라는 점이었다. 문장 단위로 임베딩을 계산해야 하니 처리 비용이 늘고, 임베딩 모델이나 임계값에 따라 결과가 달라져서 운영 복잡도도 함께 올라간다. “의미를 더 잘 이해하는 방식”이 곧 “실무에 더 적합한 방식”은 아니라는 걸 다시 확인한 셈이다.

그래서 실제로 서비스를 만든다면 처음에는 RecursiveCharacterTextSplitter 같은 길이 기반 청킹으로 시작하고, 검색이 실패하는 실제 사례를 기준으로 필요한 부분만 개선해나갈 것 같다. 처음부터 정교한 방식을 고르는 것보다, 무엇이 문제인지 눈으로 확인한 뒤 그에 맞춰 바꾸는 편이 결국 더 빠르고 덜 낭비되는 방법이라고 느꼈다.

References

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.