안녕하세요. 오늘은 33일차 학습 기록입니다. 👋
지난 시간(32일차)에는 CharacterTextSplitter를 사용하여 문자를 기준으로 문서를 단순하게 분할하는 방법을 배웠습니다.
오늘은 그보다 한 단계 정교한 분할 방식인 RecursiveCharacterTextSplitter와 TokenTextSplitter에 대해 알아보겠습니다.
이 두 분할기는 단순히 문서를 일정 길이로 잘라내는 것을 넘어서,
문맥 손실을 최소화하면서 효율적으로 분할하는 데 초점을 맞추고 있습니다.
즉, 단순 분할이 아니라 ‘LLM이 이해하기 좋은 단위로 문서를 구성하는 과정’이라고 이해하면 됩니다.
1. 문서 분할의 고도화
기존 CharacterTextSplitter는 구분자(\\n\\n)나 문자 개수 기준으로 문서를 단순하게 자릅니다.
하지만 실무에서는 다음과 같은 문제가 자주 발생할 수 있습니다.
- 문단 중간에서 분할되어 의미 단위가 끊김
- 텍스트 구조(문장, 단락, 섹션)를 고려하지 못함
- LLM 토큰 단위와 불일치 → 입력 토큰 초과 오류
이 문제를 해결하기 위해 LangChain은 여러 가지 분할기를 제공합니다.
2. RecursiveCharacterTextSplitter
RecursiveCharacterTextSplitter는 이름 그대로 “재귀적(Recursive)” 접근 방식을 사용합니다.
단순히 문자를 자르는 것이 아니라, 문서 구조를 상위 → 하위 단위로 탐색하면서 자연스럽게 분할합니다.
즉,
“먼저 문단(Paragraph)을 기준으로 자르고,
너무 크면 문장(Sentence) 단위로,
그래도 크면 줄(Line), 단어(Word) 단위로 나누는 방식”
이라고 이해할 수 있습니다.
✅ 핵심 아이디어 요약
- 구분자 목록(separators)을 우선순위에 따라 지정합니다.
- (기본값: ["\\n\\n", "\\n", " ", ""])
- 각 단계에서 청크가 chunk_size를 초과하면 더 작은 단위로 재귀적 분할
- 의미 단위를 최대한 유지하면서, 문서 전체를 고르게 나눕니다.
이 방식은 특히 자연어 문서(기사, 논문, 블로그) 분할에 효과적입니다.
예시 코드
# appendix-keywords.txt 파일을 열어서 f라는 파일 객체를 생성합니다.
with open("./data/appendix-keywords.txt") as f:
file = f.read() # 파일의 내용을 읽어서 file 변수에 저장합니다.
파일로부터 읽은 파일의 일부 내용을 출력합니다.
# 파일으로부터 읽은 내용을 일부 출력합니다.
print(file[:500])
출력 결과:
Semantic Search
정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝
Embedding
정의: 임베딩은 단어나 문장 같은 텍스트 데이터를 저차원의 연속적인 벡터로 변환하는 과정입니다. 이를 통해 컴퓨터가 텍스트를 이해하고 처리할 수 있게 합니다.
예시: "사과"라는 단어를 [0.65, -0.23, 0.17]과 같은 벡터로 표현합니다.
연관키워드: 자연어 처리, 벡터화, 딥러닝
Token
정의: 토큰은 텍스트를 더 작은 단위로 분할하는 것을 의미합니다. 이는 일반적으로 단어, 문장, 또는 구절일 수 있습니다.
예시: 문장 "나는 학교에 간다"를 "나는", "학교에", "간다"로 분할합니다.
연관키워드: 토큰화, 자연어
RecursiveCharacterTextSplitter을 불러옵니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
# 청크 크기를 매우 작게 설정합니다. 예시를 위한 설정입니다.
chunk_size=250,
# 청크 간의 중복되는 문자 수를 설정합니다.
chunk_overlap=50,
# 문자열 길이를 계산하는 함수를 지정합니다.
length_function=len,
# 구분자로 정규식을 사용할지 여부를 설정합니다.
is_separator_regex=False,
)
# text_splitter를 사용하여 file 텍스트를 문서로 분할합니다.
texts = text_splitter.create_documents([file])
print(texts[0]) # 분할된 문서의 첫 번째 문서를 출력합니다.
print("===" * 20)
print(texts[1]) # 분할된 문서의 두 번째 문서를 출력합니다.
출력 결과:
page_content='Semantic Search
정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝
Embedding'
============================================================
page_content='Embedding
정의: 임베딩은 단어나 문장 같은 텍스트 데이터를 저차원의 연속적인 벡터로 변환하는 과정입니다. 이를 통해 컴퓨터가 텍스트를 이해하고 처리할 수 있게 합니다.
예시: "사과"라는 단어를 [0.65, -0.23, 0.17]과 같은 벡터로 표현합니다.
연관키워드: 자연어 처리, 벡터화, 딥러닝
Token'
text_splitter.split_text() 함수를 사용하여 file 텍스트를 분할합니다.
# 텍스트를 분할하고 분할된 텍스트의 처음 2개 요소를 반환합니다.
text_splitter.split_text(file)[0]
text_splitter.split_text(file)[1]
출력 결과:
'Semantic Search\\n\\n정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.\\n예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.\\n연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝\\n\\nEmbedding'
'Embedding\\n\\n정의: 임베딩은 단어나 문장 같은 텍스트 데이터를 저차원의 연속적인 벡터로 변환하는 과정입니다. 이를 통해 컴퓨터가 텍스트를 이해하고 처리할 수 있게 합니다.\\n예시: "사과"라는 단어를 [0.65, -0.23, 0.17]과 같은 벡터로 표현합니다.\\n연관키워드: 자연어 처리, 벡터화, 딥러닝\\n\\nToken'
결과를 보면 문단 단위로 자연스럽게 잘려 있음을 알 수 있습니다. 즉, ‘Semantic Search’ 단락이 하나의 청크로 묶이고, 그다음 단락 ‘Embedding’이 다음 청크로 이어지는 형태입니다.
💡 chunk_overlap의 역할
청크를 분할할 때 문맥의 연속성을 유지하기 위해 일부 텍스트를 겹쳐서 포함시킬 수 있습니다.
예를 들어, chunk_overlap=50이면 앞 청크의 마지막 50문자가 다음 청크의 시작 부분에 포함됩니다.
이렇게 하면 LLM이 여러 청크를 개별적으로 처리할 때 중간 문맥이 끊기지 않게 연결할 수 있습니다.
정리
RecursiveCharacterTextSplitter는 단순한 문자 단위가 아닌,
문단 → 문장 → 단어 순으로 점진적으로 나누는 구조적 분할 방식입니다.
| 항목 | 설명 |
| 분할 기준 | 문단 → 문장 → 단어 (재귀적) |
| 측정 단위 | 문자 수(len) |
| 장점 | 자연스러운 분할, 문맥 유지 |
| 추천 용도 | 기사, 리포트, 일반 텍스트 |
3. TokenTextSplitter
어 모델은 입력 토큰 한도(예: 4k, 8k, 32k, 200k 등)가 있습니다.
문자 수 기준 분할은 토큰 길이와 불일치할 수 있기 때문에, 토큰 단위로 정확히 분할하고 싶을 때 TokenTextSplitter가 유용합니다.
핵심 개념
- 분할 기준: “문자”가 아니라 토큰 수
- 목적: 모델별 컨텍스트 한도 준수 + 비용/속도 예측 용이
- 권장 시나리오: 논문/리포트/긴 매뉴얼처럼 길이가 긴 문서, 다국어 혼용 텍스트
tiktoken
tiktoken은 OpenAI의 빠른 BPE 토크나이저로, 토큰 길이를 정확히 계산하는 데 쓰입니다.
LangChain은 CharacterTextSplitter/RecursiveCharacterTextSplitter에 tiktoken 기반 길이 함수를 장착하는 헬퍼를 제공합니다.
# data/appendix-keywords.txt 파일을 열어서 f라는 파일 객체를 생성합니다.
with open("./data/appendix-keywords.txt") as f:
file = f.read() # 파일의 내용을 읽어서 file 변수에 저장합니다.
파일로부터 읽은 파일의 일부 내용을 출력합니다.
# 파일으로부터 읽은 내용을 일부 출력합니다.
print(file[:500])
출력 결과:
Semantic Search
정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝
Embedding
정의: 임베딩은 단어나 문장 같은 텍스트 데이터를 저차원의 연속적인 벡터로 변환하는 과정입니다. 이를 통해 컴퓨터가 텍스트를 이해하고 처리할 수 있게 합니다.
예시: "사과"라는 단어를 [0.65, -0.23, 0.17]과 같은 벡터로 표현합니다.
연관키워드: 자연어 처리, 벡터화, 딥러닝
Token
정의: 토큰은 텍스트를 더 작은 단위로 분할하는 것을 의미합니다. 이는 일반적으로 단어, 문장, 또는 구절일 수 있습니다.
예시: 문장 "나는 학교에 간다"를 "나는", "학교에", "간다"로 분할합니다.
연관키워드: 토큰화, 자연어
(A) 문자 분할 + tiktoken 길이 측정 (경고가 날 수 있음)
from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
chunk_size=250,
chunk_overlap=0,
)
texts = text_splitter.split_text(file)
출력 결과:
Created a chunk of size 358, which is longer than the specified 250
Created a chunk of size 315, which is longer than the specified 250
Created a chunk of size 275, which is longer than the specified 250
Created a chunk of size 267, which is longer than the specified 250
Created a chunk of size 305, which is longer than the specified 250
Created a chunk of size 288, which is longer than the specified 250
Created a chunk of size 366, which is longer than the specified 250
Created a chunk of size 276, which is longer than the specified 250
Created a chunk of size 330, which is longer than the specified 250
Created a chunk of size 351, which is longer than the specified 250
Created a chunk of size 378, which is longer than the specified 250
Created a chunk of size 361, which is longer than the specified 250
Created a chunk of size 350, which is longer than the specified 250
Created a chunk of size 285, which is longer than the specified 250
Created a chunk of size 362, which is longer than the specified 250
Created a chunk of size 335, which is longer than the specified 250
Created a chunk of size 353, which is longer than the specified 250
Created a chunk of size 358, which is longer than the specified 250
Created a chunk of size 336, which is longer than the specified 250
Created a chunk of size 324, which is longer than the specified 250
Created a chunk of size 337, which is longer than the specified 250
Created a chunk of size 307, which is longer than the specified 250
Created a chunk of size 361, which is longer than the specified 250
Created a chunk of size 354, which is longer than the specified 250
Created a chunk of size 378, which is longer than the specified 250
Created a chunk of size 381, which is longer than the specified 250
Created a chunk of size 365, which is longer than the specified 250
Created a chunk of size 377, which is longer than the specified 250
Created a chunk of size 329, which is longer than the specified 250
출력에 Created a chunk of size XXX, which is longer than the specified 250같은 로그가 보일 수 있습니다. 이 구성은 “문자” 기준으로 자르고, tiktoken은 길이를 재고 병합하는 데만 쓰이기 때문에 토큰 길이를 100% 보장하지 않습니다. → 토큰 한도를 엄격히 지켜야 한다면 아래 (B)를 권장합니다.
(B) 재귀 분할 + tiktoken 길이 보장 (권장)
from langchain_text_splitters import RecursiveCharacterTextSplitter
rc_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=250, # 토큰 기준
chunk_overlap=30, # 토큰 기준 오버랩
)
token_safe_docs = rc_splitter.create_documents([file])
print(len(token_safe_docs))
print(token_safe_docs[0].page_content[:300])
- RecursiveCharacterTextSplitter.from_tiktoken_encoder는 토큰 한도를 넘는 청크를 재귀적으로 더 잘게 쪼개므로, 토큰 크기 보장에 유리합니다.
TokenTextSplitter
가장 직접적으로 토큰 단위로 자르고 싶다면 TokenTextSplitter를 사용하면 됩니다.
from langchain_text_splitters import TokenTextSplitter
text_splitter = TokenTextSplitter(
chunk_size=300, # 청크 크기를 10으로 설정합니다.
chunk_overlap=0, # 청크 간 중복을 0으로 설정합니다.
)
# state_of_the_union 텍스트를 청크로 분할합니다.
texts = text_splitter.split_text(file)
print(texts[0]) # 분할된 텍스트의 첫 번째 청크를 출력합니다.
출력 결과:
Semantic Search
정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연�
출력에서 간혹 …연�처럼 보이는 깨진 글자가 보일 수 있습니다.
이는 콘솔 미리보기에서 문자열을 임의 길이로 슬라이스 할 때 렌더링 이슈가 생기거나, 특정 폰트/환경에서 유니코드 출력이 매끄럽지 않은 경우 발생합니다.
일반적으로 분할 결과 문자열 자체는 유효한 UTF-8이며, 검색·임베딩에는 문제 없습니다.
SpaCy
SpacyTextSplitter는 spaCy 토크나이저/문장분리기를 활용해 문장 경계를 최대한 깔끔히 유지합니다.
문장 단위 가독성을 중시하거나, 한/영 혼합 텍스트의 문장 경계를 잘 살리고 싶은 경우 유용합니다.
기준 단위: spaCy 문장 경계
길이 측정: 문자 수 (→ 토큰 한도 보장은 아님. 필요하면 3.4처럼 사전 검증 권장)
# data/appendix-keywords.txt 파일을 열어서 f라는 파일 객체를 생성합니다.
with open("./data/appendix-keywords.txt") as f:
file = f.read() # 파일의 내용을 읽어서 file 변수에 저장합니다.
# 파일으로부터 읽은 내용을 일부 출력합니다.
print(file[:350])
출력 결과:
Semantic Search
정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된 결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
연관키워드: 자연어 처리, 검색 알고리즘, 데이터 마이닝
Embedding
정의: 임베딩은 단어나 문장 같은 텍스트 데이터를 저차원의 연속적인 벡터로 변환하는 과정입니다. 이를 통해 컴퓨터가 텍스트를 이해하고 처리할 수 있게 합니다.
예시: "사과"라는 단어를 [0.65, -0.23, 0.17]과 같은 벡터로 표현합니다.
연관키워드: 자연어 처
import warnings
from langchain_text_splitters import SpacyTextSplitter
# 경고 메시지를 무시합니다.
warnings.filterwarnings("ignore")
# SpacyTextSplitter를 생성합니다.
text_splitter = SpacyTextSplitter(
chunk_size=200, # 청크 크기를 200으로 설정합니다.
chunk_overlap=50, # 청크 간 중복을 50으로 설정합니다.
)
# text_splitter를 사용하여 file 텍스트를 분할합니다.
texts = text_splitter.split_text(file)
print(texts[0]) # 분할된 텍스트의 첫 번째 요소를 출력합니다.
출력 결과:
Semantic Search
정의: 의미론적 검색은 사용자의 질의를 단순한 키워드 매칭을 넘어서 그 의미를 파악하여 관련된
결과를 반환하는 검색 방식입니다.
예시: 사용자가 "태양계 행성"이라고 검색하면, "목성", "화성" 등과 같이 관련된 행성에 대한 정보를 반환합니다.
(추가) 기타 분할기 옵션 정리
NLTK / KoNLPy / Hugging Face Tokenizers 등도 상황에 따라 유용합니다.
- NLTK
- 장점: 간단한 영어 문장/문단 분할에 적합, 의존성 가벼움
- 단점: 한국어·다국어 혼용에서는 문장 경계 정확도가 낮을 수 있음
- 쓰임새: 빠르게 프로토타이핑할 때
- KoNLPy (한국어)
- 장점: 형태소 분석(명사/조사/어근) 기반 전처리, 한국어 문장 경계/어절 처리에 강점
- 단점: 의존성(자바/사전)과 설치 부담, 대용량 처리 속도 이슈
- 쓰임새: 한국어 중심 서비스에서 의미 단위를 더욱 촘촘히 자르고 싶을 때
- Hugging Face Tokenizers (BPE/WordPiece 등)
- 장점: 모델별 토크나이저와 정확히 일치하는 분할 가능(예: bert-base-multilingual-cased)
- 단점: 설정 폭이 넓어 러닝커브, 토큰 기준으로 자르면 문장 경계 왜곡 가능
- 쓰임새: 모델 컨텍스트 한도 준수가 최우선이고, 해당 모델 토크나이저를 그대로 쓰고 싶을 때
마무리
오늘은 토큰·문장 구조를 모두 고려한 고급 분할 전략을 정리했습니다.
- RecursiveCharacterTextSplitter: 문단→문장→단어 재귀 분할로 문맥 보존에 최적
- TokenTextSplitter: 토큰 기준 분할로 모델 컨텍스트 한도를 정확히 준수
- Spacy/KoNLPy/HF Tokenizers: 언어·도메인·모델 특성에 맞춘 맞춤형 분할에 유용
읽어주셔서 감사합니다.
