
RAG 코드 검색 정확도를 55%에서 95%로 높인 방법
요약
Spring Boot 3.4와 Spring AI를 활용하여 Java 코드베이스 검색 도구인 GitGrok을 구축한 사례를 소개합니다. 단순 임베딩 기반 RAG의 한계를 분석하고, 검색 정확도를 55%에서 95%로 높이기 위한 개선 과정을 다룹니다.
핵심 포인트
- 기본 시맨틱 검색은 코드의 결정론적 정밀함을 반영하지 못함
- 테스트 파일이 운영 코드보다 높은 순위로 검색되는 오염 문제 발생
- 단순 임베딩 방식의 한계를 극복하기 위한 의도 기반 검색 엔진 도입
- Spring AI를 활용한 Java 기반 RAG 시스템 구축 방법론
Spring Boot 3.4와 Spring AI를 사용하여 Java 코드베이스 검색 도구인 GitGrok을 구축하고, 표준 RAG 대신 의도를 이해하는 검색 엔진에 가까운 방식을 도입하여 검색 정확도를 95%까지 끌어올린 과정에 대하여.
"단순 임베딩 및 검색(just embed and retrieve)" 방식이 실제 코드에서 왜 실패하는지, 그리고 그 대신 무엇을 구축했는지에 대한 실질적인 분석.
만약 여러분이 기본적인 검색 증강 생성 (RAG, Retrieval Augmented Generation) 애플리케이션을 구축해 본 적이 있다면, 다음과 같은 일반적인 흐름을 알고 있을 것입니다:
- 텍스트를 청크 (Chunk) 단위로 나눕니다.
- 벡터 임베딩 (Vector Embeddings)을 생성 (Generate) 합니다.
- 해당 임베딩을 벡터 데이터베이스 (Vector Database)에 저장 (Store) 합니다.
- 유사도 검색 (Similarity Search)을 실행 (Run) 합니다.
이 방식은 PDF, 기사, 위키와 같은 텍스트 기반 문서에는 놀라울 정도로 잘 작동합니다. 하지만 소스 코드에 적용하려고 했을 때는 완전히 실패했습니다.
개발자들이 일반적인 영어로 자신의 저장소(Repository)와 대화할 수 있게 해주는 도구인 GitGrok을 구축할 때, 저는 사용자 경험이 단순하기를 원했습니다. 즉, 코드베이스에 대해 질문하면 즉각적인 답변을 얻는 것이죠. 간단해 보이지 않나요?
하지만 그렇지 않았습니다.
처음에는 Java 소스 코드를 Pinecone 벡터 데이터베이스에 밀어 넣고 기본적인 검색 엔드포인트를 구축했습니다. 이 기본 시맨틱 검색 (Semantic Search)은 품질이 낮고, 노이즈가 많으며, 일관성이 없고, 종종 완전히 틀린 검색 결과를 내놓았습니다. 제가 어떻게 문제를 진단하고 해결했는지 그 과정을 소개합니다.
코드베이스에서 순수 시맨틱 검색이 실패하는 이유
시맨틱 검색은 정확한 키워드 매칭 대신 문맥을 인식하는 검색(의미에 집중)을 의미합니다. 이는 강력해 보이지만, 코드베이스는 결정론적인 정밀함 (Deterministic Precision)을 요구합니다.
파일 수준의 기본 임베딩을 사용하여 GitGrok을 처음 구현했을 때, 두 가지 치명적인 결함이 나타났습니다:
1. 테스트 파일 오염 함정 (The Test File Pollution Trap)
벡터 데이터베이스(Vector Database)는 실제 운영 코드(Production Code)보다 테스트 클래스를 지속적으로 훨씬 더 높은 순위로 배치했습니다. 이는 테스트 파일이 본질적으로 더 장황하고 반복적인 반면, 운영 소스 코드 파일은 압축적이고 추상적이기 때문에 발생했습니다.
2. 의미론적 표류 및 혼동 (Semantic Drift and Confusion)
사용자가 다음과 같이 쿼리(Query)하는 경우를 가정해 보겠습니다: “소유자 등록을 담당하는 컨트롤러를 보여줘.”
• 표류 (The Drift): 시맨틱 검색(Semantic Search)이 실제 비즈니스 로직인 OwnerController.java를 반환하는 대신, Owner.java(도메인 엔티티) 또는 OwnerControllerTests.java(테스트 스위트)를 검색했습니다.
• 원인 (The Cause): 엔티티와 테스트 파일 모두
1. 테스트 클래스 완전히 제외 (Drop Test Classes Entirely)
데이터 수집 (Ingestion) 단계에서 테스트 디렉토리와 문서를 완전히 제외하여, 인덱스가 오직 프로덕션 (Production) 파일에만 엄격하게 집중되도록 했습니다.
2. 메서드 단위 청킹 (Chunk by Method)
밀접하고 국소적인 의미론적 문맥 (Semantic context)을 보존하기 위해, 클래스를 별개의 기능적 블록 (메서드) 단위로 나누었습니다.
3. 코드 인지적 용어 가중치 부여 (Code-Aware Term Weighting)
순수 의미론적 검색 (Semantic search)만으로는 충분하지 않았기에, 의미론적 의미와 정확한 구문 (Syntax)을 결합해야 했습니다. GitGrok이 임베딩 (Embeddings)에만 의존할 수 없다는 것을 깨닫고, 문맥을 위한 밀집 벡터 (Dense vectors)와 정확한 용어를 위한 희소 벡터 (Sparse vectors)를 결합하는 하이브리드 검색 (Hybrid Search) 방식으로 전환했습니다.
간단히 말해, 우리의 하이브리드 검색은 두 가지 검색 방식을 결합합니다:
• 밀집 벡터 (Dense Vectors, 의미론적 검색): 임베딩 모델이 코드를 고차원 벡터로 변환합니다. 이는 기저에 깔린 개념과 의도를 이해하는 데 탁월합니다.
• 희소 벡터 (Sparse Vectors, 키워드 검색): 토큰 빈도에 따라 문서의 점수를 매기는 메커니즘으로, 전통적인 검색 엔진처럼 작동합니다. 이는 정확한 일치 (Exact matches) 및 정밀한 변수/메서드 이름을 찾는 데 탁월합니다.
점수 산정 문제 해결 (Solving the Scoring Problem)
하이브리드 파이프라인에서 밀집 검색과 희소 검색 방식을 결합할 때, 핵심적인 수학적 난관에 부딪히게 됩니다. 바로 두 방식의 점수 산정 척도 (Scales)가 완전히 다르다는 점입니다.
밀집 검색은 0.0에서 1.0 사이의 점수를 반환하는 반면, 희소 키워드 검색은 1에서 100 이상의 점수를 반환합니다. 이를 단순히 더할 수는 없습니다 (예: 0.85 + 45.0). 만약 그렇게 한다면, 가공되지 않은 키워드 빈도가 의미론적 지능을 완전히 압도해 버릴 것입니다.
이 '사과와 오렌지'를 비교하는 듯한 문제를 해결하기 위해, 저는 데이터를 저장하기 전 희소 벡터에 **코드 인지적 토큰 승수 (Code-aware token multipliers)**를 적용하여 수집 단계에서 척도를 정규화 (Normalized)했습니다:
토큰 유형 (Token Type) 가중치 (Weight)
메서드 시그니처 (Method signatures) 3.0x
클래스 이름 (Class names) 2.5x
...
이러한 조정을 통해 데이터베이스는 쿼리가 실행되기도 전에 본질적으로 구조를 인지 (Structure-aware)할 수 있게 되었습니다.
2단계: 더 스마트한 검색 (Phase 2: Smarter Retrieval)
다음은 쿼리 처리 (query handling) 단계입니다. 사용자의 가공되지 않은(raw) 쿼리를 데이터베이스에 직접 전달하는 대신, 먼저 지능형 필터링 계층을 거치도록 경로를 설정했습니다.
1. 의도 탐지 (Intent Detection)
먼저, 시스템은 쿼리를 분석하여 사용자가 정확히 어떤 유형의 리소스를 원하는지 파악합니다. detectQueryType()이라는 헬퍼 메서드에서 미리 컴파일된 정규 표현식 (regex) 매칭 패턴을 활용하여 특정 구조적 의도를 분리해냅니다.
2. 파일명 추출 및 메타데이터 필터링 (Filename Extraction and Metadata Filtering)
의도가 식별되면, 시스템은 쿼리에서 (언급된 경우) 대상 파일명을 추출하고 메타데이터 필터 맵 (metadata filter map)을 생성합니다. 만약 의도가 메서드 조회 (method lookup)라면, 시스템은 데이터베이스 검색 범위를 symbolType: method/class로 태그된 청크 (chunk)로만 독점적으로 제한하여, 검증기 (validators), 리포지토리 (repositories), 팩토리 (factories)를 완전히 건너뜁니다. 이는 무관한 청크를 제거하여 지연 시간 (latency)을 대폭 줄여줍니다.
3. 알파 스케일 하이브리드 검색 (Alpha-Scaled Hybrid Search)
하이브리드 검색 (hybrid search)은 희소 검색 (sparse retrieval)과 밀집 검색 (dense retrieval)을 결합하지만, 무작정 결합하면 대개 한쪽이 다른 쪽을 압도하게 됩니다. 이를 해결하기 위해, 저는 $\alpha$ (alpha) 파라미터를 조정하여 정확한 균형을 제어함으로써 파이프라인을 최적화했습니다.
광범위한 실험 끝에, 저는 $\alpha = 0.6$을 최적의 지점 (sweet spot)으로 결정했습니다:
• 밀집 검색 (Dense)에 60% 가중치 부여: 원시 임베딩 (embedding) 좌표에 $\alpha$ (0.6)를 곱하여 개발자의 의도를 포착합니다.
• 희소 검색 (Sparse)에 40% 가중치 부여: 쿼리 토큰 빈도 점수에 $(1 - \alpha)$ (0.4)를 곱하여 결과가 정확한 코드 구문 (syntax)에 엄격하게 결합되도록 유지합니다.
이 스케일링 (scaling)은 페이로드 (payload)가 앱을 떠나기 전 애플리케이션 계층 내부에서 수행됩니다. Pinecone은 균형이 미리 보정된 상태로 밀집 벡터와 희소 벡터를 단일 페이로드에 담아 함께 전달받습니다.
참고: α는 저장된 문서 벡터가 아니라, 매 요청마다 쿼리 벡터 (query vectors)를 동적으로 스케일링합니다. 저장된 벡터는 인제스션 타임 (ingestion-time) 가중치에 의해 고정된 상태로 유지됩니다.
결과: 95% 정확도, 환각 (Hallucinations) 제로
두 가지 변화가 모든 차이를 만들었습니다: 코드를 깔끔한 메서드 크기의 청크 (chunks)로 분할한 것, 그리고 이를 정밀하게 조정된 하이브리드 검색 퍼널 (hybrid search funnel)과 결합한 것입니다. 이 두 가지가 결합되어 검색 정확도를 95%까지 끌어올렸습니다.
마침내 이토록 깨끗한 컨텍스트 (context)가 LLM에 도달하게 되면서, 저는 프롬프트 엔지니어링 (prompt engineering) 레이어에서 한 가지 엄격한 규칙을 강제할 수 있을 만큼 확신을 얻었습니다:
"만약 스니펫 (snippet)이 제공된 컨텍스트에 명시적으로 포함되어 있지 않다면, '찾을 수 없음 (Not Found)'이라고 말하세요. 추측하지 마세요."
이것은 완벽하게 작동했습니다. 환각 (Hallucinations)이 급감했습니다. 이는 LLM이 마법처럼 똑똑해졌기 때문이 아니라, 검색 엔진 (retrieval engine)이 그 엄격한 제약 조건을 뒷받침할 수 있을 만큼 신뢰할 수 있게 되었기 때문입니다. 저는 솔직하게 "찾을 수 없음"이라고 고백하는 시스템이, 자신 있게 코드를 지어내는 시스템보다 무한히 더 가치 있다는 것을 배웠습니다.
성능 지표 (Performance Metrics)
지표 (Metric) 이전 (Before) 이후 (After) 변화 (Change)
검색 정확도 (Retrieval Accuracy) 55% 95% +73%
환각 (Hallucinations) 40% <5% −87%
...
전체 아키텍처 (The Full Architecture)
핵심 요약 (Key Takeaways)
• 의미는 구조가 아니다: GitGrok을 구축하며 배운 점은 표준적인 시맨틱 검색 (semantic search)은 텍스트의 "분위기 (vibe)"만을 찾는다는 것입니다. 코드는 그런 방식으로 작동하지 않습니다. 저장소의 노이즈를 뚫고 나가려면, 메타데이터 필터 (metadata filters)와 엄격한 하이브리드 (벡터 + 키워드) 제약 조건을 통해 상황을 확실히 고정해야 합니다.
• 파편에서 그래프로 (From fragments to graphs): 현재 GitGrok은 고립된 코드 스니펫 (code snippets)을 가져오지만, 이들이 어떻게 연결되는지는 완전히 매핑하지 못합니다. 다음 단계는 AST (Abstract Syntax Tree, 추상 구문 트리) 기반의 그래프를 구축하여, 컨트롤러에서 데이터베이스 계층까지 API 요청을 추적하는 것과 같이 깊은 의존성 (dependencies)을 추적하는 것입니다.
• "이유"가 중요합니다 (The "Why" matters): 만약 GitGrok에게 왜 특정 설정값이 0.6으로 설정되었는지 묻는다면, 그 맥락 (context)은 소스 코드 외부에 존재하기 때문에 답변할 수 없습니다. 향후 반복 작업 (iterations)에서는 Git 커밋 히스토리 (commit histories)와 PR (Pull Request) 코멘트를 가져와 코드 라인 뒤에 숨겨진 인간의 결정 사항들을 드러낼 것입니다.
여러분의 의견을 들려주세요
매우 기술적이거나 구조화된 데이터셋을 위한 RAG 시스템을 구축할 때 이와 유사한 검색 함정 (retrieval traps)에 빠진 적이 있나요? 여러분의 청킹 (chunking) 및 하이브리드 최적화 전략에 대해 아래 댓글로 이야기를 나누어 봅시다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
