-
벡터 DB에 넣기 전, 문서 전처리 체크리스트IT 2026. 3. 24. 21:00
청킹 전에 전처리를 안 하면 생기는 일
지난 글에서 벡터 DB에 "무엇을" 넣을지 정하는 법을 다뤘어요. 이번엔 그다음 단계입니다: "어떻게" 넣을 것인가.
많은 분들이 문서를 바로 청킹(chunking)해서 벡터 DB에 넣으려 하는데, 그 사이에 빠진 단계가 있어요. 바로 전처리(preprocessing)입니다.
전처리 없이 그냥 넣으면 어떤 일이 벌어질까요?
- API 레퍼런스의 파라미터 테이블이 깨져서, 검색해도 "이 파라미터가 필수인지 선택인지" 알 수 없게 됩니다
- 가이드 문서의 제목 계층이 사라져서, "인증의 토큰 갱신"인지 "캐시 만료"인지 맥락을 잃습니다
- 한영 혼합 문장에서
"embedding모델을설치"같은 띄어쓰기 없는 구간이 통째로 하나의 토큰이 되어 검색에 걸리지 않습니다
Garbage In, Garbage Out — 임베딩 모델이 아무리 좋아도 입력이 지저분하면 검색 품질은 떨어집니다.
실전 시나리오: 이런 경험 있으시죠?
시나리오 1: API 문서를 넣었는데 파라미터를 못 찾는다
개발팀에서 API 레퍼런스를 벡터 DB에 넣었는데, "이 API의 필수 파라미터가 뭐야?"라고 물으면 엉뚱한 답이 나옵니다. 마크다운 테이블이 깨져서 파라미터 이름과 설명이 분리된 채 임베딩됐거든요.
시나리오 2: 가이드 문서에서 맥락이 사라졌다
"인증 방법"을 검색했는데, "토큰이 만료되면 갱신합니다"라는 청크만 덩그러니 나옵니다. 이게 인증 토큰인지, 세션 토큰인지, 캐시 만료인지 알 수 없어요. 가이드의 제목 계층(인증 → 토큰 갱신)이 청킹 과정에서 사라졌기 때문입니다.
전처리 체크리스트: 모든 문서에 공통
어떤 종류의 문서든 벡터 DB에 넣기 전에 거쳐야 할 공통 전처리 단계가 있어요. 체크리스트 형태로 정리했습니다.
1. 텍스트 정규화 (Text Normalization)
이 단계를 하는 이유: 같은 의미인데 표기가 달라서 검색에 걸리지 않는 경우를 방지합니다.
항목 처리 전 처리 후 왜 필요한가 유니코드 정규화 café(NFD)café(NFC)같은 글자인데 바이트가 달라서 매칭 실패 연속 공백 제거 hello worldhello world불필요한 공백이 토큰을 낭비 불필요한 제어문자 \x00,\x0b(제거) 임베딩 모델이 처리하지 못하는 문자 한영 경계 띄어쓰기 embedding모델을설치embedding 모델을 설치토크나이저가 합쳐진 한영 문자열을 제대로 분리 못함 파이썬으로 핵심 부분만 보면 이렇습니다:
import unicodedata import re def normalize_text(text: str) -> str: # 1) 유니코드 NFC 정규화 text = unicodedata.normalize("NFC", text) # 2) 제어문자 제거 (개행·탭은 유지) text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text) # 3) 연속 공백 → 단일 공백 text = re.sub(r"[^\S\n]+", " ", text) # 4) 한영 경계에 공백 삽입 text = re.sub(r"([가-힣])([a-zA-Z])", r"\1 \2", text) text = re.sub(r"([a-zA-Z])([가-힣])", r"\1 \2", text) return text.strip()4번이 특히 중요합니다. 한국어와 영어가 섞인 기술 문서에서는
"Docker컨테이너를시작"같은 표기가 자주 등장하는데, 임베딩 모델의 토크나이저 입장에서는 이게 하나의 거대한 미지의 토큰이 됩니다. 띄어쓰기 하나로 검색 품질이 크게 달라져요.2. 메타데이터 분리
이 단계를 하는 이유: 메타데이터가 본문에 섞이면 검색 결과를 오염시킵니다.
YAML frontmatter, HTML head 태그 등의 메타데이터는 본문과 분리해서 따로 저장하는 게 좋습니다. 메타데이터는 필터링 용도로 쓰고, 임베딩은 본문에만 적용하세요.
import yaml def split_frontmatter(content: str) -> tuple[dict, str]: """YAML frontmatter를 본문과 분리""" if content.startswith("---"): parts = content.split("---", 2) if len(parts) >= 3: meta = yaml.safe_load(parts[1]) or {} body = parts[2].strip() return meta, body return {}, content분리한 메타데이터(태그, 날짜, 작성자 등)는 벡터 DB에 필터 필드로 저장하면, "2026년에 작성한 RAG 관련 노트"처럼 메타데이터 필터 + 의미 검색을 조합할 수 있습니다.
3. 마크업 제거 vs 구조 보존
이 단계를 하는 이유: HTML 태그나 마크다운 기호가 임베딩에 포함되면 의미 검색 품질이 떨어집니다. 단, 구조 정보는 보존해야 할 때도 있습니다.
상황 전략 이유 일반 텍스트 검색 마크업 완전 제거 <strong>,**등은 의미에 기여하지 않음테이블 데이터 구조화된 텍스트로 변환 행-열 관계를 유지해야 검색 가능 제목 계층 제목을 청크 메타데이터로 보존 어떤 섹션의 내용인지 컨텍스트 유지 4. 중복 및 노이즈 제거
이 단계를 하는 이유: 중복 콘텐츠가 벡터 DB에 들어가면 검색 결과 상위를 같은 내용이 차지합니다.
- 정확히 같은 문서: 해시(hash) 비교로 제거
- 거의 같은 문서: MinHash 등 유사도 기반 중복 탐지
- 반복 패턴: 헤더/푸터, 저작권 고지, 네비게이션 메뉴 등 모든 페이지에 반복되는 텍스트 제거
import hashlib def content_hash(text: str) -> str: """정규화 후 해시 → 정확 중복 탐지""" normalized = re.sub(r"\s+", " ", text.lower().strip()) return hashlib.sha256(normalized.encode()).hexdigest()개발 문서 전처리: API 레퍼런스, 가이드, 샘플 코드
개발자용 문서는 일반 글과 구조가 매우 다릅니다. API 레퍼런스, 가이드, 샘플 코드 각각 전처리 전략이 다릅니다.
API 레퍼런스
문제: API 문서에는 파라미터 정보가 마크다운 테이블로 정리되어 있는 경우가 많아요. 예를 들어 이런 식이죠:
| 파라미터 | 타입 | 필수 여부 | 설명 | |----------|--------|-----------|------------| | name | string | required | 사용자 이름 |이 테이블이 청킹되면
| name | string | required | 사용자 이름 |이 한 줄의 텍스트가 됩니다. 임베딩 모델 입장에서는 파이프(|) 기호로 구분된 텍스트 조각일 뿐이에요."name 파라미터가 필수인가요?"라고 질문해도, 테이블 형식의 텍스트와는 의미적 유사도가 낮게 나옵니다.목적: 테이블의 각 행을 자연어 문장으로 변환합니다. 헤더(열 이름)와 셀 값을 조합해서 "파라미터: name, 타입: string, 필수 여부: required, 설명: 사용자 이름"처럼 읽을 수 있는 형태로 만들면, 자연어 질문과의 의미적 매칭이 훨씬 잘 됩니다.
def preprocess_api_doc(text: str) -> str: """API 문서의 테이블 구조를 검색 가능한 텍스트로 변환""" # 마크다운 테이블 → 자연어 설명 # | name | string | required | 사용자 이름 | # → "파라미터 name (타입: string, 필수): 사용자 이름" lines = text.split("\n") result = [] in_table = False headers = [] for line in lines: if "|" in line and not line.strip().startswith("|--"): cells = [c.strip() for c in line.strip("|").split("|")] if not in_table: headers = cells in_table = True else: # 헤더와 셀을 조합해 자연어 문장 생성 desc = ", ".join(f"{h}: {c}" for h, c in zip(headers, cells) if c) result.append(desc) else: if in_table: in_table = False headers = [] result.append(line) return "\n".join(result)이렇게 하면
"name 파라미터가 필수인가요?"라는 질문에 대해"파라미터 name (타입: string, 필수)"라는 텍스트가 매칭될 수 있습니다.가이드 문서
문제: 가이드 문서는 보통 계층적 구조를 갖고 있어요.
## 인증아래에### 토큰 갱신이 있고, 그 안에 "액세스 토큰이 만료되면 리프레시 토큰으로 갱신합니다"라는 본문이 있는 식이죠. 그런데 청킹을 하면 이 본문만 떨어져 나옵니다. "액세스 토큰이 만료되면..."이라는 텍스트만으로는 이게 인증에 관한 건지, 세션 관리에 관한 건지, 캐시 만료에 관한 건지 알 수 없어요.목적: 각 텍스트 조각에 "이 내용이 어떤 제목 아래에 있었는지"를 메타데이터로 붙여줍니다. "인증 > 토큰 갱신"이라는 컨텍스트가 함께 저장되면, "인증 관련 토큰 처리 방법"이라는 질문에 정확히 매칭됩니다.
def extract_heading_context(text: str) -> list[dict]: """각 섹션에 상위 제목 계층을 메타데이터로 부착""" sections = [] heading_stack = {} # level → title for line in text.split("\n"): match = re.match(r"^(#{1,6})\s+(.+)$", line) if match: level = len(match.group(1)) title = match.group(2) heading_stack[level] = title # 하위 제목 초기화 for l in list(heading_stack): if l > level: del heading_stack[l] else: if line.strip(): context = " > ".join( heading_stack[l] for l in sorted(heading_stack) ) sections.append({ "text": line, "heading_context": context }) return sections이 함수의 출력 예시:
# 입력: # ## 인증 # ### 토큰 갱신 # 액세스 토큰이 만료되면 리프레시 토큰으로 갱신합니다. # 출력: { "text": "액세스 토큰이 만료되면 리프레시 토큰으로 갱신합니다.", "heading_context": "인증 > 토큰 갱신" }heading_context를 청크의 메타데이터로 저장하거나, 청크 텍스트 앞에 붙여주면 검색 정확도가 크게 올라갑니다.샘플 코드
문제: 개발 문서에는 코드 예시가 많이 포함되어 있는데, 이 코드를 어떻게 처리할지가 애매합니다. 코드를 그대로 두면
import os,for i in range같은 프로그래밍 구문이 의미 검색을 방해할 수 있어요. 반대로 코드를 전부 제거하면 "이 API를 파이썬에서 어떻게 호출하지?"라는 질문에 답할 수 없게 됩니다.목적: 시스템의 용도에 따라 코드 블록을 다르게 처리합니다. 아래 세 가지 전략 중 상황에 맞는 걸 선택하세요:
전략 언제 사용 방법 코드 제거 코드 검색이 목적이 아닐 때 코드 블록 전체를 [코드 예시]로 대체코드 보존 코드 검색이 핵심 목적일 때 코드 블록을 그대로 유지하되 주석을 별도 청크로 코드 + 설명 결합 코드와 설명이 함께 필요할 때 코드 바로 위/아래 설명과 함께 하나의 청크로 def handle_code_blocks(text: str, strategy: str = "keep") -> str: """코드 블록 처리 전략 적용""" if strategy == "remove": # 코드 블록을 플레이스홀더로 대체 return re.sub( r"```[\w]*\n[\s\S]*?```", "[코드 예시 생략]", text ) elif strategy == "keep": # 코드는 유지하되, 언어 힌트를 텍스트로 추가 def add_lang_hint(m): lang = m.group(1) or "code" return f"\n({lang} 코드 예시)\n{m.group(0)}" return re.sub(r"```(\w*)\n", add_lang_hint, text) return text전체 전처리 파이프라인 한눈에 보기
지금까지 다룬 내용을 체크리스트로 정리하면 이렇습니다:
단계 체크 항목 공통 API/개발 문서 추가 1 유니코드 정규화 (NFC) O 2 제어문자 제거 O 3 연속 공백 정리 O 4 한영 경계 띄어쓰기 O 5 메타데이터(frontmatter) 분리 O 6 중복 문서 제거 O 7 마크업 제거 / 구조 보존 판단 O 8 테이블 → 자연어 변환 O 9 제목 계층 컨텍스트 보존 O 10 코드 블록 전략 결정 O 주의할 점: 과도한 전처리도 독이다
마지막으로 한 가지 경고를 드릴게요. 전처리를 너무 과하게 하면 오히려 역효과가 납니다.
- 불용어(stopword) 제거는 하지 마세요 — 전통적인 검색 엔진에서는 "을", "를", "the", "a" 같은 불용어를 제거했지만, 임베딩 모델은 문장 전체의 맥락을 이해하므로 불용어도 의미에 기여합니다
- 형태소 분석(stemming/lemmatization)도 불필요 — 역시 임베딩 모델이 알아서 처리합니다. "running" → "run"으로 바꾸면 오히려 맥락 정보가 손실됩니다
- 문장을 너무 짧게 자르지 마세요 — 임베딩 모델은 문장 단위 이상의 맥락이 있어야 의미를 제대로 파악합니다
전처리의 목적은 "노이즈를 제거하되 의미는 보존"하는 것입니다. 의미까지 건드리는 전처리는 피하세요.
마무리
벡터 DB에 문서를 넣을 때 전처리를 건너뛰면, 아무리 좋은 임베딩 모델과 청킹 전략을 써도 검색 품질이 나빠집니다. 특히 한국어-영어 혼합 콘텐츠의 텍스트 정규화, API 레퍼런스의 테이블 변환, 가이드 문서의 제목 계층 보존은 개발 문서 RAG에서 빠뜨리기 쉬운 핵심 전처리입니다.
위의 체크리스트를 여러분의 파이프라인에 맞게 조정해서 적용해보세요. 검색 품질이 체감될 정도로 달라질 겁니다.
다음 글에서는 전처리 다음 단계인 청킹(chunking) 전략 — 문서를 어떤 크기로, 어떤 기준으로 나눌 것인지 — 에 대해 다뤄볼게요.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
RAGAS로 RAG 시스템 평가하기 — 지표별 의미와 Python 실전 사용법 (0) 2026.03.27 Qdrant 벡터 검색에서 Reranking까지, 실전 코드 (0) 2026.03.26 Bi-Encoder vs Cross-Encoder, 왜 둘 다 필요한가 (1) 2026.03.26 2026년 RAG용 임베딩 모델 총정리 - OpenAI 넘어선 오픈소스들 (0) 2026.03.25 RAG 청킹 전략 완전 정복 — 콘텐츠별 최적 크기와 방법 (0) 2026.03.24 벡터 DB 3대장 비교, 나에게 맞는 선택은? (0) 2026.03.23 AI 검색시스템에 '무엇을' 넣을지 정하는 법 (0) 2026.03.23 AI 챗봇 프레임워크를 250줄 게이트웨이로 교체한 이유 (0) 2026.03.22 GPU 하나로 AI 작업 두 개 돌리기 — 우선순위 스케줄러 만들기 (1) 2026.03.21 OAuth 2.0: 비밀번호를 넘기지 않고 권한만 빌려주는 방법 (3) 2026.03.20