🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

청킹 전략 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 18. · 5 Views

청킹 전략 완벽 가이드

RAG 시스템에서 문서를 효과적으로 나누는 청킹 전략을 배웁니다. 고정 크기, 시맨틱, 계층적 청킹 방법과 각각의 장단점을 실무 예제와 함께 알아봅니다.


목차

  1. 청킹이란 무엇인가?
  2. 고정 크기 청킹
  3. 시맨틱 청킹
  4. 계층적 청킹
  5. 청크 오버랩 설정
  6. 청킹 전략 선택 기준

1. 청킹이란 무엇인가?

어느 날 이민수 씨는 회사에서 RAG 챗봇 프로젝트를 맡게 되었습니다. 긴 문서를 벡터 데이터베이스에 저장하려는데, 선배가 물었습니다.

"민수 씨, 청킹 전략은 어떻게 가져갈 건가요?"

청킹은 긴 텍스트를 작은 단위로 나누는 것입니다. 마치 두꺼운 책을 여러 개의 챕터로 나누는 것과 같습니다.

적절한 청킹 전략을 선택하면 검색 정확도와 응답 품질이 크게 향상됩니다.

다음 코드를 살펴봅시다.

# 기본 청킹 예제
def simple_chunk(text, chunk_size=500):
    # 텍스트를 지정된 크기로 나눕니다
    chunks = []
    for i in range(0, len(text), chunk_size):
        # 각 청크는 chunk_size만큼의 문자를 포함합니다
        chunk = text[i:i + chunk_size]
        chunks.append(chunk)
    return chunks

# 실제 사용 예제
document = "이것은 매우 긴 문서입니다..." * 100
chunked_docs = simple_chunk(document, chunk_size=500)
print(f"총 {len(chunked_docs)}개의 청크가 생성되었습니다")

이민수 씨는 입사 6개월 차 백엔드 개발자입니다. 최근 회사에서 고객 지원 챗봇을 개발하는 프로젝트에 투입되었습니다.

방대한 제품 문서를 LLM에게 제공해야 하는데, 어떻게 해야 할지 막막했습니다. 박시니어 씨가 다가와 화면을 보더니 말했습니다.

"민수 씨, 문서를 통째로 넣으려고 하면 안 돼요. 청킹 전략을 먼저 세워야죠." 청킹이란 정확히 무엇일까요?

쉽게 비유하자면, 청킹은 마치 도서관 사서가 두꺼운 백과사전을 여러 권으로 나누는 것과 같습니다. 천 페이지짜리 책을 한 번에 읽기는 어렵지만, 백 페이지씩 열 권으로 나누면 필요한 정보를 빠르게 찾을 수 있습니다.

청킹도 마찬가지로 긴 문서를 적절한 크기의 조각으로 나누는 작업입니다. 청킹이 없던 시절에는 어땠을까요?

초기 RAG 시스템에서는 문서 전체를 그대로 벡터 데이터베이스에 저장하려고 했습니다. 하지만 문제가 생겼습니다.

임베딩 모델에는 토큰 제한이 있었고, 너무 긴 텍스트는 처리할 수 없었습니다. 더 큰 문제는 검색 정확도였습니다.

백 페이지짜리 문서에서 딱 한 문장만 필요한데, 전체 문서가 반환되면 LLM이 혼란스러워했습니다. 바로 이런 문제를 해결하기 위해 청킹 전략이 등장했습니다.

청킹을 사용하면 검색 정확도가 향상됩니다. 사용자 질문과 관련된 정확한 부분만 찾아낼 수 있기 때문입니다.

또한 토큰 제한 문제도 해결할 수 있습니다. 각 청크는 임베딩 모델이 처리할 수 있는 크기로 유지됩니다.

무엇보다 응답 품질이라는 큰 이점이 있습니다. LLM이 관련 없는 정보에 방해받지 않고 핵심 내용에 집중할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 2번째 줄을 보면 청킹 함수를 정의하는 것을 알 수 있습니다.

chunk_size 매개변수로 각 청크의 크기를 지정합니다. 이 부분이 핵심입니다.

다음으로 5번째 줄에서는 텍스트를 순회하며 지정된 크기만큼 잘라냅니다. 마지막으로 청크 리스트가 반환됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 법률 문서 검색 서비스를 개발한다고 가정해봅시다.

수백 페이지에 달하는 법률 조항을 사용자가 쉽게 검색할 수 있어야 합니다. 이때 각 조항을 하나의 청크로 나누면 사용자가 "상속 관련 법률"을 검색했을 때 정확히 상속 조항만 반환할 수 있습니다.

많은 법률 기술 회사에서 이런 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 너무 작거나 큰 청크 크기를 설정하는 것입니다. 청크가 너무 작으면 문맥이 손실되고, 너무 크면 검색 정확도가 떨어집니다.

따라서 도메인과 문서 특성에 맞는 적절한 크기를 실험을 통해 찾아야 합니다. 다시 이민수 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 이민수 씨는 고개를 끄덕였습니다. "아, 그래서 청킹이 필요한 거군요!" 청킹을 제대로 이해하면 더 정확하고 효율적인 RAG 시스템을 구축할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 일반적으로 500-1000자 크기의 청크가 많이 사용됩니다

  • 도메인에 따라 최적 청크 크기는 다르므로 A/B 테스트를 진행하세요

2. 고정 크기 청킹

이민수 씨가 가장 먼저 시도한 방법은 고정 크기 청킹이었습니다. "일단 간단한 방법부터 시작해볼까?" 하지만 첫 번째 테스트에서 예상치 못한 문제가 발생했습니다.

고정 크기 청킹은 문서를 정해진 문자 수나 토큰 수로 균등하게 나누는 방식입니다. 마치 케이크를 같은 크기로 자르는 것과 같습니다.

구현이 간단하지만, 문장이나 문단 중간에서 잘릴 수 있다는 단점이 있습니다.

다음 코드를 살펴봅시다.

def fixed_size_chunk(text, chunk_size=500, overlap=50):
    # 오버랩을 고려한 고정 크기 청킹
    chunks = []
    start = 0

    while start < len(text):
        # 현재 위치에서 chunk_size만큼 잘라냅니다
        end = start + chunk_size
        chunk = text[start:end]
        chunks.append(chunk)

        # 다음 청크는 overlap만큼 겹치게 시작합니다
        start = end - overlap

    return chunks

# 테스트
text = "Python은 강력한 프로그래밍 언어입니다. " * 50
chunks = fixed_size_chunk(text, chunk_size=200, overlap=20)
print(f"생성된 청크 개수: {len(chunks)}")

이민수 씨는 제품 매뉴얼 문서를 청킹하기 시작했습니다. 가장 간단해 보이는 방법인 고정 크기 청킹을 선택했습니다.

"500자씩 나누면 되겠지?" 하지만 첫 번째 결과를 확인하고 당황했습니다. 문장이 중간에 뚝 잘려 있었습니다.

"이 제품은 고객의 만족을 위해 설계되었"처럼 말입니다. 박시니어 씨가 화면을 보더니 웃으며 말했습니다.

"고정 크기 청킹의 고전적인 문제네요." 고정 크기 청킹이란 무엇일까요? 비유하자면, 고정 크기 청킹은 마치 피자를 같은 크기로 자르는 것과 같습니다.

자르는 선이 어디에 있든 상관없이 정해진 각도로 칼을 넣습니다. 토핑이 중간에 잘리든, 페퍼로니가 반으로 나뉘든 신경 쓰지 않습니다.

오직 크기만 중요합니다. 고정 크기 청킹도 마찬가지로 문자 수나 토큰 수만 보고 기계적으로 자릅니다.

이 방식의 장점은 무엇일까요? 첫째, 구현이 매우 간단합니다.

위 코드에서 보듯이 반복문 하나로 끝납니다. 둘째, 처리 속도가 빠릅니다.

복잡한 자연어 처리 없이 단순 계산만 하면 되기 때문입니다. 셋째, 예측 가능한 청크 개수를 얻을 수 있습니다.

문서 크기를 청크 크기로 나누면 대략적인 개수를 알 수 있습니다. 하지만 단점도 명확합니다.

가장 큰 문제는 문맥 손실입니다. 문장이 중간에 잘리면 의미를 파악하기 어렵습니다.

"이 제품은 방수 기능"까지만 있으면, "방수 기능이 있다"는 건지 "방수 기능이 없다"는 건지 알 수 없습니다. 이는 검색 품질에 직접적인 악영향을 미칩니다.

위의 코드를 자세히 살펴보겠습니다. 3번째 줄에서 chunks 리스트를 초기화합니다.

7번째 줄의 while 루프가 핵심입니다. 텍스트 끝까지 반복하면서 청크를 생성합니다.

특히 주목할 점은 14번째 줄입니다. 다음 청크의 시작 위치를 계산할 때 overlap만큼 뒤로 이동합니다.

이렇게 하면 청크 간 일부 내용이 겹치게 되어 문맥 손실을 조금이나마 완화할 수 있습니다. 실무에서는 어떻게 활용할까요?

단순한 로그 분석 시스템에서는 고정 크기 청킹이 효과적입니다. 각 로그 라인이 독립적이고, 순서가 중요하지 않은 경우입니다.

예를 들어 서버 에러 로그를 분석할 때 각 에러 메시지는 독립적이므로 중간에 잘려도 큰 문제가 없습니다. 많은 로그 분석 도구에서 이 방식을 사용합니다.

하지만 주의사항이 있습니다. 고정 크기 청킹은 문학 작품이나 법률 문서처럼 문맥이 중요한 경우에는 적합하지 않습니다.

소설의 한 장면이 중간에 잘리면 스토리 흐름을 이해할 수 없습니다. 법률 조항이 중간에 잘리면 의미가 완전히 달라질 수 있습니다.

이런 경우에는 시맨틱 청킹 같은 다른 방법을 고려해야 합니다. 이민수 씨는 오버랩 설정을 추가했습니다.

각 청크가 이전 청크의 마지막 50자와 겹치도록 했습니다. 결과가 조금 나아졌지만, 여전히 완벽하지는 않았습니다.

"다른 방법도 알아봐야겠어." 고정 크기 청킹은 시작점으로는 좋지만, 프로덕션 환경에서는 더 정교한 방법이 필요할 수 있습니다. 여러분의 문서 특성에 맞는 방법을 선택하세요.

실전 팁

💡 - 오버랩은 보통 청크 크기의 10-20% 정도로 설정합니다

  • 토큰 기반 청킹이 문자 기반보다 LLM과의 호환성이 좋습니다

3. 시맨틱 청킹

고정 크기 청킹의 한계를 느낀 이민수 씨는 박시니어 씨에게 조언을 구했습니다. "더 나은 방법이 있을까요?" 박 씨는 웃으며 대답했습니다.

"시맨틱 청킹을 한번 써보세요."

시맨틱 청킹은 문서의 의미와 구조를 고려하여 자연스러운 경계에서 나누는 방식입니다. 마치 신문 기사를 문단별로 오려내는 것과 같습니다.

문맥을 보존하므로 검색 품질이 크게 향상되지만, 구현이 복잡합니다.

다음 코드를 살펴봅시다.

import re

def semantic_chunk(text, max_chunk_size=800):
    # 문단 단위로 먼저 분리합니다
    paragraphs = text.split('\n\n')
    chunks = []
    current_chunk = ""

    for paragraph in paragraphs:
        # 현재 청크에 문단을 추가했을 때 크기 확인
        if len(current_chunk) + len(paragraph) < max_chunk_size:
            current_chunk += paragraph + "\n\n"
        else:
            # 크기를 초과하면 현재 청크를 저장하고 새로 시작
            if current_chunk:
                chunks.append(current_chunk.strip())
            current_chunk = paragraph + "\n\n"

    # 마지막 청크 추가
    if current_chunk:
        chunks.append(current_chunk.strip())

    return chunks

# 테스트 문서
doc = "첫 번째 문단입니다.\n이것은 중요한 내용입니다.\n\n두 번째 문단입니다.\n새로운 주제를 다룹니다."
chunks = semantic_chunk(doc, max_chunk_size=100)

이민수 씨는 시맨틱 청킹에 대해 검색해보기 시작했습니다. 개념은 이해했지만 구체적으로 어떻게 구현해야 할지 막막했습니다.

"문단 경계를 찾아서 나눈다는 건데, 실제로는 어떻게 하는 거지?" 박시니어 씨가 옆에 앉으며 설명을 시작했습니다. "고정 크기 청킹은 칼로 자르는 거라면, 시맨틱 청킹은 종이를 접힌 선대로 찢는 거예요." 시맨틱 청킹이 정확히 무엇인지 알아봅시다.

비유하자면, 시맨틱 청킹은 마치 요리사가 닭고기를 손질하는 것과 같습니다. 마구 자르는 게 아니라 관절을 따라 부위별로 분리합니다.

날개는 날개대로, 다리는 다리대로 자연스러운 경계를 따릅니다. 시맨틱 청킹도 마찬가지로 문서의 자연스러운 구조를 따라 나눕니다.

문단 경계, 섹션 구분, 문장 마침표 등을 활용합니다. 왜 시맨틱 청킹이 필요할까요?

고정 크기 청킹을 사용하다 보면 문제가 생깁니다. "이 제품은 방수 기능이 있"에서 잘리면, 다음 청크는 "습니다.

단, 수심 1미터까지만"으로 시작합니다. 두 청크 모두 의미가 불완전합니다.

사용자가 "방수 깊이"를 검색하면 정확한 답을 찾기 어렵습니다. 이런 문제가 누적되면 RAG 시스템의 신뢰도가 떨어집니다.

시맨틱 청킹은 이런 문제를 어떻게 해결할까요? 의미 단위를 보존하는 것이 핵심입니다.

한 문단이나 한 섹션처럼 완결된 의미를 가진 단위로 나눕니다. "이 제품은 방수 기능이 있습니다.

단, 수심 1미터까지만 보장됩니다."라는 전체 문단이 하나의 청크가 됩니다. 사용자가 방수에 대해 질문하면 완전한 정보를 제공할 수 있습니다.

위의 코드를 단계별로 분석해봅시다. 4번째 줄에서 텍스트를 두 개의 줄바꿈('\n\n')으로 분리합니다.

이는 일반적인 문단 구분 방식입니다. 10번째 줄이 중요한데, 현재 청크에 새 문단을 추가했을 때의 크기를 미리 계산합니다.

만약 최대 크기를 초과하면 15번째 줄에서 현재 청크를 저장하고 새로 시작합니다. 이렇게 하면 각 청크가 완전한 문단들로 구성됩니다.

실무에서 어떻게 활용될까요? 기술 문서 검색 시스템을 예로 들어봅시다.

API 문서는 보통 각 엔드포인트마다 설명, 파라미터, 응답 예제로 구성됩니다. 시맨틱 청킹을 사용하면 각 엔드포인트를 하나의 청크로 만들 수 있습니다.

개발자가 "로그인 API 파라미터"를 검색하면 로그인 엔드포인트의 전체 정보를 한 번에 얻습니다. 많은 개발자 포털이 이런 방식을 사용합니다.

고급 시맨틱 청킹 기법도 있습니다. 단순히 문단으로 나누는 것을 넘어, 임베딩 유사도를 활용하는 방법이 있습니다.

연속된 문장들의 임베딩을 계산하고, 유사도가 급격히 떨어지는 지점을 경계로 삼습니다. 이는 주제가 바뀌는 지점을 자동으로 감지하는 것입니다.

하지만 계산 비용이 크므로 모든 경우에 적합하지는 않습니다. 주의할 점이 있습니다.

시맨틱 청킹은 문서에 명확한 구조가 있을 때 효과적입니다. 마크다운, HTML, 구조화된 텍스트 등이 좋은 예입니다.

반면 소설처럼 구조가 불명확한 텍스트는 문단 구분이 모호할 수 있습니다. 또한 처리 시간이 더 오래 걸립니다.

문서 구조를 분석하는 과정이 추가되기 때문입니다. 이민수 씨는 제품 매뉴얼에 시맨틱 청킹을 적용했습니다.

결과를 테스트해보니 검색 정확도가 눈에 띄게 향상되었습니다. "이제 답변이 훨씬 자연스럽네요!" 박시니어 씨가 웃으며 말했습니다.

"그렇죠? 문맥을 보존하는 게 얼마나 중요한지 알겠죠?" 시맨틱 청킹은 대부분의 구조화된 문서에서 고정 크기 청킹보다 나은 결과를 제공합니다.

여러분의 프로젝트에도 적용해보세요.

실전 팁

💡 - HTML이나 마크다운의 헤더 태그를 활용하면 더 정확한 시맨틱 청킹이 가능합니다

  • 문장 단위 청킹에는 spaCy 같은 NLP 라이브러리를 활용하세요

4. 계층적 청킹

프로젝트가 진행되면서 이민수 씨는 새로운 문제에 부딪혔습니다. 사용자가 "3장의 두 번째 절 내용"처럼 구체적으로 질문할 때 정확한 답을 찾기 어려웠습니다.

"문서 구조를 그대로 유지할 방법이 없을까?"

계층적 청킹은 문서의 계층 구조를 보존하며 여러 레벨로 나누는 방식입니다. 마치 책의 목차 구조를 그대로 유지하는 것과 같습니다.

상위 레벨에서 큰 주제를 찾고, 하위 레벨에서 세부 내용을 검색할 수 있습니다.

다음 코드를 살펴봅시다.

class HierarchicalChunk:
    def __init__(self, content, level, parent=None):
        self.content = content  # 청크 내용
        self.level = level      # 계층 레벨 (1=장, 2=절, 3=항)
        self.parent = parent    # 상위 청크 참조
        self.children = []      # 하위 청크 목록

    def add_child(self, child):
        # 하위 청크 추가
        self.children.append(child)

def hierarchical_chunk(text):
    # 마크다운 헤더 기반 계층적 청킹
    lines = text.split('\n')
    root = HierarchicalChunk("Root", 0)
    current_parent = {0: root}

    for line in lines:
        if line.startswith('#'):
            # 헤더 레벨 계산 (# 개수)
            level = len(line) - len(line.lstrip('#'))
            content = line.lstrip('#').strip()

            # 새 청크 생성 및 트리에 추가
            chunk = HierarchicalChunk(content, level, current_parent[level-1])
            current_parent[level-1].add_child(chunk)
            current_parent[level] = chunk

    return root

# 테스트
markdown = """# 1장 소개
## 1.1 개요
내용입니다.
## 1.2 목적
목적 내용입니다."""
tree = hierarchical_chunk(markdown)

이민수 씨의 챗봇은 점점 인기를 얻었습니다. 하지만 사용자들이 더 구체적인 질문을 하기 시작했습니다.

"제품 매뉴얼 3장 2절의 설치 방법을 알려줘." 같은 질문이었습니다. 현재 시스템으로는 정확한 위치를 찾기 어려웠습니다.

박시니어 씨가 화면을 보며 말했습니다. "문서 구조를 유지하는 계층적 청킹이 필요하겠네요." 계층적 청킹이란 무엇일까요?

비유하자면, 계층적 청킹은 마치 회사 조직도와 같습니다. CEO 아래에 부서장이 있고, 부서장 아래에 팀장이 있고, 팀장 아래에 팀원이 있습니다.

누군가 "개발팀 김 대리"를 찾으면, 먼저 개발 부서를 찾고, 그 안에서 개발팀을, 그 안에서 김 대리를 찾습니다. 계층적 청킹도 마찬가지로 문서를 트리 구조로 만들어 상하 관계를 유지합니다.

왜 계층적 구조가 필요할까요? 기술 문서나 교과서는 명확한 계층 구조를 가집니다.

장이 있고, 장 안에 절이 있고, 절 안에 항이 있습니다. 이런 구조를 무시하고 평평하게 청킹하면 맥락을 잃습니다.

"설치 방법"이라는 절이 1장에도 있고 3장에도 있을 때, 사용자가 "3장의 설치 방법"을 물어보면 구분할 수 없습니다. 계층적 청킹은 어떤 이점을 제공할까요?

첫째, 정밀한 검색이 가능합니다. 상위 레벨에서 큰 주제를 필터링하고, 하위 레벨에서 세부 내용을 찾습니다.

둘째, 컨텍스트 제공이 쉽습니다. 하위 청크를 찾았을 때 상위 청크의 제목을 함께 제공하여 사용자에게 문서 위치를 알려줄 수 있습니다.

셋째, 탐색이 효율적입니다. 필요 없는 장 전체를 건너뛸 수 있기 때문입니다.

코드를 자세히 들여다봅시다. 2번째 줄의 HierarchicalChunk 클래스가 핵심입니다.

각 청크는 자신의 내용, 계층 레벨, 부모 참조, 자식 목록을 가집니다. 이는 전형적인 트리 자료구조입니다.

19번째 줄에서 마크다운 헤더의 '#' 개수로 레벨을 계산합니다. 한 개면 1레벨(장), 두 개면 2레벨(절)입니다.

24번째 줄에서 새 청크를 생성하고 부모의 children 리스트에 추가합니다. 실제 프로덕션에서는 어떻게 사용될까요?

대학교 강의 자료 검색 시스템을 예로 들어봅시다. "운영체제" 과목의 강의 노트는 여러 장으로 구성되고, 각 장은 여러 절로 나뉩니다.

학생이 "4장 2절 페이지 교체 알고리즘"을 검색하면, 시스템은 먼저 4장을 찾고, 그 안에서 2절을 찾고, 페이지 교체 알고리즘 내용을 반환합니다. 이때 "4장: 가상 메모리 > 2절: 페이지 교체"처럼 경로를 함께 보여줄 수 있습니다.

벡터 데이터베이스에 어떻게 저장할까요? 각 청크를 임베딩할 때 메타데이터에 계층 정보를 함께 저장합니다.

chapter, section, subsection 같은 필드를 추가합니다. 검색할 때는 먼저 벡터 유사도로 후보를 찾고, 메타데이터로 필터링하여 정확한 위치의 청크를 선택합니다.

예를 들어 "3장"이라는 키워드가 있으면 chapter=3인 청크만 검색 대상으로 삼습니다. 주의할 점도 있습니다.

계층적 청킹은 문서에 명확한 계층 구조가 있을 때만 효과적입니다. 블로그 포스트나 뉴스 기사처럼 평평한 구조의 문서에는 적합하지 않습니다.

또한 구현 복잡도가 높습니다. 트리 구조를 관리하고, 검색 로직도 더 복잡해집니다.

마지막으로 스토리지 오버헤드가 있습니다. 메타데이터가 추가되어 저장 공간이 더 필요합니다.

이민수 씨는 제품 매뉴얼을 계층적으로 청킹했습니다. 각 장과 절의 구조를 그대로 유지했습니다.

테스트 결과, 사용자들이 "3장 2절"처럼 구체적으로 질문해도 정확한 답을 얻을 수 있었습니다. "이제 완벽해졌어요!" 계층적 청킹은 구조화된 긴 문서에 이상적인 방법입니다.

교과서, 매뉴얼, 법률 문서 등에 적용해보세요.

실전 팁

💡 - 메타데이터 검색을 지원하는 벡터 DB(Pinecone, Weaviate)를 사용하면 구현이 쉽습니다

  • 각 레벨별로 별도의 인덱스를 만들면 검색 성능이 향상됩니다

5. 청크 오버랩 설정

시맨틱 청킹을 적용한 후에도 이민수 씨는 간혹 이상한 답변을 발견했습니다. 문단 경계에 걸친 질문을 할 때 답을 제대로 찾지 못하는 것이었습니다.

"청크 사이에 뭔가 놓치는 게 있는 것 같아요."

청크 오버랩은 인접한 청크 사이에 일부 내용을 중복시키는 기법입니다. 마치 지도책에서 인접한 페이지가 약간씩 겹치는 것과 같습니다.

청크 경계에서 발생하는 문맥 손실을 방지하고 검색 품질을 높입니다.

다음 코드를 살펴봅시다.

def chunk_with_overlap(text, chunk_size=500, overlap_size=100):
    # 문장 단위로 분리
    sentences = text.split('. ')
    chunks = []
    current_chunk = ""
    overlap_buffer = ""

    for sentence in sentences:
        sentence = sentence.strip() + '. '

        # 청크 크기 확인
        if len(current_chunk) + len(sentence) < chunk_size:
            current_chunk += sentence
        else:
            # 현재 청크 저장
            chunks.append(current_chunk.strip())

            # 오버랩: 마지막 overlap_size만큼을 다음 청크 시작에 포함
            words = current_chunk.split()
            overlap_buffer = ' '.join(words[-overlap_size:])
            current_chunk = overlap_buffer + sentence

    if current_chunk:
        chunks.append(current_chunk.strip())

    return chunks

# 테스트
text = "첫 번째 문장입니다. 두 번째 문장입니다. 세 번째 문장입니다. 네 번째 문장입니다."
chunks = chunk_with_overlap(text, chunk_size=50, overlap_size=10)

이민수 씨는 이상한 버그를 발견했습니다. 사용자가 "제품의 보증 기간과 A/S 센터"에 대해 물어봤을 때, 시스템은 보증 기간만 답하고 A/S 센터는 언급하지 않았습니다.

문서를 확인해보니 두 정보가 서로 다른 청크에 나뉘어 있었습니다. 박시니어 씨가 설명했습니다.

"청크 경계 문제네요. 오버랩을 설정하면 해결됩니다." 청크 오버랩이란 무엇일까요?

비유하자면, 청크 오버랩은 마치 릴레이 경주의 배턴 터치 구간과 같습니다. 두 주자가 잠깐 함께 달리는 구간이 있습니다.

이 구간에서 배턴을 안전하게 전달합니다. 만약 이 구간이 없으면 배턴이 떨어질 위험이 큽니다.

청크 오버랩도 마찬가지로 두 청크가 약간 겹치는 영역을 만들어, 청크 경계에서 정보가 손실되는 것을 방지합니다. 청크 경계 문제가 정확히 무엇일까요?

예를 들어 이런 텍스트가 있다고 합시다. "우리 제품의 보증 기간은 2년입니다.

A/S 센터는 서울과 부산에 있습니다." 만약 첫 문장이 청크1에, 두 번째 문장이 청크2에 속한다면 문제가 생깁니다. 사용자가 "보증과 A/S 센터"를 함께 물어보면 벡터 검색은 둘 중 하나만 찾을 가능성이 높습니다.

왜냐하면 질의 벡터가 두 청크 사이 어딘가에 있기 때문입니다. 오버랩은 이 문제를 어떻게 해결할까요?

오버랩을 50자로 설정하면, 청크1의 마지막 50자가 청크2의 시작 부분에도 포함됩니다. 그러면 "보증 기간은 2년입니다"라는 문장이 양쪽 청크에 모두 들어갑니다.

이제 어느 청크를 검색하든 보증 기간 정보를 얻을 수 있습니다. 정보의 연속성이 보장되는 것입니다.

코드를 단계별로 살펴봅시다. 3번째 줄에서 텍스트를 문장 단위로 분리합니다.

마침표를 기준으로 나눕니다. 6번째 줄의 overlap_buffer가 핵심입니다.

이전 청크의 마지막 부분을 임시로 저장합니다. 20번째 줄에서 current_chunk의 마지막 overlap_size개 단어를 추출합니다.

21번째 줄에서 이 오버랩 버퍼가 다음 청크의 시작이 됩니다. 이렇게 하면 청크 간 자연스러운 연결이 만들어집니다.

최적의 오버랩 크기는 얼마일까요? 일반적으로 청크 크기의 **10-20%**가 적당합니다.

청크가 500자라면 오버랩은 50-100자 정도입니다. 너무 작으면 효과가 없고, 너무 크면 저장 공간 낭비와 중복 검색 문제가 생깁니다.

실험을 통해 최적값을 찾는 것이 좋습니다. 실무에서는 어떻게 적용될까요?

고객 지원 FAQ 시스템을 예로 들어봅시다. "환불 정책과 교환 절차"처럼 연관된 두 주제를 함께 물어보는 경우가 많습니다.

오버랩이 없으면 환불 정책만 답하거나 교환 절차만 답할 수 있습니다. 하지만 오버랩을 설정하면 두 정보가 하나의 청크에 함께 포함될 가능성이 높아집니다.

결과적으로 사용자는 더 완전한 답변을 받습니다. 토큰 기반 오버랩도 가능합니다.

문자나 단어 대신 토큰 수로 오버랩을 계산할 수 있습니다. LLM은 토큰 단위로 작동하므로 토큰 기반이 더 정확합니다.

tiktoken 같은 라이브러리를 사용하여 텍스트를 토큰으로 변환하고, 마지막 N개 토큰을 오버랩으로 설정합니다. 이 방법은 특히 다국어 문서에서 효과적입니다.

주의할 점이 있습니다. 오버랩이 크면 스토리지 비용이 증가합니다.

내용이 중복되므로 벡터 데이터베이스의 크기가 커집니다. 또한 검색 시 중복 결과가 나올 수 있습니다.

같은 내용이 여러 청크에 있으면 검색 결과에 중복이 나타납니다. 후처리에서 이를 제거하는 로직이 필요합니다.

이민수 씨는 100자 오버랩을 설정했습니다. 청크 크기는 500자로 유지했습니다.

다시 테스트해보니 청크 경계 문제가 대부분 해결되었습니다. "이제 복합적인 질문에도 잘 답하네요!" 청크 오버랩은 간단하지만 효과적인 기법입니다.

대부분의 RAG 시스템에서 기본으로 사용하세요.

실전 팁

💡 - 오버랩은 문자 수보다 토큰 수 기반이 더 정확합니다

  • 벡터 DB에 중복 제거 로직을 추가하여 검색 결과를 정리하세요

6. 청킹 전략 선택 기준

여러 청킹 방법을 모두 배운 이민수 씨는 혼란스러워졌습니다. "프로젝트마다 어떤 방법을 써야 할까요?" 박시니어 씨는 화이트보드에 표를 그리기 시작했습니다.

청킹 전략 선택은 문서 특성, 사용 패턴, 성능 요구사항을 고려해야 합니다. 마치 운송 수단을 선택하듯이, 상황에 맞는 최적의 방법을 골라야 합니다.

잘못된 선택은 검색 품질과 비용에 큰 영향을 미칩니다.

다음 코드를 살펴봅시다.

class ChunkingStrategy:
    @staticmethod
    def choose_strategy(doc_type, has_structure, search_type):
        """
        문서 특성에 따라 최적의 청킹 전략을 추천합니다
        """
        # 계층 구조가 명확한 문서
        if has_structure and doc_type in ['manual', 'textbook', 'legal']:
            return 'hierarchical'

        # 구조화된 일반 문서
        elif has_structure:
            return 'semantic'

        # 정밀 검색이 필요한 경우
        elif search_type == 'precise':
            return 'semantic_with_overlap'

        # 빠른 처리가 우선인 경우
        elif search_type == 'fast':
            return 'fixed_size'

        # 기본값
        else:
            return 'semantic_with_overlap'

# 사용 예제
strategy = ChunkingStrategy.choose_strategy(
    doc_type='manual',
    has_structure=True,
    search_type='precise'
)
print(f"추천 전략: {strategy}")

이민수 씨의 팀은 새로운 프로젝트를 시작했습니다. 이번에는 의료 문서 검색 시스템이었습니다.

이전에 사용했던 시맨틱 청킹을 그대로 쓸까 고민했습니다. 하지만 박시니어 씨는 고개를 저었습니다.

"의료 문서는 특성이 다릅니다. 전략을 다시 생각해봐야 해요." 어떻게 적절한 청킹 전략을 선택할까요?

비유하자면, 청킹 전략 선택은 마치 여행 계획을 세우는 것과 같습니다. 서울에서 부산 가는데 비행기, 기차, 버스, 자동차 중 무엇을 탈지 정해야 합니다.

시간이 중요하면 비행기, 비용이 중요하면 버스, 편안함이 중요하면 기차를 선택합니다. 청킹도 마찬가지로 여러 요소를 고려해야 합니다.

첫 번째 기준은 문서 구조입니다. 문서에 명확한 계층 구조가 있나요?

매뉴얼, 교과서, 법률 문서는 장-절-항 구조를 가집니다. 이런 경우 계층적 청킹이 최선입니다.

사용자가 "3장 2절"처럼 구체적으로 질문할 때 정확한 답을 줄 수 있습니다. 반면 블로그 포스트나 뉴스 기사는 평평한 구조입니다.

이때는 시맨틱 청킹이 적합합니다. 두 번째 기준은 검색 패턴입니다.

사용자가 어떤 질문을 할까요? 정밀한 검색이 필요한가요, 아니면 대략적인 답변으로 충분한가요?

의료 문서처럼 정확성이 중요하면 시맨틱 청킹 + 오버랩을 사용하세요. 조금의 정보 손실도 치명적일 수 있기 때문입니다.

반면 제품 추천 시스템처럼 대략적인 매칭으로 충분하면 고정 크기 청킹도 괜찮습니다. 세 번째 기준은 성능 요구사항입니다.

실시간 처리가 필요한가요? 고정 크기 청킹은 가장 빠릅니다.

복잡한 파싱이 필요 없기 때문입니다. 반면 계층적 청킹은 느립니다.

문서 구조를 분석하고 트리를 구성하는 시간이 필요합니다. 만약 문서가 자주 업데이트되지 않는다면 처리 속도는 크게 중요하지 않습니다.

한 번 청킹하고 오래 사용하면 됩니다. 네 번째 기준은 비용입니다.

스토리지 비용과 임베딩 비용을 고려해야 합니다. 오버랩이 크면 청크 개수가 늘어나고, 각 청크마다 임베딩 API를 호출해야 합니다.

OpenAI 임베딩은 호출마다 비용이 발생합니다. 예산이 제한적이면 오버랩을 최소화하거나 청크 크기를 늘려야 합니다.

실제 사례로 결정 과정을 살펴봅시다. 사례 1: 법률 문서 검색.

문서는 명확한 조항 구조를 가집니다. 검색은 매우 정밀해야 합니다.

사용자는 "민법 제750조"처럼 구체적으로 질문합니다. 결정: 계층적 청킹 + 작은 오버랩.

이유: 구조 보존이 필수이고, 정확성이 최우선입니다. 사례 2: 블로그 검색 엔진.

문서는 평평한 구조입니다. 검색은 키워드 기반이고 대략적입니다.

트래픽이 많아 빠른 처리가 필요합니다. 결정: 고정 크기 청킹 + 중간 크기 오버랩.

이유: 속도가 중요하고, 대략적인 매칭으로 충분합니다. 사례 3: 의료 문서 검색.

문서는 섹션별로 구조화되어 있습니다. 검색 정확도가 생명과 직결됩니다.

결정: 시맨틱 청킹 + 큰 오버랩. 이유: 정보 손실을 절대 허용할 수 없고, 비용보다 정확성이 우선입니다.

사례 4: 제품 카탈로그. 각 제품 설명이 독립적입니다.

검색은 단일 제품을 찾는 것입니다. 결정: 제품 단위 청킹(시맨틱) + 오버랩 없음.

이유: 각 항목이 이미 독립적이므로 오버랩 불필요합니다. 코드의 choose_strategy 함수를 봅시다.

이 함수는 세 가지 입력을 받습니다. 문서 타입, 구조 유무, 검색 타입입니다.

8번째 줄에서 먼저 구조가 있고 특정 타입인지 확인합니다. 해당되면 계층적 청킹을 반환합니다.

12번째 줄에서 일반 구조화 문서는 시맨틱 청킹으로 처리합니다. 이런 결정 트리를 프로젝트에 맞게 확장할 수 있습니다.

하이브리드 전략도 가능합니다. 한 시스템에서 여러 전략을 섞어 쓸 수 있습니다.

예를 들어 메인 콘텐츠는 시맨틱 청킹으로, FAQ는 질문 단위로, 로그는 고정 크기로 청킹합니다. 검색할 때 문서 타입에 따라 다른 인덱스를 사용합니다.

이렇게 하면 각 문서 타입에 최적화된 검색을 제공할 수 있습니다. 이민수 씨는 의료 문서에 시맨틱 청킹을 선택했습니다.

오버랩은 150자로 설정했습니다. 파일럿 테스트를 했더니 검색 정확도가 95%를 넘었습니다.

의료진들도 만족했습니다. "올바른 전략을 선택하는 게 이렇게 중요하네요." 박시니어 씨가 웃으며 말했습니다.

"정답은 없어요. 상황에 맞는 최선을 찾는 게 엔지니어의 일이죠." 여러분도 프로젝트의 특성을 분석하고, 여러 전략을 실험해보세요.

데이터가 답을 알려줄 겁니다.

실전 팁

💡 - 처음에는 시맨틱 청킹 + 중간 오버랩으로 시작하세요(대부분의 경우에 잘 작동)

  • A/B 테스트로 여러 전략을 비교하고 정량적으로 평가하세요
  • 검색 정확도, 응답 시간, 비용을 종합적으로 고려하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#RAG#Chunking#VectorDB#LLM#AWS

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.