벡터 데이터베이스와 문서 파이프라인
RAG의 성패는 답변 전에 결정된다
지난 장에서 RAG를 "필요한 문서를 찾아 LLM에게 읽히는 구조"라고 설명했습니다. 그렇다면 자연스럽게 다음 질문이 생깁니다.
"그 문서는 누가, 언제, 어떤 모양으로 준비해 두는가?"
많은 사람이 처음 RAG를 만들 때 LLM 모델부터 고릅니다. 더 똑똑한 모델을 쓰면 답변이 좋아질 것이라고 기대합니다. 물론 모델 성능은 중요합니다. 하지만 문서 기반 Q&A에서 더 자주 발목을 잡는 것은 모델이 아니라 문서 준비와 검색 설계입니다.
사용자의 질문에 맞는 문서가 검색되지 않으면, LLM은 좋은 답을 만들 재료가 없습니다. 검색된 문서가 오래된 버전이면, LLM은 낡은 정보를 근거로 자신 있게 답합니다. PDF에서 표가 깨진 채로 추출되면, LLM은 잘못된 숫자를 읽을 수 있습니다. 권한 필터가 빠지면, 사용자가 보면 안 되는 문서를 LLM이 답변에 사용할 수도 있습니다.
그래서 실전 RAG는 단순히 "문서를 임베딩해서 저장한다"가 아닙니다. 문서를 읽고, 깨끗하게 만들고, 의미 단위로 나누고, 출처와 권한을 붙이고, 검색 가능한 저장소에 넣고, 문서가 바뀌면 다시 갱신하는 데이터 파이프라인입니다. RAG 시스템을 도서관에 비유하면, LLM은 말을 잘하는 사서이고, 벡터 데이터베이스는 찾아보기 카드함이며, 문서 파이프라인은 책을 들여와 분류하고 라벨을 붙이는 정리 작업입니다.
겉으로 보이는 챗봇 답변은 마지막 장면일 뿐입니다. 좋은 답변은 그보다 훨씬 앞에서, 문서를 준비하는 순간부터 만들어집니다.

문서 파이프라인의 큰 그림
RAG에는 크게 두 흐름이 있습니다. 하나는 사용자가 질문하기 전에 미리 돌아가는 인덱싱 파이프라인이고, 다른 하나는 사용자가 질문했을 때 실시간으로 돌아가는 질의 파이프라인입니다. 여러 RAG 구현 가이드에서도 이 둘을 나누어 설명합니다. 문서를 준비하는 오프라인 단계와 질문을 처리하는 온라인 단계를 분리하면 시스템을 훨씬 안정적으로 설계할 수 있습니다.1
인덱싱 파이프라인은 문서를 검색 가능한 형태로 바꾸는 일입니다.
문서 소스
↓
문서 로딩
↓
텍스트 추출
↓
정제
↓
메타데이터 부여
↓
청킹
↓
임베딩 생성
↓
벡터 데이터베이스 저장질의 파이프라인은 사용자의 질문에 맞는 문서 조각을 찾아 LLM에게 전달하는 일입니다.
사용자 질문
↓
질문 정제 또는 재작성
↓
질문 임베딩
↓
벡터 검색 + 키워드 검색
↓
메타데이터 필터링
↓
재랭킹
↓
컨텍스트 구성
↓
LLM 답변 생성이번 장의 초점은 첫 번째 흐름, 즉 문서를 넣는 쪽입니다. 하지만 문서 파이프라인은 검색과 분리해서 생각할 수 없습니다. 어떻게 나누고 저장하느냐가 나중에 어떻게 검색되느냐를 결정하기 때문입니다.
예를 들어 문서 조각에 page 메타데이터를 저장하지 않으면 답변에 페이지 출처를 표시하기 어렵습니다. department 메타데이터를 저장하지 않으면 부서별 검색 필터를 걸기 어렵습니다. 문서를 너무 크게 나누면 정확한 문장만 찾아내기 어렵고, 너무 작게 나누면 답변에 필요한 맥락이 사라집니다.
즉 인덱싱은 단순 저장이 아니라, 미래의 검색을 미리 설계하는 일입니다.
문서 로딩: 지식은 여러 곳에 흩어져 있다
실제 서비스에서 문서는 한곳에 얌전히 모여 있지 않습니다. 어떤 팀은 PDF로 매뉴얼을 관리하고, 어떤 팀은 Notion에 정책을 적어 둡니다. 개발팀은 GitHub나 사내 위키에 문서를 두고, 고객지원팀은 FAQ 시스템과 티켓 데이터를 씁니다. 영업팀은 스프레드시트와 제안서 파일을 갖고 있을 수 있습니다.
문서 로딩은 이런 다양한 소스에서 데이터를 가져오는 첫 단계입니다. 이때 중요한 것은 "파일을 읽는다"보다 "원천 정보를 잃지 않는다"입니다. 나중에 답변이 틀렸을 때 원본을 다시 확인해야 하고, 문서가 수정되었을 때 어떤 청크를 다시 만들지 알아야 하기 때문입니다.
문서 로더는 보통 다음 정보를 함께 가져옵니다.
원본 문서의 고유 ID
파일명 또는 제목
원본 URL이나 저장 경로
작성일과 수정일
문서 유형
작성자 또는 소유 팀
접근 권한 정보
예를 들어 제품 매뉴얼 PDF를 읽는다면 단순히 본문 텍스트만 가져오면 부족합니다. 제품매뉴얼_v4.pdf라는 파일명, 18페이지라는 위치, "결제와 환불"이라는 섹션 제목, 마지막 수정일, 고객지원팀이 관리한다는 정보까지 가져와야 합니다.
이 정보들은 훗날 검색 품질과 운영 품질을 동시에 좌우합니다. 답변에 출처를 붙이는 데도 필요하고, 오래된 문서를 검색에서 제외하는 데도 필요하며, 사용자가 볼 수 있는 문서만 검색하도록 권한을 제한하는 데도 필요합니다.
텍스트 추출: 문서는 생각보다 지저분하다
마크다운이나 일반 텍스트 파일은 비교적 다루기 쉽습니다. 하지만 PDF, Word, PowerPoint, Excel, HTML, 이미지 스캔 문서는 이야기가 달라집니다. 사람에게는 멀쩡해 보이는 문서도 기계가 읽으면 엉망이 될 수 있습니다.
PDF를 예로 들어 보겠습니다. PDF는 "읽기 좋은 문서"라기보다 "보이는 모양을 고정하는 문서"에 가깝습니다. 그래서 텍스트 추출을 해 보면 줄바꿈이 이상하게 들어가거나, 머리말과 꼬리말이 페이지마다 반복되거나, 표의 열 순서가 뒤섞일 수 있습니다. 2단 편집 문서에서는 왼쪽 열을 읽다가 갑자기 오른쪽 열로 넘어가기도 합니다. 스캔 PDF라면 OCR, 즉 이미지 속 글자를 인식하는 과정도 필요합니다.
웹페이지도 만만하지 않습니다. 본문보다 메뉴, 광고, 푸터, 추천 글, 쿠키 배너가 더 많이 추출될 수 있습니다. Notion 페이지는 제목 계층, 토글, 표, 데이터베이스 블록을 어떻게 펼쳐 읽을지 결정해야 합니다. 사내 문서는 보안 문구, 워터마크, 결재선, 반복되는 템플릿 문장이 섞여 있을 수 있습니다.
텍스트 추출 단계에서는 다음을 점검해야 합니다.
본문과 불필요한 UI 텍스트가 분리되었는가
표의 행과 열 관계가 보존되었는가
제목과 소제목 계층이 남아 있는가
페이지 번호와 섹션 위치를 추적할 수 있는가
OCR 오류로 인해 중요한 단어가 깨지지 않았는가
이미지나 차트의 의미를 설명할 별도 처리가 필요한가
문서 기반 RAG에서 "문서를 넣었는데 답을 못 한다"는 문제의 뿌리는 종종 이 단계에 있습니다. 실제로는 LLM이 못 읽는 것이 아니라, 우리가 읽을 수 없는 형태로 문서를 건네준 것입니다.
정제: 검색에 방해되는 먼지를 털어내기
텍스트를 추출한 뒤에는 정제가 필요합니다. 정제는 검색에 방해되는 노이즈를 줄이고, 문서의 구조를 더 잘 드러나게 만드는 과정입니다.
예를 들어 PDF에서 모든 페이지마다 다음 문구가 반복된다고 해 봅시다.
Confidential - ABC Corporation - Do not distribute이 문구가 모든 청크에 들어가면 임베딩에도 영향을 줍니다. 검색 결과가 실제 내용보다 반복 문구에 끌릴 수 있고, 불필요한 토큰 비용도 늘어납니다. 웹페이지에서 "로그인", "회원가입", "관련 글", "맨 위로" 같은 텍스트가 계속 들어가는 것도 비슷한 문제입니다.
정제 단계에서는 보통 중복 공백을 정리하고, 반복 머리말과 꼬리말을 제거하고, 깨진 줄바꿈을 합치고, 불필요한 HTML 태그를 없앱니다. 다만 조심해야 할 점이 있습니다. 너무 공격적으로 정제하면 중요한 구조까지 사라집니다.
예를 들어 목록 기호를 모두 없애면 절차 문서의 단계가 흐려질 수 있습니다.
1. 관리자 콘솔에 로그인합니다.
2. 사용자 관리 메뉴를 엽니다.
3. 초대할 사용자의 이메일을 입력합니다.이 문장을 단순히 한 줄로 합쳐 버리면 순서 정보가 약해집니다. 표도 마찬가지입니다. 표를 그냥 문장으로 펼칠 때는 각 값이 어떤 열 이름에 해당하는지 함께 남겨야 합니다.
나쁜 정제는 문서를 깨끗하게 만드는 것이 아니라, 의미를 지워 버립니다. 좋은 정제는 먼지를 털어내되 뼈대는 남깁니다.
메타데이터: 청크의 신분증 만들기
RAG에서 청크는 혼자 떠다니면 안 됩니다. 모든 청크는 자신이 어디에서 왔고, 어떤 문서의 어느 부분이며, 누가 볼 수 있는지 설명할 수 있어야 합니다. 이 정보를 메타데이터라고 부릅니다.
메타데이터는 검색 품질뿐 아니라 출처 표시, 권한 관리, 문서 갱신, 디버깅에 필수입니다. 다음은 실전에서 자주 쓰는 청크 저장 형태입니다.
{
"chunk_id": "product_manual_v4_000128",
"text": "연간 요금제는 결제일로부터 7일 이내에만 전액 환불할 수 있습니다...",
"embedding": [0.021, -0.114, 0.087, "..."],
"metadata": {
"source_id": "product_manual_v4",
"source_type": "pdf",
"title": "제품 매뉴얼 v4",
"uri": "https://docs.example.com/manual/v4.pdf",
"page": 42,
"section": "결제와 환불",
"chunk_index": 128,
"created_at": "2026-04-02T10:00:00Z",
"updated_at": "2026-05-01T09:30:00Z",
"version": "4.0",
"language": "ko",
"department": "customer_support",
"access_level": "internal"
}
}여기서 text와 embedding만 있으면 의미 검색은 할 수 있습니다. 하지만 page가 없으면 페이지 출처를 보여 주기 어렵습니다. updated_at이 없으면 최신 문서만 검색하기 어렵습니다. access_level이 없으면 권한 필터링을 하기 어렵습니다. source_id가 없으면 원본 문서가 수정되었을 때 기존 청크를 찾아 삭제하거나 갱신하기 어렵습니다.
메타데이터는 나중에 넣으려면 번거롭습니다. 이미 수십만 개의 청크를 저장한 뒤 "아, 부서 정보도 필요하네"라고 깨닫는 순간 재처리 작업이 시작됩니다. 처음부터 완벽할 필요는 없지만, 최소한 출처, 위치, 버전, 권한, 문서 유형은 저장하는 습관을 들이는 것이 좋습니다.
청킹: 문서를 어디서 자를 것인가
이제 RAG 문서 파이프라인의 핵심인 청킹을 살펴봅시다. 청킹은 긴 문서를 검색 가능한 작은 단위로 나누는 작업입니다. 그런데 이 작업은 단순한 가위질이 아닙니다. 어디서 자르느냐에 따라 검색 결과가 완전히 달라집니다.
다음 문장을 봅시다.
환불은 결제일로부터 7일 이내에 신청할 수 있습니다.
단, 이미 30% 이상 사용한 구독 상품은 환불 대상에서 제외됩니다.첫 문장만 검색되면 사용자는 "7일 이내면 무조건 환불되는구나"라고 오해할 수 있습니다. 두 번째 문장만 검색되면 "무엇이 환불 대상에서 제외되는지" 알기 어렵습니다. 이 두 문장은 함께 있어야 합니다.
반대로 너무 많은 내용을 한 청크에 넣어도 문제가 생깁니다.
계정 생성, 로그인, 비밀번호 재설정, 요금제 변경, 쿠폰 등록, 환불, 세금계산서, 데이터 삭제, 팀 초대...이 모든 내용이 한 청크에 있으면 "환불은 언제 되나요?"라는 질문에 검색될 수는 있지만, 청크 안에 다른 정보가 너무 많아집니다. LLM은 긴 청크에서 필요한 문장만 골라야 하고, 컨텍스트 윈도우도 낭비됩니다.
좋은 청크는 너무 작지도, 너무 크지도 않아야 합니다. 하나의 질문에 답할 만큼 충분한 맥락을 갖고, 동시에 불필요한 주제가 많이 섞이지 않아야 합니다.
고정 길이 청킹: 가장 단순한 출발점
고정 길이 청킹은 일정한 글자 수나 토큰 수마다 문서를 자르는 방식입니다.
원문: A B C D E F G H I J K L M N O
청크 1: A B C D E
청크 2: F G H I J
청크 3: K L M N O이 방식은 구현이 쉽고 빠릅니다. 문서 구조를 분석하지 않아도 되고, 모든 청크 크기를 비슷하게 맞출 수 있습니다. 초보 단계나 프로토타입에서는 충분히 쓸 만합니다.
하지만 단점도 분명합니다. 문장 중간, 문단 중간, 표 중간에서 잘릴 수 있습니다. 중요한 조건이 앞 청크와 뒤 청크로 갈라지면 검색 결과가 반쪽짜리가 됩니다. 그래서 고정 길이 청킹을 쓸 때는 보통 오버랩을 함께 둡니다.
청크 1: A B C D E
청크 2: D E F G H
청크 3: G H I J K오버랩은 이전 청크의 끝부분을 다음 청크 앞에 조금 반복하는 방식입니다. 경계에 걸린 문장이 잘려 나가는 문제를 줄일 수 있습니다. Azure AI Search의 문서 청킹 가이드도 큰 문서를 모델 입력 한도 아래로 나누는 이유와 함께, 일정 크기와 오버랩을 두는 접근을 설명합니다.2
다만 오버랩이 많을수록 저장량과 임베딩 비용이 늘어납니다. 같은 문장이 여러 청크에 반복되어 검색 결과가 중복될 수도 있습니다. 보통은 처음에 10~25% 정도의 오버랩으로 시작한 뒤, 실제 검색 결과를 보며 조정합니다. 중요한 것은 "정답 숫자"를 외우는 것이 아니라, 우리 문서와 질문 유형에 맞게 실험하는 것입니다.
문단 기준 청킹: 문서의 자연스러운 호흡을 살리기
문단 기준 청킹은 제목, 소제목, 문단, 목록, 표 같은 문서 구조를 기준으로 나누는 방식입니다. 사람이 읽을 때의 의미 단위를 최대한 보존하려는 접근입니다.
예를 들어 마크다운 문서라면 ## 환불 정책 아래의 몇 개 문단을 하나의 청크로 묶을 수 있습니다. HTML 문서라면 h2, h3, p, li 같은 태그 구조를 활용할 수 있습니다. Word 문서나 PDF에서도 제목 스타일이나 페이지 구조를 추출할 수 있다면 이를 활용합니다.
문단 기준 청킹의 장점은 의미가 비교적 잘 유지된다는 점입니다. "단,"으로 시작하는 예외 조항이 앞 조건과 함께 묶일 가능성이 높아집니다. 절차 문서의 1단계부터 5단계까지가 같은 청크 안에 남을 수 있습니다. 사용자가 출처를 확인할 때도 섹션 단위로 보기 좋습니다.
다만 문단이 항상 적당한 크기라는 보장은 없습니다. 어떤 문단은 한 줄이고, 어떤 문단은 2,000토큰이 넘을 수 있습니다. 그래서 실제로는 문단 기준으로 먼저 나눈 뒤, 너무 큰 조각은 다시 토큰 길이 기준으로 나누는 혼합 방식이 많이 쓰입니다.
1단계: 제목과 문단 기준으로 크게 나눈다.
2단계: 너무 큰 조각은 최대 토큰 수에 맞춰 다시 나눈다.
3단계: 각 청크 앞에 섹션 제목을 붙여 맥락을 보강한다.예를 들어 청크 본문 앞에 다음처럼 제목 경로를 붙일 수 있습니다.
문서: 제품 매뉴얼 v4
섹션: 결제와 환불 > 연간 요금제 환불
연간 요금제는 결제일로부터 7일 이내에만 전액 환불할 수 있습니다...이렇게 하면 청크만 따로 검색되어도 "이 문장이 어떤 주제에 속하는지" 모델과 검색 시스템이 더 잘 이해할 수 있습니다.
의미 기반 청킹: 내용이 바뀌는 지점을 찾기
의미 기반 청킹은 텍스트의 의미 흐름을 보고 주제가 바뀌는 지점에서 나누려는 방식입니다. 고정 길이 청킹이 자를 위치를 숫자로 정한다면, 의미 기반 청킹은 "여기부터 다른 이야기가 시작되는가?"를 묻습니다.
예를 들어 한 문서 안에 다음 주제가 이어진다고 해 봅시다.
회원 가입 방법
이메일 인증 절차
비밀번호 재설정
2단계 인증
계정 삭제고정 길이 방식은 이 주제들을 중간에서 자를 수 있습니다. 문단 기준 방식은 구조가 잘 잡힌 문서에서는 잘 동작하지만, 제목이 엉성하거나 긴 서술형 문서에서는 애매할 수 있습니다. 의미 기반 청킹은 문장이나 문단의 임베딩을 비교해, 앞뒤 내용의 유사도가 크게 떨어지는 지점을 경계 후보로 볼 수 있습니다.
이 방식은 더 똑똑해 보이지만 비용과 복잡도가 있습니다. 문장 또는 문단 단위 임베딩을 추가로 계산해야 하고, 임계값을 정해야 하며, 결과가 항상 예측 가능하지 않을 수 있습니다. 최근 청킹 평가 연구들도 최적의 청킹 방식은 작업과 데이터에 따라 달라진다고 보고합니다. 어떤 경우에는 단순한 구조 기반 방식이 강하고, 어떤 경우에는 문맥을 더 많이 반영한 방식이 유리합니다.3
따라서 의미 기반 청킹은 만능 정답이라기보다 선택지입니다. 고객지원 FAQ처럼 짧고 구조적인 문서에는 문단 기준 청킹만으로 충분할 수 있습니다. 긴 연구 보고서, 정책 문서, 회의록처럼 주제가 자연스럽게 이동하는 문서에서는 의미 기반 청킹을 실험해 볼 가치가 있습니다.
청킹 전략을 고르는 감각
청킹을 고를 때는 "어떤 문서인가"와 "사용자가 무엇을 물을 것인가"를 함께 봐야 합니다.
FAQ 문서는 이미 질문과 답변 단위로 잘 나뉘어 있습니다. 이런 문서를 굳이 1,000토큰씩 묶으면 오히려 검색이 흐려집니다. 한 질문과 답변을 하나의 청크로 두고, 관련 카테고리와 태그를 메타데이터로 붙이는 편이 낫습니다.
정책 문서나 계약서는 조항과 예외가 중요합니다. 조항 하나만 뚝 떼면 예외 조건이 빠질 수 있으므로 조항 단위로 나누되, 앞뒤 조항이나 제목을 함께 보존해야 합니다. 표와 각주는 특히 조심해야 합니다.
기술 문서는 제목 계층이 중요합니다. "설치", "인증", "오류 코드", "API 파라미터"가 서로 다른 검색 의도를 갖기 때문입니다. 코드 문서라면 함수나 클래스 단위로 나누는 것이 문자 수 기준보다 자연스럽습니다.
요약 작업은 Q&A보다 큰 청크가 유리할 때가 많습니다. 반대로 정확한 값을 찾는 질의응답은 작은 청크가 유리합니다. "이 문서를 세 줄로 요약해 줘"와 "환불 가능 기간이 며칠이야?"는 필요한 검색 단위가 다릅니다.
실전에서는 처음부터 완벽한 청킹을 찾으려 하지 않습니다. 기준값으로 시작하고, 실제 질문 세트를 만들어 검색 결과를 눈으로 확인하며 조정합니다. "정답이 들어 있는 청크가 상위 5개 안에 들어오는가?", "청크만 읽어도 답할 수 있는가?", "불필요한 내용이 너무 많이 섞이지 않았는가?"를 반복해서 봅니다.
벡터 데이터베이스는 무엇을 저장하는가
청킹을 마치면 각 청크를 임베딩으로 바꾸고 저장해야 합니다. 이때 사용하는 저장소가 벡터 데이터베이스입니다. 벡터 데이터베이스는 텍스트 자체를 저장하는 일반 데이터베이스와 달리, 고차원 벡터를 빠르게 검색하는 데 특화되어 있습니다.
임베딩 벡터는 보통 수백에서 수천 차원의 숫자 배열입니다.
"비밀번호를 재설정하는 방법" → [0.012, -0.221, 0.087, ...]사용자 질문도 같은 방식으로 벡터가 됩니다.
"암호를 잊어버렸어요" → [0.018, -0.204, 0.091, ...]두 벡터가 가까우면 의미가 비슷하다고 볼 수 있습니다. 벡터 데이터베이스는 이런 가까운 벡터를 빠르게 찾습니다. 문서가 1,000개라면 단순 비교도 가능하지만, 100만 개, 1,000만 개가 되면 효율적인 인덱스가 필요합니다. 많은 벡터 저장소는 HNSW 같은 근사 최근접 이웃 검색 알고리즘을 사용해 정확도와 속도 사이의 균형을 잡습니다.
벡터 데이터베이스가 저장하는 것은 보통 세 가지입니다.
{
"id": "chunk_001",
"vector": [0.012, -0.221, 0.087, "..."],
"payload": {
"text": "비밀번호 재설정은 로그인 화면의 '비밀번호 찾기'에서 진행합니다...",
"source": "help_center.md",
"section": "로그인 문제",
"updated_at": "2026-04-12"
}
}id는 청크를 구분하는 키입니다. vector는 검색에 쓰이는 임베딩입니다. payload 또는 metadata는 원문과 부가 정보입니다. 어떤 시스템은 원문 텍스트를 벡터 DB에 함께 저장하고, 어떤 시스템은 벡터 DB에는 ID와 메타데이터만 저장한 뒤 원문은 별도 문서 저장소에서 가져옵니다. 규모가 커질수록 원문 저장소와 벡터 인덱스를 분리하는 설계도 자주 등장합니다.
FAISS, Chroma, Pinecone, Weaviate는 무엇이 다른가
벡터 저장소에는 여러 선택지가 있습니다. 이름이 많아서 처음에는 혼란스럽지만, 크게 보면 "로컬 라이브러리", "개발용 벡터 DB", "운영용 관리형 서비스", "검색 엔진형 벡터 DB" 정도로 나누어 이해할 수 있습니다.
FAISS는 Meta에서 만든 벡터 검색 라이브러리입니다. 데이터베이스라기보다 빠른 벡터 검색 엔진에 가깝습니다. 로컬 실험이나 직접 인덱스를 관리하는 시스템에 잘 맞습니다. 매우 빠르고 강력하지만, 사용자 권한, 메타데이터 관리, 백업, 분산 운영 같은 기능은 직접 설계해야 합니다.
Chroma는 RAG 프로토타입을 빠르게 만들 때 많이 쓰입니다. 파이썬 코드 몇 줄로 문서를 넣고 검색하기 쉽습니다. 작은 프로젝트, 실험, 로컬 개발 환경에서 편합니다. 다만 큰 규모의 운영 시스템에서는 성능, 확장, 운영 관리 요구사항을 따져 봐야 합니다. Chroma는 청킹 전략이 검색 성능에 미치는 영향을 별도로 평가한 연구 보고서도 공개했는데, 여기서도 청킹 선택이 검색 정확도와 효율에 큰 영향을 준다는 점을 강조합니다.4
Pinecone은 관리형 벡터 데이터베이스 서비스입니다. 서버를 직접 운영하고 싶지 않은 팀에게 유리합니다. 인프라 관리, 확장, 가용성 같은 부담을 줄일 수 있습니다. 대신 데이터가 외부 서비스로 나가도 되는지, 비용 구조가 서비스 규모에 맞는지 검토해야 합니다.
Weaviate는 벡터 검색과 함께 메타데이터 필터링, 하이브리드 검색, GraphQL API 같은 기능을 제공하는 벡터 데이터베이스입니다. 검색 기능을 풍부하게 쓰고 싶은 팀에게 매력적입니다.
여기에 Qdrant, Milvus, pgvector 같은 선택지도 자주 등장합니다. Qdrant는 메타데이터 필터링과 운영 성능 측면에서 많이 쓰이고, Milvus는 대규모 벡터 검색에 강한 오픈소스 벡터 DB입니다. pgvector는 PostgreSQL 안에서 벡터 검색을 가능하게 해 주므로, 이미 Postgres를 쓰는 작은 서비스에서 시작하기 좋습니다.
선택 기준은 "무엇이 제일 유명한가"가 아닙니다. 다음 질문에 답해 보면 됩니다.
몇 개의 벡터를 저장할 것인가
검색 지연 시간 목표는 어느 정도인가
자체 운영할 인프라 역량이 있는가
데이터가 외부 관리형 서비스로 나가도 되는가
메타데이터 필터링과 권한 관리가 중요한가
하이브리드 검색을 기본 제공하는가
백업, 복제, 모니터링, 장애 대응이 쉬운가
작은 실습에서는 Chroma나 FAISS로 충분합니다. 제품 수준의 서비스에서는 Pinecone, Weaviate, Qdrant, Milvus, Azure AI Search 같은 선택지를 운영 요구사항에 맞게 비교해야 합니다. 중요한 것은 벡터 DB가 답변 품질을 자동으로 보장하지 않는다는 점입니다. 벡터 DB는 좋은 문서 조각을 빠르게 찾는 도구입니다. 무엇을 넣고 어떻게 찾을지는 여전히 우리가 설계해야 합니다.
메타데이터 필터링: 검색 범위를 먼저 좁히기
벡터 검색은 의미가 비슷한 문서를 찾는 데 강합니다. 하지만 실전에서는 "의미가 비슷한가"만으로 충분하지 않습니다.
사용자가 한국어로 질문했으면 한국어 문서에서 먼저 찾아야 할 수 있습니다. 2024년 정책보다 2026년 정책이 우선이어야 할 수 있습니다. 무료 요금제 사용자는 공개 도움말만 볼 수 있고, 사내 직원은 내부 문서까지 볼 수 있습니다. 인사팀 직원은 인사 문서를 볼 수 있지만, 일반 직원은 일부 문서만 볼 수 있습니다.
이때 필요한 것이 메타데이터 필터링입니다.
예를 들어 사용자가 고객지원 챗봇에서 환불 정책을 묻는다고 해 봅시다. 검색 조건은 다음처럼 구성할 수 있습니다.
{
"query": "연간 요금제 환불 가능 기간",
"vector": [0.018, -0.204, 0.091, "..."],
"filters": {
"language": "ko",
"source_type": "help_center",
"product": "pro_plan",
"is_active": true,
"access_level": "public"
},
"top_k": 10
}필터링을 잘 쓰면 검색 품질과 보안이 동시에 좋아집니다. 관련 없는 문서가 후보에 들어오는 것을 줄이고, 사용자가 접근할 수 없는 문서가 LLM 프롬프트에 들어가는 것을 막습니다.
권한 필터링은 특히 중요합니다. LLM에게 "비밀 문서는 말하지 마"라고 지시하는 것으로는 부족합니다. 애초에 비밀 문서가 검색 결과에 들어가지 않게 해야 합니다. RAG에서 보안의 첫 번째 원칙은 "모델에게 보여 주지 말아야 할 문서는 검색 단계에서 제거한다"입니다.
하이브리드 검색: 의미와 키워드를 함께 잡기
벡터 검색은 강력하지만 모든 질문에 완벽하지 않습니다. 특히 제품 코드, 오류 코드, 법 조항 번호, 함수명, 고유명사, 약어처럼 정확한 문자열이 중요한 경우에는 키워드 검색이 더 잘 맞을 수 있습니다.
예를 들어 사용자가 이렇게 묻는다고 합시다.
ERR-4072 오류는 어떻게 해결하나요?임베딩 모델은 ERR-4072라는 코드의 의미를 잘 모를 수 있습니다. 이때는 "ERR-4072"라는 문자열이 정확히 들어 있는 문서를 찾는 키워드 검색이 매우 중요합니다.
반대로 사용자가 이렇게 묻는다면 벡터 검색이 유리합니다.
결제는 됐는데 서비스가 활성화되지 않았어요.문서에는 "결제 완료 후 라이선스 반영 지연"이라고 적혀 있을 수 있습니다. 단어는 다르지만 의미는 가깝습니다. 이런 경우에는 의미 기반 벡터 검색이 잘 동작합니다.
그래서 실전 RAG에서는 두 방식을 함께 쓰는 하이브리드 검색이 자주 사용됩니다. BM25 같은 전통적인 키워드 검색으로 정확한 단어 매칭을 잡고, 벡터 검색으로 의미 유사성을 잡은 뒤, 두 결과를 합칩니다. 여러 생산 환경 RAG 가이드에서도 순수 벡터 검색만 쓰기보다 키워드 검색과 결합하고 재랭킹을 추가하는 구성을 권장합니다.5
간단한 흐름은 다음과 같습니다.
사용자 질문
↓
벡터 검색 상위 20개
+
키워드 검색 상위 20개
↓
결과 병합
↓
중복 제거
↓
재랭킹
↓
최종 5개 청크 선택결과를 합칠 때는 단순히 점수를 더하기도 하고, 각 검색 결과의 순위를 바탕으로 합치는 RRF, 즉 Reciprocal Rank Fusion 같은 방식을 쓰기도 합니다. 여기서 중요한 감각은 "검색은 하나의 기술이 아니라 여러 신호를 조합하는 일"이라는 점입니다.
벡터 검색은 "말은 다르지만 의미가 비슷한 것"을 잘 찾습니다. 키워드 검색은 "반드시 포함되어야 하는 단어"를 잘 찾습니다. 메타데이터 필터는 "검색해서는 안 되는 영역"을 제거합니다. 재랭킹은 "처음 찾은 후보 중 진짜 답에 가까운 것"을 다시 고릅니다. 좋은 RAG는 이 도구들을 상황에 맞게 조합합니다.
문서 갱신: 인덱스는 살아 있어야 한다
RAG에서 문서는 한 번 넣고 끝나는 것이 아닙니다. 제품 정책은 바뀌고, 매뉴얼은 업데이트되고, Notion 페이지는 수정되고, 사내 규정은 개정됩니다. 원본 문서가 바뀌었는데 벡터 인덱스가 예전 상태라면, RAG는 오래된 문서를 근거로 답합니다.
따라서 문서 파이프라인에는 갱신 전략이 필요합니다.
가장 단순한 방식은 전체 재인덱싱입니다. 모든 문서를 다시 읽고, 다시 청킹하고, 다시 임베딩하고, 벡터 DB를 새로 만드는 방식입니다. 문서가 적을 때는 괜찮지만 규모가 커지면 비용과 시간이 많이 듭니다.
운영에서는 보통 증분 인덱싱을 사용합니다. 문서의 수정 시간을 확인하거나 파일 해시를 계산해, 바뀐 문서만 다시 처리합니다. 삭제된 문서는 인덱스에서도 삭제해야 합니다. 문서 ID와 청크 ID를 안정적으로 설계하면 이 과정이 쉬워집니다.
예를 들어 source_id가 refund_policy인 문서가 업데이트되었다면 기존 청크를 삭제하고 새 청크를 넣을 수 있습니다.
def reindex_document(source):
old_chunks = vector_store.find_by_metadata({"source_id": source.id})
vector_store.delete([chunk.id for chunk in old_chunks])
text = load_and_extract(source)
clean_text = clean(text)
chunks = split_into_chunks(clean_text, source.metadata)
embeddings = embed([chunk.text for chunk in chunks])
vector_store.upsert(chunks, embeddings)더 안전한 방식은 새 인덱스를 따로 만든 뒤, 검증이 끝나면 검색 트래픽을 새 인덱스로 전환하는 것입니다. 대규모 서비스에서는 임베딩 모델을 바꿀 때도 이런 전략이 필요합니다. 기존 임베딩과 새 임베딩은 벡터 공간이 다르기 때문에, 같은 인덱스 안에 섞어 넣으면 검색 품질이 깨질 수 있습니다. 그래서 embedding_model 메타데이터를 저장하고, 모델 변경 시 재임베딩 계획을 세워야 합니다.
좋은 문서 파이프라인은 새 문서를 넣는 일뿐 아니라, 낡은 문서를 빼고 바뀐 문서를 다시 넣는 일까지 포함합니다.
PDF 전처리: 페이지, 표, 그림을 잃지 않기
PDF 기반 RAG는 가장 흔하면서도 까다로운 작업입니다. 회사의 매뉴얼, 계약서, 보고서, 논문, 제안서가 PDF로 존재하는 경우가 많기 때문입니다.
PDF에서 중요한 것은 페이지와 레이아웃입니다. 사용자가 답변을 확인하려면 "어느 PDF의 몇 페이지인지"가 필요합니다. 표가 많은 문서에서는 열 제목과 값의 관계가 깨지면 안 됩니다. 그림이나 차트가 핵심인 문서라면 텍스트 추출만으로는 부족할 수 있습니다.
PDF 전처리에서는 다음 전략이 유용합니다.
먼저 페이지 단위 메타데이터를 반드시 저장합니다. 청크가 여러 페이지에 걸치면 시작 페이지와 끝 페이지를 함께 저장합니다. 답변 출처에 페이지를 표시하려면 이 정보가 필요합니다.
다음으로 반복되는 머리말과 꼬리말을 제거합니다. 페이지마다 같은 회사명, 문서명, 보안 문구가 들어가면 청크 품질이 떨어집니다.
표는 가능하면 Markdown 표나 행 중심 텍스트로 보존합니다. 예를 들어 다음 표가 있다고 해 봅시다.
| 요금제 | 월 가격 | 저장 용량 |
|---|---:|---:|
| Basic | 10,000원 | 10GB |
| Pro | 30,000원 | 100GB |
이 표를 단순히 Basic 10,000원 10GB Pro 30,000원 100GB로 추출하면 의미가 흐려집니다. "Pro의 저장 용량은?"이라는 질문에 답하려면 행과 열 관계가 살아 있어야 합니다.
스캔 PDF는 OCR 품질을 확인해야 합니다. 숫자 0과 알파벳 O, 숫자 1과 알파벳 I가 헷갈리면 계약 금액이나 오류 코드가 틀어질 수 있습니다. 중요한 문서라면 OCR 결과를 샘플링해서 사람이 검수하는 과정도 필요합니다.
그림과 차트가 중요한 문서에서는 멀티모달 처리를 고려합니다. Azure GPT-RAG ingestion 같은 도구들은 PDF, 이미지, 스프레드시트, 전사 파일 등 다양한 형식에 맞춰 청킹하고 텍스트 및 이미지 임베딩을 생성하는 방향을 제공합니다.6 모든 프로젝트가 처음부터 멀티모달까지 갈 필요는 없지만, "우리 문서의 핵심 정보가 텍스트에만 있는가?"는 반드시 물어봐야 합니다.
Notion 전처리: 블록 구조를 살리기
Notion은 팀 문서화 도구로 많이 쓰입니다. 하지만 Notion 페이지는 일반 문서처럼 한 덩어리 텍스트가 아닙니다. 제목, 문단, 토글, 콜아웃, 표, 데이터베이스, 체크리스트, 하위 페이지가 블록 단위로 구성됩니다.
Notion을 RAG에 넣을 때는 블록 계층을 살리는 것이 중요합니다. 예를 들어 다음 구조가 있다고 해 봅시다.
고객지원 정책
├─ 환불 정책
│ ├─ 월간 요금제
│ └─ 연간 요금제
└─ 계정 삭제 정책"연간 요금제는 결제 후 7일 이내 전액 환불"이라는 문장만 청크로 저장하면, 이 문장이 환불 정책 안의 연간 요금제 규칙이라는 맥락이 약해집니다. 청크 앞에 페이지 제목과 상위 제목 경로를 붙이면 검색 품질과 답변 품질이 좋아질 수 있습니다.
페이지: 고객지원 정책
섹션: 환불 정책 > 연간 요금제
연간 요금제는 결제일로부터 7일 이내에만 전액 환불할 수 있습니다.토글 블록도 주의해야 합니다. 사람이 볼 때는 접혀 있지만, RAG에는 펼쳐서 넣어야 할 수 있습니다. 데이터베이스형 페이지는 각 행을 하나의 문서로 볼지, 전체 테이블을 하나의 문서로 볼지 결정해야 합니다. 예를 들어 "오류 코드 목록" 데이터베이스라면 각 오류 코드를 하나의 청크로 만드는 편이 검색에 유리합니다.
Notion의 장점은 메타데이터가 풍부하다는 점입니다. 페이지 생성일, 수정일, 작성자, 태그, 데이터베이스 속성, 상위 페이지를 함께 저장할 수 있습니다. 이 정보를 잘 활용하면 최신 문서 우선 검색, 팀별 필터링, 태그 기반 검색이 쉬워집니다.
웹페이지 전처리: 본문만 가져오는 기술
웹페이지를 RAG에 넣을 때 가장 큰 문제는 본문이 아닌 텍스트가 너무 많다는 점입니다. 메뉴, 사이드바, 광고, 댓글, 추천 글, 푸터, 스크립트 텍스트가 함께 추출될 수 있습니다. 이런 노이즈가 임베딩에 섞이면 검색 품질이 떨어집니다.
웹페이지 전처리에서는 먼저 본문 추출이 필요합니다. 뉴스 기사라면 기사 제목, 작성일, 본문, 소제목, 이미지 캡션 정도가 중요합니다. 제품 문서라면 본문과 코드 블록, 표, 경고 박스가 중요합니다. 반면 내비게이션 메뉴와 푸터는 대체로 제거해도 됩니다.
HTML의 제목 계층도 살려야 합니다. h1, h2, h3는 문서 구조를 알려 주는 중요한 신호입니다. 청크마다 해당 제목 경로를 붙이면 검색된 조각만 봐도 문맥을 이해할 수 있습니다.
웹페이지는 변경이 잦으므로 크롤링 주기도 고려해야 합니다. 매일 바뀌는 가격표와 1년에 한 번 바뀌는 회사 소개 페이지를 같은 주기로 재인덱싱할 필요는 없습니다. last_modified, etag, 콘텐츠 해시 등을 이용해 바뀐 페이지에만 임베딩 비용을 쓰는 것이 좋습니다.
또 하나 중요한 것은 robots.txt와 서비스 약관입니다. 공개 웹페이지라고 해서 아무렇게나 수집해도 된다는 뜻은 아닙니다. 특히 외부 웹을 대상으로 하는 RAG라면 법적·윤리적 수집 범위를 검토해야 합니다.
사내 문서 전처리: 권한이 검색 품질만큼 중요하다
사내 문서 기반 RAG는 매력적입니다. "우리 회사 문서를 다 아는 AI 비서"는 누구나 원하는 기능입니다. 하지만 사내 문서에는 민감한 정보가 많습니다. 인사 평가, 연봉, 계약서, 고객 개인정보, 보안 정책, 미공개 제품 계획이 섞여 있을 수 있습니다.
사내 문서 RAG에서 첫 번째 원칙은 권한을 문서 파이프라인에 포함하는 것입니다. 사용자가 질문한 뒤 LLM에게 "권한 없는 내용은 말하지 마"라고 부탁하는 것은 안전하지 않습니다. 검색 단계에서 권한 없는 청크가 아예 나오지 않아야 합니다.
문서 메타데이터에는 다음과 같은 권한 정보를 담을 수 있습니다.
{
"tenant_id": "company_a",
"department": "hr",
"allowed_groups": ["hr_team", "executives"],
"access_level": "confidential"
}검색할 때는 현재 사용자의 그룹과 권한을 기준으로 필터를 겁니다.
filters = {
"tenant_id": current_user.tenant_id,
"allowed_groups": {"$in": current_user.groups},
"is_active": True
}물론 실제 벡터 DB마다 필터 문법은 다릅니다. 중요한 것은 설계 원칙입니다. 권한은 답변 생성 후처리가 아니라 검색 전 조건이어야 합니다.
사내 문서는 중복도 많습니다. 같은 정책이 PDF, Notion, 이메일 공지, 위키에 동시에 존재할 수 있습니다. 이때 오래된 문서와 최신 문서가 함께 검색되면 LLM이 충돌하는 근거를 받습니다. 따라서 문서 버전, 시행일, 폐기 여부, 공식 문서 여부를 메타데이터로 관리해야 합니다.
{
"policy_name": "remote_work",
"effective_from": "2026-03-01",
"is_official": true,
"is_active": true,
"supersedes": "remote_work_2025"
}좋은 사내 RAG는 문서를 많이 넣는 시스템이 아닙니다. 믿을 수 있는 문서를 올바른 권한으로 검색하는 시스템입니다.
작은 문서 파이프라인 코드로 보기
이제 지금까지의 흐름을 아주 단순한 코드 형태로 연결해 보겠습니다. 실제 운영 코드라기보다, 구조를 이해하기 위한 뼈대입니다.
from pathlib import Path
from hashlib import sha256
def load_documents(folder):
documents = []
for path in Path(folder).glob("*.md"):
text = path.read_text(encoding="utf-8")
documents.append({
"source_id": path.stem,
"source_type": "markdown",
"uri": str(path),
"title": path.stem,
"updated_at": path.stat().st_mtime,
"text": text,
})
return documents
def clean_text(text):
text = text.replace("rn", "n")
text = "n".join(line.rstrip() for line in text.splitlines())
return text.strip()
def chunk_by_paragraphs(document, max_chars=1200, overlap_chars=200):
paragraphs = [p.strip() for p in document["text"].split("nn") if p.strip()]
chunks = []
current = ""
for paragraph in paragraphs:
if len(current) + len(paragraph) + 2 <= max_chars:
current += "nn" + paragraph if current else paragraph
else:
chunks.append(current)
tail = current[-overlap_chars:] if overlap_chars > 0 else ""
current = tail + "nn" + paragraph
if current:
chunks.append(current)
result = []
for i, chunk_text in enumerate(chunks):
chunk_id_source = f"{document['source_id']}:{i}:{chunk_text}"
chunk_id = sha256(chunk_id_source.encode("utf-8")).hexdigest()[:16]
result.append({
"chunk_id": chunk_id,
"text": chunk_text,
"metadata": {
"source_id": document["source_id"],
"source_type": document["source_type"],
"uri": document["uri"],
"title": document["title"],
"chunk_index": i,
"updated_at": document["updated_at"],
}
})
return result
def index_folder(folder, embedder, vector_store):
documents = load_documents(folder)
all_chunks = []
for doc in documents:
doc["text"] = clean_text(doc["text"])
chunks = chunk_by_paragraphs(doc)
all_chunks.extend(chunks)
texts = [chunk["text"] for chunk in all_chunks]
vectors = embedder.embed_documents(texts)
for chunk, vector in zip(all_chunks, vectors):
vector_store.upsert(
id=chunk["chunk_id"],
vector=vector,
text=chunk["text"],
metadata=chunk["metadata"]
)이 코드는 매우 작지만 핵심은 들어 있습니다. 문서를 읽고, 정제하고, 문단 기준으로 나누고, 청크 ID와 메타데이터를 붙이고, 임베딩을 만든 뒤, 벡터 저장소에 넣습니다.
여기에 PDF 로더, Notion API, 웹 크롤러, 권한 메타데이터, 증분 갱신, 실패 로그, 재시도, 배치 임베딩, 삭제 처리 등을 붙이면 운영용 문서 파이프라인이 됩니다.
중요한 것은 처음부터 거대한 시스템을 만들 필요가 없다는 점입니다. 작은 파이프라인을 먼저 만들고, 어떤 단계에서 품질 문제가 생기는지 관찰하면서 확장하면 됩니다.
검색 품질은 눈으로 확인해야 한다
문서 파이프라인을 만들면 곧바로 챗봇 화면에서 답변을 확인하고 싶어집니다. 하지만 그전에 반드시 해야 할 일이 있습니다. LLM 답변을 보기 전에 검색 결과 자체를 확인하는 것입니다.
사용자 질문을 몇 개 준비하고, 각 질문에 대해 검색된 상위 청크를 직접 봅니다.
질문: 연간 요금제를 중간에 해지하면 환불되나요?
검색 결과 1:
제품 매뉴얼 v4 > 결제와 환불 > 연간 요금제
연간 요금제는 결제일로부터 7일 이내에만 전액 환불할 수 있습니다...
검색 결과 2:
FAQ > 구독 해지
구독 해지는 계정 설정의 결제 메뉴에서 진행할 수 있습니다...
검색 결과 3:
릴리스 노트 > 2025년 가격 정책 변경
2025년 9월부터 연간 요금제 가격이 변경됩니다...이 결과를 보면 첫 번째는 매우 관련 있고, 두 번째는 일부 관련 있으며, 세 번째는 별로 관련이 없다는 것을 알 수 있습니다. 이런 확인을 반복하면 문제가 보입니다. 청크가 너무 큰지, 제목 정보가 부족한지, 오래된 문서가 자주 올라오는지, 키워드 검색이 필요한지 판단할 수 있습니다.
RAG를 평가할 때는 최종 답변만 보면 원인 분석이 어렵습니다. 답변이 틀렸을 때 그 이유가 검색 실패인지, 청킹 실패인지, 문서 자체의 오류인지, LLM의 해석 오류인지 나누어 보아야 합니다. 그 첫 단추가 검색 결과 점검입니다.
운영 단계에서는 검색 로그를 남겨야 합니다. 질문, 검색된 청크 ID, 유사도 점수, 필터 조건, 최종 프롬프트에 들어간 청크, 답변, 사용자 피드백을 기록하면 품질 개선의 재료가 됩니다.
좋은 문서 파이프라인의 체크리스트
마지막으로 RAG용 문서 파이프라인을 설계할 때 확인할 항목을 정리해 보겠습니다. 모든 항목을 처음부터 완벽히 구현할 필요는 없습니다. 하지만 제품으로 운영하려면 결국 하나씩 마주치게 됩니다.
문서 로딩: PDF, Notion, 웹페이지, 사내 문서 등 주요 소스를 안정적으로 가져오는가
텍스트 추출: 표, 제목, 페이지, 이미지 캡션, 코드 블록 같은 구조가 보존되는가
정제: 반복 문구와 노이즈를 제거하되 의미 구조를 망가뜨리지 않는가
메타데이터: 출처, 위치, 버전, 수정일, 권한, 문서 유형을 저장하는가
청킹: 질문 유형과 문서 구조에 맞는 크기와 경계를 사용하는가
임베딩: 같은 모델과 설정으로 문서와 질문을 일관되게 벡터화하는가
벡터 저장소: 규모, 비용, 필터링, 운영 요구에 맞는 저장소를 선택했는가
검색: 벡터 검색, 키워드 검색, 메타데이터 필터, 재랭킹을 적절히 조합하는가
갱신: 새 문서, 수정 문서, 삭제 문서를 인덱스에 반영하는가
관찰: 검색 결과, 점수, 출처, 사용자 피드백을 로그로 남기는가
이 체크리스트의 핵심은 하나입니다. RAG는 LLM 하나로 완성되는 기능이 아니라, 문서와 검색과 생성이 이어지는 시스템입니다.
이제 검색 가능한 지식 창고가 생겼다
이번 장에서는 RAG의 뒷부분, 즉 문서를 준비하고 저장하는 파이프라인을 살펴보았습니다. 문서 로딩, 텍스트 추출, 정제, 메타데이터, 청킹, 임베딩, 벡터 데이터베이스, 메타데이터 필터링, 하이브리드 검색까지 이어지는 흐름을 하나로 연결했습니다.
좋은 RAG는 모델보다 문서 처리와 검색 설계에서 성패가 갈리는 경우가 많습니다. LLM은 마지막에 문장을 만들어 주지만, 어떤 근거를 읽을지는 파이프라인이 결정합니다. 잘못 자른 문서, 낡은 문서, 출처 없는 문서, 권한 없는 문서가 들어가면 아무리 강한 모델을 써도 신뢰할 수 있는 서비스를 만들기 어렵습니다.
이제 우리에게는 검색 가능한 지식 창고가 생겼습니다. 다음 단계는 이 창고를 실제 챗봇과 연결하는 일입니다. 사용자가 질문하면 관련 청크를 찾고, 프롬프트를 구성하고, 답변과 출처를 함께 보여 주는 문서 기반 Q&A RAG 챗봇을 만들어 볼 차례입니다.
참고
1Build an End-to-End RAG Pipeline for LLM Applications
2Chunk large documents for RAG and vector search in Azure AI Search
4Evaluating Chunking Strategies for Retrieval
5RAG Pipeline Production Guide: From Vector DB Selection to Chunking, Reranking, and Evaluation
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.