본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 25. 05:48

RAG 앱을 직접 구축한 뒤 내가 어떤 차를 좋아하는지 물어봤다. 하지만 앱은 알지 못했다.

요약

Spring AI, pgvector, Ollama를 사용하여 RAG 애플리케이션을 처음부터 구축하며 겪은 시행착오를 다룹니다. 청킹 버그와 모델 성능 한계, 그리고 개발 과정에서의 설정 오류를 해결하는 과정을 공유합니다.

핵심 포인트

  • Spring AI와 pgvector를 활용한 RAG 파이프라인 구축 과정
  • 엔티티 이름 충돌(Document 클래스)로 인한 개발 혼선 해결
  • 로컬 Ollama 프로세스와 Docker 컨테이너 간의 포트 충돌 문제
  • 3B 모델의 성능 한계와 청킹 전략의 중요성

요약 (TL;DR): 처음부터 직접 만든 RAG 앱의 1단계 — Spring AI, pgvector, 로컬 Ollama 사용 — 는 작동하는 파이프라인을 완성하며 마무리되었지만, 겉보기에는 동일해 보이지만 서로 전혀 관련 없는 두 가지 실패를 겪었습니다. 하나는 청킹 (chunking) 버그였고, 다른 하나는 3B 모델의 성능 한계였습니다. 제가 이 둘을 어떻게 구분했는지 소개합니다.

왜 이것을 만드는가

저는 _애플리케이션 개발 IT 전문가_로서의 수습 과정을 마무리하는 중이며, 단순한 CRUD 앱 이상의 포트폴리오 프로젝트를 원했습니다. Kenning은 문서 채팅 도구입니다. 파일을 업로드하고, 그에 대해 질문하면 출처가 첨부된 답변을 얻을 수 있습니다. 표준 RAG (Retrieval-Augmented Generation) 방식이지만, 튜토리얼을 짜깁기하는 대신 처음부터 끝까지 직접 구축했습니다.

0단계는 인프라 구축이었습니다: pgvector/pgvector:pg16ollama/ollama를 포함한 Docker Compose, Spring Boot 스캐폴딩 (scaffold), Angular 스캐폴딩을 준비했습니다. 1단계의 목표는 더 좁지만 더 중요했습니다: 실제 RAG 루프가 작동함을 증명하는 것 — 즉, 문서 하나를 업로드하고, 질문 하나를 던져서, 출처가 첨부된 실제 답변을 받아내는 것입니다. 로그인, UI 다듬기, 다중 문서 처리 등은 고려하지 않았습니다. 오직 이것만 확인합니다: 이 아키텍처가 실제로 작동하는가.

기술 스택

  • Java 21, Spring Boot 4.1.0, Spring AI
  • Angular (프론트엔드, 이 단계에서는 거의 수정하지 않음)
  • PostgreSQL + pgvector (벡터 저장소로 사용)
  • 로컬에서 실행되는 Ollama: 임베딩 (embeddings)을 위한 nomic-embed-text 및 채팅을 위한 llama3.2:3b

파이프라인: 업로드 → Apache Tika가 텍스트 추출 → TokenTextSplitter가 이를 청크 (chunk)로 분할 → Spring AI의 Ollama 임베딩 클라이언트가 각 청크를 벡터로 변환 → pgvector에 저장 → 질문이 동일한 방식으로 임베딩됨 → 유사도 검색 (similarity search)을 통해 가장 유사한 청크를 가져옴 → 해당 청크들을 질문과 함께 ChatClient 호출에 포함 → 모델이 답변하고, 사용된 소스 청크를 첨부함.

이것이 이론적인 과정입니다. 하지만 현실과 마주하며 이 중 어느 것도 순탄하게 넘어가지는 않았습니다.

실수 #1: 내 엔티티 이름을 Document라고 지은 것

Spring AI는 텍스트 청크 (chunk)와 메타데이터 (metadata)를 표현하기 위해 자체적인 Document 클래스를 제공합니다. 저 또한 "사용자가 업로드한 파일"을 나타내기 위해 Document라는 이름의 엔티티 (entity)를 만들고 싶었습니다. 이름은 같지만 완전히 다른 두 가지였고, 자동 완성 (autocomplete) 기능이 잘못 예측할 때마다 짜증 나는 임포트 (import) 모호함이 발생했습니다. 제 것을 SourceDocument로 이름을 바꾸고 넘어갔습니다.

실수 #2: 두 개의 Ollama, 하나의 포트

로컬 모델 (local models)을 테스트하기 위해 코딩 어시스턴트용으로 Windows에서 qwen2.5-coder:14b를 네이티브 (native)로 실행해 두고 잊어버렸습니다. 그런데 Docker Compose 또한 Kenning을 서비스하기로 되어 있는 Ollama 컨테이너 (container)를 위해 11434 포트를 사용하려고 했습니다. 두 프로세스 (process)가 하나의 포트를 점유하니, 결과는 예측 가능했습니다. 해결 방법은 간단했습니다. 컨테이너를 시작하기 전에 네이티브 Windows 프로세스를 중단하는 것이었지만, 에러 메시지 (error message)는 이것이 원인이라는 힌트를 전혀 주지 않았습니다. 만약 동일한 머신에서 로컬 AI 코딩 도구와 Ollama 기반 앱을 동시에 실행한다면 꼭 기억해 둘 만한 사항입니다.

실수 #3 (사실 실수는 아님): 아무것도 하지 않는 GPU

이 머신에는 12 GB의 VRAM을 가진 AMD RX 6700 XT가 장착되어 있지만, 로컬 추론 (inference)을 위해 아무런 일도 하지 않고 있었습니다. 추측하는 대신 직접 확인해 보았습니다:

docker exec -it ollama ollama ps
NAME                       ID              SIZE      PROCESSOR    CONTEXT    UNTIL
llama3.2:3b                a80c4f17acd5    2.6 GB    100% CPU     4096       4 minutes from now
nomic-embed-text:latest    0a109f422b47    376 MB    100% CPU     2048       4 minutes from now

100% CPU 사용, 확인되었습니다. 제가 파악한 바로는, 문제는 ROCm이 컨테이너에 /dev/kfd를 노출해야 하는데 WSL2가 이를 노출하지 않는다는 점입니다. 따라서 이 설정으로는 WSL2 상의 Docker에서 실행되는 Ollama의 GPU 가속 (GPU acceleration)은 막다른 길인 것으로 보입니다.

아직 시도해보지 않은 한 가지는 WSL2/Docker 내부가 아닌 Windows에서 Ollama를 네이티브(natively)로 실행하는 것입니다. Windows 네이티브 Ollama는 WSL2의 패스스루(passthrough) 제한을 거치지 않고 GPU 드라이버에 접근하는 자체적인 경로를 가지고 있으므로, 6700 XT를 실제로 사용할 수 있는 아마도 더 현실적인 방법일 것입니다. 다만 아직 Kenning을 전환하여 테스트해보지는 못했습니다. 현재로서는 모델이 로드되기도 전에 RAM 사용량이 이미 96%에 달하는 머신에서 로컬 개발을 위해 CPU 전용(CPU-only)으로 사용 중입니다.

결국 공개 데모를 진행할 때는 이 점을 솔직하게 밝힐 계획입니다. GPU가 없는 VPS(Virtual Private Server)에서 실행되는 작은 로컬 모델은 느리고 가끔 틀릴 수 있다는 점을 명시할 것이며, 더 나은 답변을 원한다면 사용자의 API 키를 직접 연결할 수 있는 옵션을 제공할 것입니다.

진짜 테스트: 나에 대해 물어보기

루프가 기계적으로 작동하기 시작한 후, 저는 스스로 검증할 수 있는 사실이 담긴 문서가 필요했습니다. 그래서 짧은 약력을 작성했습니다. Kenning이 무엇인지, 어떤 스택을 사용하는지, 그리고 코딩 외에 무엇에 관심이 있는지 등을 포함했으며, 여기에는 다음과 같은 한 줄이 들어 있었습니다: "그는 기계식 키보드를 즐기며 BMW 자동차에 관심이 있다."

질문 1: "이 프로젝트는 어떤 임베딩 모델 (embedding model)을 사용하나요?"

문서에는 명확한 한 문장의 답이 있습니다: "임베딩에는 nomic-embed-text를, 채팅에는 llama3.2:3b를 사용함". 청크(chunk)는 실제로 검색되었고 — sources에 전체 문서가 반환되었습니다 — 따라서 이것은 컨텍스트 누락(missing-context) 문제는 아니었습니다. 모델이 실제로 답변한 내용은 다음과 같습니다:

"이 프로젝트가 구체적으로 어떤 임베딩 모델을 사용하는지는 알 수 없습니다. 텍스트에는 Ollama가 두 가지 모델을 제공한다고 언급되어 있습니다: 임베딩 생성을 위한 nomic-embed-text가 있지만, Kenning 프로젝트에서 어떤 모델이 사용되는지는 명시되어 있지 않습니다."

이 문장을 두 번 읽어보십시오. 모델은 모른다고 말하며 시작하더니, 바로 다음 문장에서 올바른 임베딩 모델인 nomic-embed-text for generating embeddings를 언급합니다. 그러고는 프로젝트에서 어떤 모델을 사용하는지 명시되어 있지 않다고 주장하며 마무리합니다. 모델은 자신의 설명 안에 정답을 올려두고도 여전히 확답을 내리지 못하고 있습니다.

제 생각으로는: 원래 문장이 하나의 절에서 두 개의 모델을 언급하고 있는데, 각각은 다른 작업(

문서가 입력되고, 질문이 입력되면, 소스 청크 (source chunk)가 첨부된 답변이 돌아옵니다. 이 모든 과정은 API 비용이 전혀 들지 않는 완전한 로컬 스택 (local stack)에서 엔드 투 엔드 (end-to-end)로 이루어집니다. 속도는 느리고, 소형 모델 (small models)이 흔히 저지르는 방식의 오류를 가끔 범하며, 청킹 (chunking) 방식은 여전히 단순합니다. 하지만 아키텍처 (architecture)는 견고하게 작동하며, 이를 증명하는 것이 이번 단계의 실제 목적이었습니다.

만약 여러분이 이 정도 수준에서 RAG 파이프라인 (RAG pipelines)을 실제로 디버깅해 본 경험이 있다면, 위에서 언급한 두 가지 가설이 유효한지 진심으로 알고 싶습니다. 즉, 임베딩 모델 (embedding-model) 문제에 대한 속성 결합 (attribute-binding) 설명과, BMW 질문에 대한 청크 희석 (chunk-dilution) 설명 말입니다. 저는 이 현상들을 끝까지 추적하여 얻은 결과가 아니라, 여기서 관찰한 내용을 바탕으로 추론하고 있습니다. 따라서 이 분야에 더 많은 경험이 있으시며, 저의 추측이 틀렸거나, 절반만 맞거나, 혹은 명백한 무언가를 놓치고 있다면 꼭 의견을 듣고 싶습니다.

2단계 (Phase 2)는 인증 (Google OAuth2), 실제 업로드 UI, 그리고 사용자당 멀티 문서 (multi-document) 지원입니다. 이 기능이 작동하게 되면 다시 정리해서 올리겠습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0