ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RAG 청킹 전략 완전 정복 — 콘텐츠별 최적 크기와 방법
    IT 2026. 3. 24. 22:00
    RAG 청킹 전략 완전 정복 — 콘텐츠별 최적 크기와 방법

    왜 청킹이 RAG의 성패를 가르는가

    지난 글에서 벡터 DB에 넣기 전 전처리가 중요하다고 했는데요, 전처리 다음 단계가 바로 청킹(chunking)입니다. 문서를 어떤 크기로, 어떤 기준으로 자르느냐에 따라 검색 품질이 완전히 달라집니다.

    같은 임베딩 모델, 같은 벡터 DB를 써도 청킹 전략만 바꾸면 검색 정확도가 54%에서 69%까지 차이가 납니다 (FloTorch 2026 벤치마크). 청킹은 RAG 파이프라인에서 가장 투자 대비 효과가 큰 구간이에요.

    diagram

    청킹 전략 6가지 — 각각 언제 쓸까

    청킹 전략은 크게 6가지로 나눌 수 있어요. 중요한 건 "어떤 전략이 최고인가"가 아니라 "내 콘텐츠에 맞는 전략이 뭔가"입니다.

    1. Fixed-Size Chunking (고정 크기 분할)

    가장 단순한 방법입니다. 정해진 토큰 수(예: 512개)마다 기계적으로 잘라요.

    💡 이런 상황에 적합:
    프로토타입이나 MVP를 빠르게 만들 때. 구현이 5줄이면 끝나니까요.

    왜 이 전략이 존재하나: 아무런 NLP 라이브러리나 모델 호출 없이 즉시 구현 가능합니다. 하지만 문장 중간에서 잘리거나 의미 경계를 무시하는 치명적 단점이 있어요. FloTorch 벤치마크에서 512토큰 기준 67% 정확도를 기록했는데, 이 정도면 "쓸 수는 있지만 더 나은 방법이 있다"는 수준입니다.

    2. Recursive Character Splitting (재귀 문자 분할) ⭐ 현재 업계 표준

    이름이 왜 "Recursive Character"일까요? 이 전략은 구분자(separator)를 계층적으로 시도합니다. 먼저 가장 큰 단위인 "빈 줄(단락)"로 자르고, 그래도 청크가 너무 크면 "줄바꿈"으로, 그래도 크면 "문장 끝(.)"으로, 최후에는 "문자(character)" 단위까지 내려갑니다. 이 과정을 재귀적(recursive)으로 반복하기 때문에 "Recursive Character Splitting"이라 부릅니다.

    핵심 아이디어: 가능한 한 큰 의미 단위(단락)로 자르되, 목표 크기를 넘으면 한 단계 작은 단위로 재시도하는 것이에요.

    # LangChain에서의 사용 예시
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=512,        # 목표 토큰 수
        chunk_overlap=50,      # 10% 중첩
        separators=["\n\n", "\n", ". ", " "]
        #           단락    줄바꿈  문장   단어 ← 이 순서대로 재귀 시도
    )

    왜 업계 표준이 됐나: 모델 호출 없이(=비용 0) 문서 구조를 존중합니다. FloTorch 2026 벤치마크(50개 학술논문, 905,746 토큰)에서 512토큰 기준 69% 정확도로, 훨씬 비싼 semantic chunking(54%)을 이겼어요. 가성비가 압도적이라 대부분의 RAG 프로젝트가 이것부터 시작합니다.

    3. Semantic Chunking (의미론적 분할)

    연속된 문장들의 임베딩 유사도를 계산해서, 주제가 바뀌는 지점에서 자릅니다.

    왜 이 전략이 존재하나: "의미적으로 일관된 청크"라는 아이디어는 직관적으로 훌륭합니다. 실제로 Chroma 평가에서 91.9% recall을 달성했어요.

    하지만 함정이 있습니다: FloTorch의 end-to-end 테스트에서는 54% 정확도로 최하위를 기록했어요. 이유는 주제가 자주 바뀌는 문서에서 평균 청크 크기가 43토큰까지 줄어들어, LLM이 답변을 생성하기에 충분한 맥락을 받지 못했기 때문이에요.

    Vectara가 NAACL 2025에서 발표한 연구도 같은 결론입니다: "현실적 문서에서 고정 크기가 의미론적 분할을 모든 태스크에서 일관되게 앞섰다."

    ⚠️ 주의: Semantic chunking은 "검색"만 놓고 보면 좋지만, "검색→LLM 답변 생성"의 전체 파이프라인에서는 오히려 역효과가 날 수 있습니다. 청크가 너무 작아지지 않도록 최소 크기 제한을 꼭 설정하세요.

    4. Document-Structure-Based Chunking (문서 구조 기반)

    Markdown 헤더(H1~H6), HTML 태그, PDF 페이지 경계 같은 문서 자체 구조를 기준으로 자릅니다.

    왜 이 전략이 존재하나: 문서의 저자가 이미 의미 단위로 구조를 나눠놨으니, 그 구조를 활용하는 것이 합리적이에요. NVIDIA 2024 벤치마크에서 페이지 단위 분할이 0.648 정확도, 최저 분산(0.107)으로 1위를 차지했습니다. 정확도뿐 아니라 안정성도 가장 좋았다는 뜻이에요.

    # Markdown 헤더 기반 분할 예시
    from langchain.text_splitter import MarkdownHeaderTextSplitter
    
    headers = [
        ("#", "h1"), ("##", "h2"), ("###", "h3")
    ]
    md_splitter = MarkdownHeaderTextSplitter(
        headers_to_split_on=headers
    )
    # 큰 섹션은 다시 Recursive로 2차 분할
    chunks = md_splitter.split_text(document)
    for chunk in chunks:
        if len(chunk) > 512:
            sub_chunks = recursive_splitter.split_text(chunk)

    5. AST-Based Chunking (코드 전용)

    AST는 Abstract Syntax Tree(추상 구문 트리)의 약자입니다. "추상(Abstract)"이라는 이름이 붙은 이유는, 소스 코드의 괄호나 세미콜론 같은 표면적 문법 요소를 버리고 의미 구조만 트리 형태로 추출하기 때문이에요.

    예를 들어 이런 Python 코드가 있다면:

    def calculate_total(items):
        total = 0
        for item in items:
            total += item.price
        return total

    AST 파서는 이 코드를 이렇게 이해합니다:

    Module
     └─ FunctionDef: "calculate_total"
         ├─ args: ["items"]
         ├─ Assign: total = 0
         ├─ For: item in items
         │   └─ AugAssign: total += item.price
         └─ Return: total

    왜 코드에 적합한가: 일반 텍스트 분할기는 줄바꿈이나 빈 줄로 자르기 때문에 for 루프 중간에서 잘리거나, 함수의 앞부분과 뒷부분이 다른 청크로 분리될 수 있어요. AST 기반 분할은 "함수 하나 = 청크 하나"처럼 프로그래밍 언어의 의미 단위를 기준으로 자르기 때문에, 검색된 청크가 항상 완결된 코드 조각이 됩니다.

    성능: cAST 프레임워크의 벤치마크에서 RepoEval Recall@5가 4.3포인트, SWE-bench Pass@1이 2.3~2.7포인트 향상됐어요.

    6. Agentic Chunking (에이전트 기반)

    AI 에이전트가 문서 전체를 분석한 뒤, 섹션마다 다른 전략을 동적으로 적용합니다. 예를 들어 의료 보고서에서 환자 이력은 semantic, 검사 수치는 structured 방식으로 나누는 식이에요.

    왜 이 전략이 존재하나: 현실 문서는 한 가지 전략으로 커버되지 않는 경우가 많아요. 하지만 아직 실험적 단계이고, 모든 청크에 LLM 호출이 필요해서 비용이 높습니다.

    청크 크기 — "1500 토큰"은 맞는 얘기일까?

    "청크 크기는 1500 토큰이 좋다"는 얘기가 돌아다닙니다. 하지만 벤치마크들을 종합하면, 대부분의 상황에서 400~512 토큰이 최적이에요. 1500 토큰은 특정 상황에서만 정당화됩니다.

    벤치마크 출처별 권장 크기

    출처 권장 크기 비고
    FloTorch 2026 (50개 논문) 512 토큰 7개 전략 중 1위
    Chroma Research 400 토큰 88~89% recall
    NVIDIA 2024 256~1024 토큰 극단적 크기는 성능 저하
    LlamaIndex 권장 400~600 토큰 베이스라인 시작점
    Unstructured ~250 토큰 실험 시작점

    쿼리 유형별 최적 크기

    가장 중요한 인사이트: 청크 크기는 "쿼리가 어떤 종류인가"에 따라 달라야 합니다.

    쿼리 유형 최적 크기 이유
    Factoid (이름, 날짜, 사실 질문) 256~512 토큰 정밀한 키워드 매칭이 중요
    Analytical (설명, 비교, 분석) 1024+ 토큰 넓은 맥락이 필요
    혼합 (다양한 질문) 400~512 토큰 양쪽의 균형점

    데이터셋별 실험 결과

    2025년 Multi-Dataset 논문에서 재미있는 결과가 나왔어요. 데이터셋마다 최적 크기가 다릅니다.

    데이터셋 최적 크기 Recall@1
    SQuAD (짧은 위키 질답) 64 토큰 64.1%
    NewsQA (뉴스 기사) 512 토큰 55.9%
    TechQA (기술 문서) 512 토큰 61.3%
    Natural Questions 512~1024 토큰 47.7%
    NarrativeQA (서사 텍스트) 1024 토큰 10.7%

    기술 문서(TechQA)에서 512 토큰이 최적이라는 점, 그리고 짧은 팩트 질문(SQuAD)에서는 64 토큰만으로도 최고 성능이라는 점이 눈에 띕니다.

    Context Cliff — 2,500 토큰의 절벽

    2026년 1월 체계적 분석에서 발견된 흥미로운 현상이 있어요. 청크 크기가 ~2,500 토큰을 넘으면 응답 품질이 급격히 떨어집니다. LLM이 긴 청크 안에서 핵심 정보를 찾아내는 데 어려움을 겪기 때문이에요.

    diagram

    그래서 1500 토큰은 안전 범위 안에 있지만, 대부분의 상황에서 400~512가 더 낫습니다. 1500 토큰이 정당화되는 경우는:

    • 법률/학술 문서: 맥락 의존성이 매우 높아 넓은 범위가 필요할 때
    • 금융 보고서: 800~1500 토큰 권장 (수치와 맥락이 함께 있어야 의미)
    • Parent-Child 구조의 Parent 청크: 500~2000 토큰. 이건 같은 문서를 "큰 덩어리(parent)"와 "작은 조각(child)" 두 가지 크기로 동시에 저장하는 기법이에요. 검색은 작은 child로 정밀하게 하고, 매칭된 child의 parent(큰 덩어리)를 LLM에 전달해서 충분한 맥락을 제공합니다. 이때 parent 크기가 1500 토큰 정도면 적당해요. 아래 고급 기법 섹션에서 자세히 다룹니다.

    콘텐츠 유형별 실전 전략

    개인 지식 금고 (Obsidian 스타일)

    Obsidian 같은 개인 노트에는 Markdown Header Splitting + Recursive 하이브리드가 최적입니다.

    왜: 개인 노트에는 frontmatter(YAML), wikilink([[노트명]]), 태그(#태그) 같은 고유 요소가 있어요. 이것들을 "텍스트"로 취급하면 임베딩 품질이 떨어지고, "메타데이터"로 분리하면 필터링과 그래프 탐색에 활용할 수 있어요.

    요소 처리 방법 이유
    Frontmatter (YAML) 파싱 → 메타데이터 필드로 분리 tags, date, category를 필터링에 활용
    Wikilink [[note]] 텍스트에서 보존 + 관계 정보를 메타데이터에 기록 GraphRAG 연결, 관련 문서 추적
    태그 #tag 메타데이터 필드로 추출 검색 시 필터 조건으로 활용
    짧은 노트 (일일 메모 등) 청킹 안 함, 노트 전체 = 1 청크 이미 충분히 짧으므로 자를 필요 없음
    긴 프로젝트 노트 H1/H2/H3 기반 1차 분할 → Recursive 2차 분할 섹션별 의미 단위 유지

    권장 크기: 256~512 토큰 (짧은 노트는 전체를 하나의 청크로)

    개발자 API Reference & Guide

    API 문서에는 Structure-aware Splitting이 적합합니다. 이건 새로운 전략이 아니라, 위에서 소개한 4번 Document-Structure-Based Chunking을 API 문서에 맞게 적용한 것이에요. API 문서는 이미 endpoint별, 메서드별로 잘 구조화되어 있으니 — Markdown 헤더나 HTML 태그 같은 문서 구조를 그대로 분할 기준으로 활용하고, 큰 섹션은 2번 Recursive Splitting으로 2차 분할하는 하이브리드 방식입니다.

    핵심 원칙:

    • 각 API endpoint = 하나의 청크 (설명 + 파라미터 + 코드 예제를 함께 유지)
    • 파라미터 테이블, 응답 스키마는 절대 중간에서 자르지 않기
    • 코드 블록(backtick fence)은 하나의 단위로 보존
    • 각 청크 앞에 맥락 헤더(Contextual Chunk Header) 추가
    # API 문서 청킹 시 Contextual Header 추가 예시
    def add_context_header(chunk, doc_title, section_path):
        """
        예시 결과:
        "[API: POST /users/create > Authentication > Request Body]
         이 endpoint는 새 사용자를 생성합니다..."
        """
        header = f"[{doc_title} > {' > '.join(section_path)}]\n"
        return header + chunk.text
    💡 실전 팁: 문서 제목만 청크 앞에 추가해도 임베딩 유사도가 0.1 → 0.92로 급증한 테스트 결과가 있습니다 (Contextual Chunk Headers 연구). 가장 비용 대비 효과가 큰 기법이에요.

    권장 크기: 512~1024 토큰 (API 설명 + 코드 예제가 함께 들어가야 하므로 일반 문서보다 크게)

    코드 샘플 & 튜토리얼

    코드는 AST 기반 분할(5번 전략)이 이상적이지만, 구현 부담이 크다면 코드 전용 Recursive Splitting도 효과적입니다. "코드 전용 Recursive"란, 위에서 설명한 2번 Recursive Character Splitting의 구분자(separator) 목록을 프로그래밍 언어에 맞게 바꾼 것이에요. 일반 텍스트에서는 "빈 줄 → 줄바꿈 → 문장 → 단어"로 재귀하지만, 코드에서는 "클래스 → 함수 → 빈 줄 → 줄바꿈"으로 재귀합니다.

    # 방법 1: LangChain의 언어별 Recursive Splitter (가장 간편)
    from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
    
    # from_language()를 쓰면 언어별 separator를 자동 설정
    python_splitter = RecursiveCharacterTextSplitter.from_language(
        language=Language.PYTHON,
        chunk_size=1024,
        chunk_overlap=0     # 코드는 overlap 불필요
    )
    # Python, JS, TS, Go, Java, Rust, Ruby 등 20+ 언어 지원
    # 방법 2: LlamaIndex의 AST 기반 CodeSplitter (tree-sitter 활용)
    from llama_index.core.node_parser import CodeSplitter
    
    # tree-sitter로 실제 AST를 파싱해서 함수/클래스 경계로 분할
    splitter = CodeSplitter(
        language="python",
        chunk_lines=40,
        chunk_lines_overlap=15,
        max_chars=1500
    )
    # 함수 중간에서 절대 잘리지 않음 — AST 노드 단위로 분할

    AST 기반 도구 더 보기:

    • tree-sitter (pip install tree-sitter tree-sitter-languages): 100+ 언어 지원 파서. LlamaIndex CodeSplitter의 내부 엔진이기도 합니다. 직접 AST를 걸어다니며 커스텀 분할 로직을 짤 수 있어요.
    • code-chunker (pip install code-chunker): tree-sitter 기반의 경량 청킹 라이브러리. 함수/클래스/메서드를 개별 청크로 추출하며, 파일 경로·시작/끝 라인 등 메타데이터도 함께 반환합니다.

    왜 코드에는 overlap이 불필요한가: 함수/클래스 경계로 자르면 의미 단위가 이미 완결되어 있어요. 중첩을 넣으면 오히려 불완전한 코드 조각이 중복되어 혼란만 줍니다.

    권장: 비공백 문자 수 기준으로 크기 측정. 같은 줄 수라도 주석이 많은 코드와 로직이 빽빽한 코드는 정보 밀도가 다르기 때문이에요.

    고급 기법 — 청킹 품질을 더 높이려면

    Overlap (중첩)

    청크 크기의 10~20%를 겹치게 설정합니다. 512토큰이면 50~100토큰 overlap이에요.

    효과: 청크 경계에서 잘린 문맥을 복구할 수 있어요. 하지만 overlap이 클수록 인덱스 크기가 늘어나고, 같은 정보가 여러 청크에 중복되어 LLM이 혼란을 겪을 수 있으니 적정선을 지키세요.

    Contextual Retrieval (Anthropic 방식)

    Anthropic이 발표한 기법으로, 각 청크에 대해 전체 문서 맥락에서 짧은 설명(50~100 토큰)을 LLM이 생성해서 앞에 붙입니다.

    예시 — before:
    "The company's revenue grew by 3% over the previous quarter."

    예시 — after (with context):
    "[This chunk is from ACME Corp's Q2 2023 SEC filing. Previous quarter revenue was $314M.] The company's revenue grew by 3% over the previous quarter."

    성능:

    • Contextual Embeddings만: 검색 실패 35% 감소
    • + Contextual BM25: 49% 감소
    • + Reranking까지: 67% 감소 (5.7% → 1.9%)

    비용: Prompt caching 활용 시 문서 100만 토큰당 약 $1.02. 품질 대비 비용이 합리적이에요.

    Parent-Child Chunking (부모-자식)

    앞서 청크 크기 논의에서 "작은 청크가 검색에 유리하고, 큰 청크가 답변 품질에 유리하다"는 딜레마를 봤어요. Parent-Child Chunking은 이 딜레마를 정면으로 해결합니다: 같은 문서를 두 가지 크기로 동시에 저장하는 거예요.

    비유하자면 도서관에서 책 전체(parent)와 핵심 문장 카드(child)를 동시에 보관하는 것과 같아요. 검색할 때는 핵심 문장 카드에서 정확한 매칭을 찾고, 찾으면 그 카드가 속한 책의 해당 챕터 전체를 꺼내서 읽는 방식입니다.

    diagram

    • Child 청크 (100~500 토큰): 정밀한 검색용 — 임베딩 인덱스에 저장
    • Parent 청크 (500~2000 토큰): 답변 생성용 — child가 매칭되면 parent를 LLM에 전달

    이 방법이 "1500 토큰" 논의와 연결됩니다. 검색은 작게, 답변은 크게 — 두 마리 토끼를 잡는 구조예요.

    Parent-Child를 쓰려면 특별한 벡터 DB가 필요할까?

    결론부터 말하면: 아니요, 어떤 벡터 DB든 가능합니다. Parent-Child는 벡터 DB의 기능이 아니라 애플리케이션 레벨의 패턴이에요. 구현 방법은 간단합니다:

    1. Child 청크에 parent_id를 메타데이터로 저장
    2. 검색 시 child 청크가 매칭되면, parent_id로 parent 청크를 별도 조회

    다만 벡터 DB마다 이 과정의 편의성은 차이가 있어요:

    벡터 DB Parent-Child 지원 구현 방식
    Weaviate 가장 편리 (cross-reference 기능) 객체 간 참조 관계를 네이티브로 정의, 한 번의 쿼리로 child→parent 탐색
    Qdrant 편리 (grouping API) payload에 parent_id 저장, 그룹별 검색 또는 ID 조회로 parent 획득
    Pinecone / Milvus / Chroma 수동 구현 메타데이터에 parent_id 저장 → 2단계 조회 (child 검색 → parent ID로 fetch)

    실무에서는 LangChain의 ParentDocumentRetrieverLlamaIndex의 AutoMergingRetriever를 쓰면 벡터 DB와 무관하게 Parent-Child 패턴을 자동으로 처리해줍니다. 프레임워크가 parent를 별도 docstore(SQLite, Redis 등)에 저장하고, child 매칭 시 자동으로 parent를 꺼내주는 방식이에요.

    시나리오별 종합 권장표

    시나리오 전략 청크 크기 Overlap 추가 기법
    일반 Q&A Recursive 400~512 토큰 10~20% Contextual headers
    검색/탐색 Recursive + BM25 256~512 토큰 10~20% 메타데이터 필터링
    문서 요약 Page-level / Recursive 1024 토큰 15% Parent-child
    코드 검색 AST-based 함수/클래스 단위 불필요 비공백 문자 기준
    API 문서 Markdown Header + Recursive 512~1024 토큰 10% endpoint 메타데이터
    개인 지식 금고 Markdown Header + Recursive 256~512 토큰 10~20% frontmatter → 메타데이터
    법률/학술 문서 Recursive / Page-level 512~1024 토큰 15% Contextual retrieval
    FAQ/짧은 문서 청킹 안 함 문서 전체 N/A
    금융 보고서 Page-level 1024 토큰 15% 표 구조 보존

    정리 — 청킹은 "정답"이 아니라 "선택"이다

    청킹에는 만능 정답이 없습니다. 하지만 방향은 명확해요:

    1. 시작은 Recursive + 512 토큰으로. 대부분의 상황에서 가장 안정적인 성능을 보입니다.
    2. 콘텐츠 구조가 있다면 활용하세요. Markdown 헤더, API endpoint 경계 — 문서의 저자가 이미 만들어둔 의미 단위를 무시하지 마세요.
    3. 2,500 토큰 절벽을 넘지 마세요. 아무리 맥락이 필요해도 2,500 토큰 이상은 역효과입니다.
    4. 검색과 답변의 크기를 분리하세요. Parent-Child 구조로 작은 청크는 검색용, 큰 청크는 답변용으로 활용하면 두 가지 장점을 모두 얻을 수 있어요.
    5. Contextual headers는 반드시 적용하세요. 문서 제목만 붙여도 검색 품질이 극적으로 올라갑니다.

    다음 글에서는 실제로 이 전략들을 적용해서 벡터 DB에 넣고 검색하는 과정을 코드와 함께 다뤄보겠습니다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.