[SpringAI] Vector DB와 Spring AI

Raha·4일 전

SpringAI

목록 보기
5/5

들어가며

LLM은 Stateless하기 때문에 매 요청마다 컨텍스트를 직접 전달해야 한다. 그런데 컨텍스트가 길어질수록 토큰 비용이 폭증하고, 필요한 정보만 골라서 전달해야 한다는 문제가 남아 있었다.

이번 글에서는 그 해결책의 핵심인 Vector DB를 다룬다. 아래 세 가지 질문을 중심으로 풀어나간다.

  1. 기존 DB 검색과 Vector DB 검색은 무엇이 다른가?
  2. 텍스트를 어떻게 숫자로 바꾸면 "의미가 비슷하다"는 걸 판단할 수 있는가?
  3. Spring AI와 pgvector로 어떻게 구현하는가?

1. 기존 DB 검색의 한계

MyBatis로 Oracle을 다뤄본 사람이라면 검색 쿼리를 이렇게 짰을 것이다.

SELECT * FROM documents WHERE content LIKE '%병가%'

이 방식은 글자가 일치하는지만 본다. 사용자가 "몸이 안 좋아서 쉬고 싶어"라고 검색했을 때, DB에 "병가 신청 방법"이라는 문서가 있어도 찾지 못한다. 두 표현은 의미상 같은 말이지만 글자가 완전히 다르기 때문이다.

Vector DB는 이 문제를 다르게 접근한다. 글자 일치가 아니라 의미의 유사함으로 검색한다.


2. 임베딩(Embedding): 의미를 숫자로

컴퓨터가 "의미의 유사함"을 판단하려면 텍스트를 숫자로 바꿔야 한다. 이 과정을 임베딩이라고 한다.

원리는 간단하다. 단어를 여러 기준으로 점수 매기는 것이다.

"강아지" 기준별 점수:
- 생물인가?    → 0.9
- 귀여운가?   → 0.8
- 바퀴가 있나? → 0.0

→ 벡터: [0.9, 0.8, 0.0]

"개":           [0.9, 0.8, 0.0]  ← 거의 동일!
"자동차":       [0.1, 0.2, 1.0]  ← 완전히 다름!

두 벡터의 숫자가 비슷하다는 건, 좌표 공간에서 가까운 위치에 있다는 뜻이다. 실제 임베딩 모델은 이 기준(차원)이 3개가 아니라 768~3072개다. 차원이 많아질수록 단어의 의미를 더 세밀하게 표현할 수 있다.---

3. Vector DB 전체 흐름

Vector DB를 쓰는 흐름은 크게 두 단계다: 저장검색.---

4. 왜 pgvector인가

Vector DB 전용 솔루션(Pinecone, Weaviate 등)을 별도로 도입하면 세 가지 문제가 생긴다.

  • 서버를 하나 더 관리해야 한다
  • 비용이 추가로 발생한다
  • 기존 DB와 데이터 동기화 문제가 생긴다

pgvector는 이 문제를 한 줄로 해결한다.

CREATE EXTENSION IF NOT EXISTS vector;

기존 PostgreSQL에 이 한 줄만 추가하면 Vector DB 기능이 생긴다. 새로운 인프라 없이, SQL 문법 그대로, ACID 트랜잭션 보장까지.

-- 벡터 유사도 검색
SELECT content FROM vector_store
ORDER BY embedding <-> '[0.12, 0.05, ...]'
LIMIT 5;

<-> 연산자 하나로 수학적 거리 계산이 처리된다.


5. 테이블 설계: 왜 두 테이블인가

처음 보면 "왜 하나로 합치지 않지?"라는 의문이 생긴다. 답은 간단하다. 1:N 관계이기 때문이다.

vector_documents (1)
└── vector_store (N)
    ├── 청크 1: "병가는 연간..."  [0.9, 0.2, ...]
    ├── 청크 2: "신청 방법은..."  [0.8, 0.3, ...]
    └── 청크 3: "HR팀 연락처..."  [0.7, 0.1, ...]

파일 하나를 쪼개면 청크가 여러 개 나온다. 원본은 vector_documents에, 청크들은 vector_store에 따로 관리한다.

부가적으로, 각 청크의 메타데이터에 document_id를 달아두면 검색 결과가 "어느 파일에서 왔는지" 역추적이 가능하다. 출처 표시가 되는 것이다.


6. 핵심 구현 코드

문서 업로드 흐름

@Transactional
public DocumentUploadResponse uploadDocument(MultipartFile file) throws IOException {
    String content = new String(file.getBytes(), StandardCharsets.UTF_8);

    // 1. 원본 저장
    VectorDocument saved = vectorDocumentRepository.save(
        VectorDocument.builder()
            .fileName(file.getOriginalFilename())
            .content(content)
            .build()
    );

    // 2. 청크 분할 + 임베딩 + Vector DB 저장
    List<Document> chunks = createChunks(content, saved);
    vectorStore.add(chunks); // Spring AI가 임베딩까지 처리

    return DocumentUploadResponse.builder()
        .documentId(saved.getId().toString())
        .chunkCount(chunks.size())
        .build();
}

청크 분할

TextSplitter splitter = new TokenTextSplitter(
    500,   // 청크당 최대 토큰 수
    100,   // 오버랩 크기 (앞뒤 청크와 겹치는 양)
    5,     // 너무 짧은 문장은 제외
    10000, // 파일당 최대 청크 수
    true   // 문단 구분자 유지
);

유사도 검색

List<Document> results = vectorStore.similaritySearch(
    SearchRequest.builder()
        .query(query)
        .topK(5) // 유사도 상위 5개만 반환
        .filterExpression(/* document_id 필터 */)
        .build()
);

7. Top-K: 몇 개를 가져올 것인가

문제점
너무 작을 때 (1~2)필요한 정보 누락 → 불완전한 답변
적당할 때 (3~5)정확도와 비용의 균형점
너무 클 때 (10+)관련 없는 청크 유입 → 환각 증가, 비용 폭증

3~5가 권장값이다.


마치며

Vector DB의 핵심은 "글자 일치"에서 "의미 유사도"로의 패러다임 전환이다. 임베딩으로 텍스트를 벡터로 바꾸고, 그 벡터 간 거리를 계산해서 의미적으로 가까운 데이터를 찾는다.

profile
Backend Developer | Aspiring Full-Stack Enthusiast

0개의 댓글