들어가며
지난 글에서는 RAG의 기본 개념과, 사서(검색기)와 LLM이 협업하여 팩트 기반의 답변을 내놓는 원리를 알아보았습니다. 이번에는 이 RAG 아키텍처를 실제 제가 개발 중인 '게임 무물봇' 프로젝트에 어떻게 적용했는지 그 배경을 이야기해보려 합니다.
보통 'RAG'라고 하면 사내 규정집, PDF 매뉴얼, 블로그 포스팅 같은 비정형 텍스트 문서를 검색하는 것을 떠올립니다. 하지만 게임 데이터의 세계는 조금 다릅니다. 사용자들은 "스킬 2레벨 효과가 뭐야?"라고 묻기도 하지만, 대부분 "특정 캐릭터의 현재 장비 레벨 평균은 얼마야?", "오늘 경매장에서 특정 아이템의 최저가는 얼마야?"처럼 숫자와 조건, 그리고 집계가 필요한 질문을 던집니다.
이러한 정보는 텍스트 문서가 아니라 관계형 데이터베이스 안에 정형 데이터로 저장되어 있습니다. 벡터 유사도 검색만으로는 수많은 아이템 가격의 평균을 내거나 특정 레벨 이상의 유저만 필터링할 수 없었습니다.
1. Text-to-SQL 도입
이 문제를 해결하기 위해 도입한 기술이 바로 Text-to-SQL입니다. 단어 뜻 그대로, 사용자의 자연어 질문을 LLM이 이해하고 이를 데이터베이스에 직접 날릴 수 있는 SQL 쿼리문으로 번역해 주는 기술입니다.
사용자가 "로아 경매장에서 보석 시세 알려줘"라고 입력하면, LLM이 이를 분석해
SELECT price FROM auction_table WHERE item_category = 'gem'
같은 쿼리를 생성하고, 챗봇 시스템이 DB에서 실제 데이터를 뽑아와 사용자에게 자연스러운 문장으로 대답해 주는 구조입니다.
하지만 이 기특한 Text-to-SQL 기술을 실제 서비스에 적용하려고 하니, 거대한 딜레마에 빠지게 되었습니다.
2. 스키마 주입의 딜레마: 돈을 쓸 것인가, 할루시네이션을 견딜 것인가
LLM이 정확한 SQL을 짜려면, 현재 우리 DB에 어떤 테이블이 있고 어떤 컬럼이 있는지(스키마 정보)를 알아야 합니다. 이 스키마 정보를 프롬프트에 어떻게 주입하느냐를 두고 뼈아픈 시행착오를 겪었습니다.
- Phase 1. 전체 스키마 주입
- 처음에는 데이터베이스의 모든 테이블과 컬럼 정보를 프롬프트에 담아 LLM에게 던졌습니다. 정확도는 83%로 나쁘지 않았지만, 토큰 비용이 폭발했습니다. 질문 한 번에 기준점 대비 5,800개가 넘는 토큰이 소모되었습니다. 트래픽이 늘어나면 감당할 수 없는 구조였습니다.
- Phase 2. LLM 자체 판단
- 비용을 줄이기 위해 스키마 주입을 최소화하고 LLM의 추론 능력에 기대보았습니다. 토큰 소모량은 극적으로 줄었지만(92% 절감), 치명적인 문제가 발생했습니다. LLM이 존재하지도 않는 테이블이나 컬럼을 지어내어 쿼리를 짜는 할루시네이션(Hallucination)이 발생한 것입니다.
3. 문서 대신 '스키마'를 RAG로 검색하다
비용과 정확도, 두 마리 토끼를 잡기 위해 고민하던 중 아이디어가 떠올랐습니다. "일반적인 RAG처럼, 전체 스키마를 다 주지 말고 사용자의 질문과 연관된 테이블 스키마만 '검색'해서 LLM에게 넘겨주면 어떨까?"
- Phase 3. In-memory Vector Search 활용
- 테이블 메타데이터를 임베딩하여 메모리에 올려두고, 질문이 들어올 때마다 코사인 유사도를 계산해 필요한 스키마만 추출해 프롬프트에 넣어주었습니다. 결과는 대성공이었습니다. 토큰은 81% 절감되었고, 엉뚱한 테이블을 참조하는 일 없이 스키마 매핑 정확도는 100%를 달성했습니다.
하지만 인프라 환경에서 새로운 복병이 튀어나왔습니다. 저는 서버리스 환경인 GCP Cloud Run을 사용 중이었는데, 요청이 없을 때 컨테이너가 내려갔다가 다시 켜지는 과정에서 엄청난 지연이 발생했습니다. 컨테이너가 기동될 때마다 임베딩된 벡터 데이터를 애플리케이션 메모리에 다시 로드해야 했고, 이 초기 로딩에만 2,520ms가 소요되는 'Cold Start' 병목이 발생한 것입니다.
4. pgvector: RDB와 Vector DB의 관리 포인트 통합
앱단에서 벡터를 메모리에 올리는 방식은 서버리스 환경과 맞지 않았습니다. 벡터 인덱스를 외부에 영구적으로 보관해야 했지만, 이를 위해 별도의 Vector DB를 추가로 구축하는 것은 인프라 관리 관점에서 오버스펙이라고 판단했습니다. 이 제약을 해결하기 위해 대안으로 선택한 것이 pgvector입니다.
- Phase 4. DB-native Vector의 도입
- pgvector는 기존에 사용 중이던 PostgreSQL 환경을 그대로 활용해 벡터 데이터를 저장하고 검색할 수 있게 해주는 확장 모듈입니다. 스키마 임베딩 데이터를 DB에 직접 저장하고, 애플리케이션은 단순히 쿼리만 요청하도록 구조를 변경했습니다.
- 이렇게 아키텍처를 수정하자, 컨테이너 기동 시 애플리케이션 메모리에 벡터 인덱스를 로드하는 과정 자체가 생략되면서 2,520ms에 달하던 Cold Start 지연 문제를 근본적으로 해결할 수 있었습니다. 결과적으로 토큰 소모량 최적화(84% 절감)와 검색 정확도 100%를 유지하면서, 초기 응답 지연이라는 서버리스 환경의 인프라 병목 현상까지 개선한 파이프라인을 구축하게 되었습니다.
마치며
이번 프로젝트는 일반적인 텍스트 문서 검색을 넘어, 'DB 스키마'를 검색 대상으로 삼는 RAG 아키텍처를 구축한 사례였습니다. 사용자의 자연어 질문 의도에 맞춰 필요한 테이블과 컬럼 정보만 동적으로 추출하여 LLM에 제공함으로써, Text-to-SQL 변환 과정의 토큰 비용을 최적화하고 할루시네이션을 억제할 수 있었습니다. 또한 pgvector를 활용한 스토리지 통합은 인프라 추가 없이 관리 포인트를 일원화하고 시스템 복잡도를 낮추는 합리적인 선택이었습니다.
하지만 연관된 테이블 스키마를 정확히 제공한다고 해서, 확률 모델인 LLM이 항상 무결점의 SQL 문법을 생성하는 것은 아니었습니다. 유저가 "뭉가 2렙"이라고 하든 "뭉가 2단계"라고 하든 동일하고 안정적인 쿼리를 만들어내는 추가적인 제어 장치가 필요했습니다. 다음 포스팅에서는 이러한 LLM의 쿼리 생성 결과를 일관되게 통제하기 위해 적용한 'Few-shot 프롬프팅과 메타데이터 매핑' 전략에 대해 다루어보겠습니다.
'AI' 카테고리의 다른 글
| [Claude] 서브에이전트에서 에이전트 팀으로 (0) | 2026.05.21 |
|---|---|
| [Claude] 멀티에이전트 Spark 학습 구축기 (0) | 2026.05.19 |
| [Claude 가이드] 나만의 Claude 사용 팁 (0) | 2026.05.13 |
| [Text-to-SQL 구축기] LLM의 확률적 한계 극복: Vector Similarity와 In-Context Learning의 결합 (0) | 2026.05.06 |
| RAG 기초 — LLM의 한계를 극복하는 가장 현실적인 방법 (0) | 2026.05.01 |