RAG란 무엇인가: 긴 문맥 모델 시대에도 검색 증강이 필요한 경우와 필요 없는 경우

전체 문서를 넣을지 검색해서 넣을지 판단하는 기준과, 이 사이트 글 78건으로 만든 최소 검색 예제

RAG가 여전히 필요한 이유 썸네일

핵심 요약

  1. 지식 문서가 작고 자주 바뀌지 않으면 RAG 대신 전체를 넣고 프롬프트 캐싱을 쓰는 방식이 더 단순하다.
  2. 문서가 크거나, 비용·지연이 중요하거나, 출처 표시·권한 구분이 필요하면 RAG가 필요하다.
  3. 긴 문맥의 중간에 있는 정보는 활용도가 떨어진다는 연구가 있어, 관련 조각만 고르는 것에도 의미가 있다.
  4. 단순 키워드 검색은 관계없는 조각을 섞으므로 하이브리드 검색과 리랭킹으로 보완한다.

RAG(Retrieval-Augmented Generation, 검색 증강 생성)는 질문이 들어오면 먼저 관련 문서 조각을 찾고, 그 조각을 프롬프트에 넣어 모델이 답하게 하는 방식이다. 모델이 수십만 토큰을 한 번에 읽을 수 있게 되면서 "그냥 문서를 다 넣으면 되지 않나"라는 질문이 자주 나온다. 실제로 문서가 작으면 RAG가 필요 없다. 이 글은 RAG가 필요한 경우와 필요 없는 경우를 나누는 기준을 정리하고, 이 사이트의 글을 대상으로 최소 검색 예제를 실행해 RAG의 첫 단계가 무엇을 하는지 보여 준다.

문서를 전부 넣어도 되는 경우

Anthropic은 Contextual Retrieval 글에서 지식 문서가 20만 토큰(약 500쪽)보다 작다면 RAG 없이 전체를 프롬프트에 넣으면 된다고 설명하고, 프롬프트 캐싱을 쓰면 지연 시간이 2배 이상 줄고 비용이 최대 90% 줄어든다고 밝혔다. 사내 FAQ 몇십 개, 제품 매뉴얼 한 권처럼 작고 자주 바뀌지 않는 문서라면 검색 파이프라인을 만드는 대신 전체를 넣고 캐싱하는 것이 단순하다.

RAG가 필요한 네 가지 경우

조건전체를 넣을 때의 문제RAG가 하는 일
문서가 컨텍스트 한도보다 크다애초에 다 넣을 수 없다질문과 관련된 조각만 고른다
요청마다 비용·지연이 중요하다모든 요청이 전체 문서 토큰을 읽는다조각 몇 개만 읽어 입력 토큰을 줄인다
답의 근거를 보여 줘야 한다어느 부분을 근거로 답했는지 추적하기 어렵다검색된 조각의 출처를 함께 반환한다
문서가 자주 바뀌거나 사용자마다 볼 수 있는 범위가 다르다프롬프트를 매번 다시 만들어야 하고 권한 구분이 어렵다색인만 갱신하고, 검색 단계에서 권한을 거른다

긴 문맥에도 약점이 있다. Lost in the Middle 연구는 관련 정보가 입력의 처음이나 끝에 있을 때 성능이 가장 높고, 긴 문맥의 중간에 있을 때는 긴 문맥용 모델에서도 성능이 크게 떨어진다고 보고했다. 관련 조각만 골라 앞쪽에 두는 RAG는 이 문제를 줄이는 방법이기도 하다.

최소 검색 예제: 이 사이트 글 78건

RAG의 첫 단계는 문서를 조각(청크)으로 나누고, 질문과 가까운 조각을 찾는 것이다. 아래 스크립트는 이 사이트의 공개 글을 H2 소제목 단위로 나누고, 한글에도 동작하도록 문자 2-gram TF-IDF로 질문과 가까운 조각 3개를 찾는다. 임베딩 모델이나 벡터 데이터베이스 없이 동작하는 가장 단순한 형태다.

// 사용법: node mini-rag.mjs <posts.js 경로> "질문"
// 글을 H2 단위로 청크로 나누고, 한글에 맞게 문자 2-gram TF-IDF로 상위 3개 청크와 출처를 찾는다.
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

const [postsPath, query] = process.argv.slice(2);
const posts = require(postsPath).filter((p) => p.status === 'published');
const strip = (h) => h.replace(/<[^>]+>/g, ' ').replace(/&[a-z]+;/g, ' ').replace(/\s+/g, ' ').trim();

// 1) H2 기준 청크 분할: 출처 표시용으로 slug와 소제목을 함께 저장한다.
const chunks = [];
for (const p of posts) {
  const parts = p.bodyHtml.split(/(?=<h2)/);
  for (const part of parts) {
    const heading = (part.match(/<h2[^>]*>(.*?)<\/h2>/) || [, '도입부'])[1].replace(/<[^>]+>/g, '');
    const text = strip(part);
    if (text.length > 80) chunks.push({ source: `${p.slug} › ${heading}`, text });
  }
}

// 2) 문자 2-gram 토큰화 (형태소 분석기 없이 한글 검색이 되게 하는 가장 단순한 방법)
const grams = (s) => {
  const t = s.toLowerCase().replace(/[^0-9a-z가-힣]+/g, ' ');
  const out = [];
  for (const w of t.split(' ')) for (let i = 0; i < w.length - 1; i++) out.push(w.slice(i, i + 2));
  return out;
};
const df = new Map();
const vecs = chunks.map((c) => {
  const tf = new Map();
  for (const g of grams(c.text)) tf.set(g, (tf.get(g) || 0) + 1);
  for (const g of tf.keys()) df.set(g, (df.get(g) || 0) + 1);
  return tf;
});
const idf = (g) => Math.log((chunks.length + 1) / ((df.get(g) || 0) + 1));

// 3) 질문과 청크의 TF-IDF 점수
const q = [...new Set(grams(query))];
const scored = vecs.map((tf, i) => {
  const len = [...tf.values()].reduce((a, b) => a + b, 0);
  const score = q.reduce((s, g) => s + ((tf.get(g) || 0) / len) * idf(g), 0);
  return { i, score };
}).sort((a, b) => b.score - a.score).slice(0, 3);

const totalChars = chunks.reduce((s, c) => s + c.text.length, 0);
console.log(`글 ${posts.length}건 → 청크 ${chunks.length}개, 전체 ${totalChars.toLocaleString()}자`);
console.log(`질문: ${query}`);
scored.forEach(({ i, score }, rank) => {
  console.log(`${rank + 1}. [${score.toFixed(4)}] ${chunks[i].source}`);
  console.log(`   ${chunks[i].text.slice(0, 90)}…`);
});

2026-10-07에 이 사이트의 data/posts.js로 실행한 결과다.

$ node mini-rag.mjs data/posts.js "스택 PR에서 아래 PR을 squash 병합하면 충돌이 나는 이유"
글 78건 → 청크 479개, 전체 245,562자
질문: 스택 PR에서 아래 PR을 squash 병합하면 충돌이 나는 이유
1. [0.2236] github-ai-generated-pr-stacking-review-workflow › 아래 PR을 squash 병합한 뒤의 정리
2. [0.1457] github-ai-generated-pr-stacking-review-workflow › 스택 만들기와 PR 올리기
3. [0.1176] claude-code-subagents-role-separation-guide › 3단계: 탐색 → 구현 → 리뷰 순서로 실행하기
$ node mini-rag.mjs data/posts.js "MCP 서버에 OAuth 인가를 붙일 때 필수 구현 항목"
글 78건 → 청크 479개, 전체 245,562자
질문: MCP 서버에 OAuth 인가를 붙일 때 필수 구현 항목
1. [0.3666] mcp-server-oauth-permission-basics › 역할부터 맞추기: 서버는 리소스 서버다
2. [0.3095] mcp-2026-07-28-spec-stateless-migration-guide › 세션이 사라진 뒤의 보안 포인트
3. [0.2293] mcp-server-oauth-permission-basics › 도입부

결과에서 읽을 수 있는 것

  • 입력 크기: 글 전체는 24만 5천여 자인데, 검색 결과 3개 조각은 그중 1%도 안 된다. 모든 질문에 전체를 넣는 대신 이 정도만 넣으면 입력 토큰이 크게 준다. 다만 이 사이트 규모라면 위의 20만 토큰 기준 안쪽일 수 있어, 전체를 넣고 캐싱하는 방식도 비교 대상이 된다.
  • 출처 표시: 결과마다 글 slug와 소제목이 붙어 있어 답변에 근거 링크를 달 수 있다.
  • 단순 검색의 한계: 첫 질문의 3위 결과(서브에이전트 글)는 "리뷰", "순서" 같은 단어가 겹쳤을 뿐 질문과 관계가 없다. 글자 단위 일치만 보는 검색은 이런 오답을 섞는다.

3위처럼 엉뚱한 조각이 섞이면 모델이 그 내용까지 근거로 쓸 수 있다. 실무에서는 키워드 검색과 임베딩 검색을 결합하고 리랭킹으로 순서를 다시 매긴다. Anthropic의 같은 글은 자체 평가에서 Contextual Embeddings와 Contextual BM25를 함께 쓰면 검색 실패율이 49%(5.7%→2.9%), 리랭킹까지 더하면 67%(5.7%→1.9%) 줄었다고 보고했다. 구현 방법은 하이브리드 검색과 리랭킹, 임베딩의 원리는 벡터 임베딩과 코사인 유사도에서 다룬다.

도입 여부를 정하는 순서

  1. 대상 문서의 전체 토큰 수를 잰다. 기준(예: 20만 토큰)보다 작고 자주 바뀌지 않으면 전체 넣기 + 캐싱부터 시도한다.
  2. 답변에 출처 표시가 필요한지, 사용자마다 볼 수 있는 문서가 다른지 확인한다. 둘 중 하나라도 해당하면 RAG 쪽으로 기운다.
  3. 질문 20~30개와 정답 근거 위치를 적은 평가셋을 만든다.
  4. 단순 검색으로 시작해 평가셋에서 근거를 몇 개나 찾는지 잰다. 부족하면 하이브리드 검색과 리랭킹을 추가한다.
  5. 검색 결과에 없는 내용을 답하지 않게 하는 검증 단계를 붙인다. 방법은 LLM 환각을 줄이는 검증에서 다룬다.

초보자가 자주 실수하는 포인트

  • 문서 몇십 쪽짜리 서비스에 벡터 데이터베이스부터 도입한다.
  • 반대로 수천 쪽 문서를 매 요청 전체로 넣어 비용과 지연을 키운다.
  • 검색 결과의 출처를 저장하지 않아 답변 근거를 보여 줄 수 없다.
  • 검색 품질을 평가셋 없이 몇 개 질문으로만 판단한다.

체크리스트

  • 대상 문서의 전체 토큰 수를 쟀다.
  • 출처 표시와 사용자별 권한 요구를 확인했다.
  • 질문과 정답 근거 위치로 평가셋을 만들었다.
  • 검색 결과에 출처(문서·섹션)를 함께 저장한다.
  • 검색 품질이 부족할 때 하이브리드 검색·리랭킹을 추가할 계획이 있다.

자주 묻는 질문

긴 문맥 모델이 더 커지면 RAG는 사라지나요?

문서가 작을 때는 이미 RAG 없이 전체를 넣는 방식이 가능합니다. 다만 문서가 컨텍스트보다 크거나, 요청마다 비용을 줄여야 하거나, 출처와 권한을 다뤄야 하는 경우에는 관련 조각을 고르는 단계가 계속 필요합니다.

이 글의 2-gram 검색을 실제 서비스에 써도 되나요?

구조를 이해하기 위한 예제입니다. 결과에서 보듯 관계없는 조각이 섞이므로, 실제 서비스에서는 BM25 같은 키워드 검색과 임베딩 검색을 결합하고 리랭킹을 붙이는 것이 일반적입니다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.