본문 바로가기
IT/AI

RAG 완전 정복: 벡터DB 데이터 적재 파이프라인 전체 프로세스 상세 가이드

by twofootdog 2026. 6. 7.
반응형

1. RAG(검색 증강 생성)란 무엇인가?

RAG는 Retrieval-Augmented Generation의 약자로, 우리말로는 검색 증강 생성이라고 부릅니다.

생성형 AI가 답변을 만들기 전에 외부 데이터베이스에서 관련 정보를 먼저 검색하고, 그 결과를 바탕으로 더 정확한 답변을 생성하는 기술입니다.

쉽게 비유하면 오픈북 시험과 같습니다. 아무것도 없이 기억만으로 답하는 것이 아니라, 참고 문서를 펼쳐보고 나서 답을 작성하는 방식입니다. AI가 학습 당시 알지 못했던 최신 정보나 사내 전용 문서도 참고해서 답할 수 있게 됩니다.

* RAG가 해결하는 핵심 문제 두 가지

AI는 학습이 완료된 시점 이후의 정보를 모릅니다. 또 사내 전용 문서, 규정집, 제품 매뉴얼 같은 외부 공개되지 않은 데이터도 알지 못합니다. RAG는 이 두 가지 한계를 동시에 해결합니다.

실제로 국내 기업들도 RAG를 적극 도입하고 있습니다. 포스코는 사내 지식 정보와 언어 모델을 결합한 'P-GPT' 서비스를 운영 중이고, KB국민카드는 RAG 기반 고객 상담 챗봇 'BELLA QNA'를 구축했습니다. 이처럼 RAG는 이제 특정 기업만의 실험적 기술이 아니라, 업무 자동화의 표준 아키텍처로 자리 잡고 있습니다.

 


2. 왜 벡터DB인가: 의미 기반 검색의 원리

기존의 키워드 검색은 단어가 정확히 일치할 때만 결과를 찾아줍니다. "강아지 사료 추천"으로 검색하면 "강아지", "사료", "추천"이라는 단어가 있는 문서만 찾습니다. 하지만 "반려동물 먹이 제안"이라는 표현으로 작성된 문서는 찾지 못합니다.

벡터DB는 이 문제를 해결합니다. 텍스트를 의미를 담은 숫자 배열(벡터) 로 변환해서 저장하기 때문에, 표현이 달라도 의미가 비슷하면 높은 유사도로 검색됩니다. "강아지 사료"와 "반려동물 먹이"는 서로 다른 단어지만, 벡터 공간에서는 매우 가까운 위치에 존재합니다.

* 벡터란?

텍스트를 수백~수천 개의 숫자로 이루어진 배열로 표현한 것입니다.

예시: [0.12, -0.34, 0.89, 0.03, ...]

의미가 비슷한 문장일수록 이 숫자 배열이 서로 가깝게 위치합니다. 이 변환 작업을 해주는 것이 바로 임베딩 모델입니다.


3. RAG의 두 가지 파이프라인 구분

RAG 시스템을 이해하려면 두 가지 파이프라인이 완전히 분리되어 있다는 점을 먼저 이해해야 합니다.

구분 인덱싱 파이프라인 (적재) 조회 파이프라인 (검색+생성)
실행 시점 파일 업로드 시 사용자 질문 시
처리 방식 오프라인/배치 처리 실시간 처리
속도 느려도 괜찮음 빨라야 함
주요 작업 파싱→청킹→임베딩→저장 임베딩→검색→생성
반복 여부 한 번 처리 후 재사용 매 질문마다 반복

중요한 점은, 벡터DB에 데이터를 적재하는 것은 Agent나 챗봇이 실행될 때가 아니라, 파일이 업로드되거나 문서가 추가될 때라는 것입니다. Agent는 이미 구축된 벡터DB에서 검색만 합니다. 이 구조 덕분에 수백만 건의 문서도 빠르게 검색할 수 있습니다.

 


4. 적재 파이프라인 전체 흐름

파일이 S3나 파일시스템에 업로드되는 순간부터 벡터DB에 저장되기까지 전체 흐름입니다.

각 단계를 이제 하나씩 상세히 살펴보겠습니다.

 

 

① 1단계: 파싱(Parsing) — 파일에서 텍스트 추출

파싱은 원본 파일에서 텍스트를 꺼내는 작업입니다. PDF, Word, 엑셀, 이미지 등 파일 형식마다 내부 구조가 완전히 다르기 때문에, 파일 종류에 따라 다른 파서를 사용합니다.

파일 종류별로 파서를 일일이 지정하기 번거롭다면, Unstructured.io 라이브러리가 파일 타입을 자동으로 감지해서 적절한 파서를 선택해줍니다. LangChain의 Document Loader도 같은 역할을 합니다.

 

※ 파싱에 AI가 필요한 경우는?

스캔된 PDF(이미지로 저장된 문서), 복잡한 표가 포함된 문서, 이미지 파일 등은 규칙 기반 파서로 텍스트를 추출하기 어렵습니다. 이 경우 Vision 모델(GPT-4V, Claude Vision 등)을 사용해서 이미지를 이해하고 텍스트로 변환합니다. 단, 일반적인 텍스트 PDF나 Word 파일은 AI 없이도 충분히 파싱할 수 있습니다.

 

* 파일 타입별 파서 선택

파일 형식 라이브러리/도구 AI 필요여부 주요 처리
PDF (텍스트형) PyMuPDF, pdfplumber 불필요 (규칙기반) 텍스트 추출, 레이아웃 복원
PDF (스캔/이미지형) Tesseract OCR + GPT-4V AI 필요 OCR 후 구조 이해
DOCX python-docx, Unstructured 불필요 단락/표/헤딩 추출
PPTX python-pptx 불필요 슬라이드별 텍스트, 노트
HTML BeautifulSoup, Trafilatura 불필요 태그 제거, 본문 추출
CSV/Excel pandas 불필요 행/열 → 자연어 변환
이미지 (JPG/PNG) GPT-4V, Claude Vision AI 필요 이미지 설명 생성
오디오 (MP3/WAV) OpenAI Whisper AI 필요 음성 → 텍스트(STT)

 

# Unstructured: 파일 타입 자동 감지 및 파싱
from unstructured.partition.auto import partition

elements = partition(filename="document.pdf")  # PDF든 DOCX든 자동 처리
text = "\n".join([str(el) for el in elements])

# LangChain Document Loader 방식
from langchain.document_loaders import PyPDFLoader, CSVLoader
from pathlib import Path

loaders = {
    ".pdf":  PyPDFLoader,
    ".csv":  CSVLoader,
}

ext = Path(file_path).suffix
loader = loaders[ext](file_path)
docs = loader.load()

 

 

② 2단계: 전처리(Preprocessing) — 텍스트 정제

파싱으로 추출한 텍스트는 바로 사용하기 어렵습니다. HTML 태그, 특수문자, 인코딩 오류, 의미 없는 헤더/푸터 등 불필요한 내용이 섞여 있기 때문입니다. 전처리는 이런 노이즈를 제거하고 텍스트를 일관된 형태로 만드는 작업입니다. 이 단계는 AI가 전혀 필요하지 않습니다. 정규식과 간단한 라이브러리만으로 충분히 처리할 수 있습니다.

 

* 전처리 작업 목록

작업 목록 라이브러리 설명
HTML 태그 제거 BeautifulSoup, regex <div>, <p> 등 HTML 태그 모두 제거
특수문자 정제 re (내장) 의미 없는 기호, 이상한 공백 제거
인코딩 정규화 ftfy, chardet 깨진 한글, 특수 인코딩 자동 복구
언어 감지 langdetect, fasttext 한국어/영어 등 언어 자동 분류
헤더/푸터 제거 규칙기반 패턴 "페이지 3/10", "기밀문서" 등 반복 패턴 제거
공백 정규화 re (내장) 줄바꿈, 탭, 다중 공백 통일
import re
import ftfy
from langdetect import detect

def preprocess(text: str) -> str:
    # 인코딩 오류 자동 수정
    text = ftfy.fix_text(text)

    # HTML 태그 제거
    text = re.sub(r'<[^>]+>', '', text)

    # 특수문자 정제 (한글, 영문, 숫자, 기본 구두점 유지)
    text = re.sub(r'[^\w\s.,!?가-힣]', '', text)

    # 다중 공백 → 단일 공백
    text = re.sub(r'\s+', ' ', text).strip()

    return text

# 언어 감지
lang = detect(text)  # 'ko', 'en', 'ja' 등 반환

 

 

③ 3단계: 클렌징(Cleansing) — 데이터 품질 보장

전처리가 텍스트 형태를 다듬는 작업이라면, 클렌징은 데이터의 품질 자체를 보장하는 작업입니다. 중복 문서, 너무 짧은 텍스트, 개인정보 포함 데이터 등을 걸러내거나 처리합니다.

※ 클렌징을 건너뛰면 안 되는 이유

중복 데이터가 많으면 특정 내용이 과도하게 검색되어 편향된 답변이 나옵니다. 개인정보가 포함된 채로 적재되면 보안 사고로 이어질 수 있습니다. 클렌징은 귀찮아도 반드시 거쳐야 하는 단계입니다.

 

* 클렌징 작업 목록

작업 목록 라이브러리 방식
중복 문서 제거 MinHash, SimHash 유사도 기반 중복 탐지
짧은 텍스트 필터 길이 기반 규칙 50자 미만 청크 제거
개인정보(PII) 마스킹 presidio (Microsoft) 이름, 전화번호, 이메일 자동 탐지 및 마스킹
불필요 반복 제거 spaCy, 규칙기반 "다음 페이지 계속됩니다" 등 반복 문구 제거
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

# 개인정보 탐지
results = analyzer.analyze(text=text, language='ko')

# 마스킹 처리 (이름 → <PERSON>, 전화번호 → <PHONE_NUMBER>)
anonymized = anonymizer.anonymize(
    text=text,
    analyzer_results=results
)

# 중복 문서 제거 (MinHash)
from datasketch import MinHash, MinHashLSH

lsh = MinHashLSH(threshold=0.8, num_perm=128)
# 유사도 80% 이상인 문서를 중복으로 처리

 

 

④ 4단계: 청킹(Chunking) — 문서를 적절한 크기로 분할

청킹은 긴 문서를 검색하기 좋은 작은 조각으로 나누는 작업입니다. 100페이지짜리 PDF 전체를 하나의 단위로 저장하면, 나중에 질문과 관련된 부분을 정확하게 찾기 어렵습니다. 반면 너무 작게 나누면 맥락이 끊겨 의미 파악이 힘들어집니다.

※ 오버랩(Overlap)이 중요한 이유

청크를 자를 때 앞뒤 청크와 일정 부분(보통 50~100 토큰)을 겹치게 설정합니다. 문장이 청크 경계에서 잘렸을 때도 맥락이 유지되도록 하기 위함입니다.

청크 크기 선택 가이드

일반적으로 300~600 토큰이 적절합니다. 너무 작으면(100 토큰 이하) 맥락이 부족해 답변 품질이 낮아지고, 너무 크면(1000 토큰 이상) 검색 정확도가 떨어집니다. 문서 특성과 실제 질문 패턴을 보면서 조정하는 것이 좋습니다.

 

* 청킹 전략 비교

전략 라이브러리 특징 적합한 문서
고정 크기 청킹 RecursiveCharacterTextSplitter 단순, 빠름, 안정적 일반 문서, 뉴스 기사
문장 단위 청킹 NLTK, spaCy 문장 경계 보존 학술 논문, 보고서
시맨틱 청킹 SemanticChunker 의미 단위 분리, 품질 높음 의미 구분이 중요한 문서
헤딩 기반 청킹 MarkdownHeaderTextSplitter 문서 구조 보존 Markdown, 기술 문서

 

from langchain.text_splitter import RecursiveCharacterTextSplitter

# 고정 크기 청킹 (가장 일반적)
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,       # 청크당 최대 토큰 수
    chunk_overlap=50,     # 앞뒤 청크와 겹치는 토큰 수
    separators=["\n\n", "\n", ".", " "]  # 분할 우선순위
)
chunks = splitter.split_text(text)

# 시맨틱 청킹 (의미 단위로 분할, 품질 우수)
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

chunker = SemanticChunker(OpenAIEmbeddings())
chunks = chunker.split_text(text)

# Markdown 헤딩 기반 청킹
from langchain.text_splitter import MarkdownHeaderTextSplitter

headers = [("#", "H1"), ("##", "H2"), ("###", "H3")]
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers)
chunks = md_splitter.split_text(markdown_text)

 

 

⑤ 5단계: 임베딩(Embedding) — 텍스트를 벡터로 변환

임베딩은 텍스트를 숫자 배열(벡터)로 변환하는 작업입니다. 이 숫자 배열에 텍스트의 의미가 압축되어 있어서, 나중에 질문이 들어왔을 때 의미적으로 가장 유사한 청크를 빠르게 찾을 수 있게 됩니다.

임베딩 모델은 GPT-4, Claude 같은 생성형 AI와는 다른 종류의 모델입니다. 텍스트를 생성하는 것이 목적이 아니라, 텍스트의 의미를 벡터라는 형태로 압축하는 것이 목적입니다. 구조도 다르고(Encoder 기반), 출력도 다릅니다(문장이 아닌 숫자 배열).

※ 가장 중요한 규칙: 임베딩 모델은 반드시 동일해야 합니다

적재 시 text-embedding-3-small로 벡터를 만들었다면, 질문을 검색할 때도 반드시 text-embedding-3-small을 사용해야 합니다. 모델이 다르면 벡터가 표현하는 공간 자체가 달라지기 때문에, 유사도 계산이 전혀 의미 없어집니다.

한국어 문서를 주로 처리한다면 BGE-M3나 embed-multilingual-v3가 유리합니다. 비용이 중요하다면 오픈소스 모델을 로컬에서 실행하는 것도 좋은 선택입니다.

* 주요 임베딩 모델 비교

모델 제공사 벡터 차원 특징
text-embedding-3-small OpenAI 1,536 가성비, API 호출
text-embedding-3-large OpenAI 3,072 고성능, API 호출
text-embedding-004 Google 768 Gemini 계열, API 호출
embed-multilingual-v3 Cohere 1,024 다국어 강점, API 호출
BGE-M3 BAAI 1,024 오픈소스, 한국어 강점
E5-large Microsoft 1,024 오픈소스, 무료

 

# OpenAI 임베딩 (API 호출)
from langchain_openai import OpenAIEmbeddings

embed_model = OpenAIEmbeddings(model="text-embedding-3-small")
vector = embed_model.embed_query("텍스트 내용")
# → [0.12, -0.34, 0.89, ...] (1536개 숫자)

# BGE-M3 (오픈소스, 로컬 실행, 한국어 강점)
from langchain_huggingface import HuggingFaceEmbeddings

embed_model = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3"
)
vector = embed_model.embed_query("텍스트 내용")

# 여러 청크를 한번에 임베딩 (배치 처리)
vectors = embed_model.embed_documents(chunks)

 

 

⑥ 6단계: 벡터DB 저장 — 검색 가능한 형태로 보관

임베딩된 벡터를 벡터DB에 저장합니다. 벡터만 저장하는 것이 아니라, 원본 텍스트와 메타데이터(파일명, 페이지 번호, 작성일 등)도 함께 저장합니다. 나중에 검색 결과를 사용자에게 보여줄 때 출처를 알려주기 위해서입니다.

* 주요 벡터DB 비교

벡터DB 형태 특징 추천 환경
Pinecone 완전 관리형 클라우드 빠름, 확장성 최고, 유료 프로덕션, 대용량
Weaviate 오픈소스/클라우드 하이브리드 검색 지원 프로덕션, 온프레미스
Qdrant 오픈소스/클라우드 경량, 빠른 설치 중소규모, 온프레미스
ChromaDB 로컬/경량 설치 간편, 무료 개발/테스트, 소규모
pgvector PostgreSQL 확장 기존 DB 통합 PostgreSQL 사용 중인 팀
FAISS 인메모리 초고속, Meta 개발 대용량 로컬 처리
# ChromaDB (로컬 개발용, 가장 간편)
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

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

vectorstore = Chroma.from_documents(
    documents=chunks,        # 청킹된 문서 리스트
    embedding=embed_model,   # 임베딩 모델
    persist_directory="./chroma_db"
)

# Pinecone (프로덕션용)
from langchain_pinecone import PineconeVectorStore

vectorstore = PineconeVectorStore.from_documents(
    documents=chunks,
    embedding=embed_model,
    index_name="my-rag-index"
)

# 저장되는 데이터 구조
# {
#   "id": "doc_001_chunk_003",
#   "vector": [0.12, -0.34, 0.89, ...],     ← 임베딩 결과
#   "text": "3분기 매출은 150억이었으며...", ← 원본 텍스트
#   "metadata": {
#     "source": "2024_Q3_report.pdf",
#     "page": 12,
#     "created_at": "2024-10-15"
#   }
# }

 

 


5. 마치며

RAG 데이터 적재 파이프라인에서 가장 중요한 것인 3가지 입니다.

첫째, 벡터DB 적재는 파일 업로드 시점에 미리 합니다. 질문이 들어올 때마다 하면 너무 느리고 비용도 많이 발생합니다.

둘째, 임베딩 모델은 적재와 조회 시 반드시 동일해야 합니다. 다르면 검색이 전혀 동작하지 않습니다.

셋째, 파싱/클렌징/청킹의 품질이 최종 답변 품질을 결정합니다. 쓰레기 데이터가 들어가면 쓰레기 답변이 나옵니다(Garbage In, Garbage Out).

이것으로 RAG가 무엇이고 RAG 데이터 적재 파이프라인에 대한 상세 설명을 마무리 하도록 하겠습니다.

다음 시간에는 RAG 조회 파이프라인에 대해 알아보도록 하겠습니다.

 

 

 


6. 참고 자료

 

반응형

댓글