임베딩 모델 교체 시 벡터 데이터 마이그레이션하기
지난 글에서 RAG 챗봇에 캐싱과 비동기 처리를 적용해 트래픽에 대응하는 방법을 다뤘습니다. 서비스를 운영하다 보면 언젠가 한 번은 마주치는 이슈가 있습니다. 더 성능 좋은 임베딩 모델이 나오거나, 비용 절감을 위해 다른 모델로 바꿔야 할 때입니다. 이번 글에서는 임베딩 모델을 교체할 때 왜 기존 벡터를 그대로 쓸 수 없는지, 그리고 어떻게 안전하게 마이그레이션하는지 정리합니다.
1. 왜 임베딩 모델을 바꾸면 벡터를 다시 만들어야 하는가
임베딩은 모델마다 벡터 공간 자체가 다릅니다. 같은 문장이라도 모델이 다르면 완전히 다른 좌표에 매핑되기 때문에, 서로 다른 모델로 만든 벡터끼리는 비교 자체가 의미가 없습니다.
"재택근무는 주 2회까지 가능합니다"
text-embedding-3-small → [0.021, -0.184, 0.093, ...] (1536차원)
text-embedding-3-large → [0.008, -0.201, 0.077, ...] (3072차원, 값도 다름)
차원 수부터 다른 경우도 있고, 설령 차원이 같더라도 모델이 학습한 방식이 다르면 벡터 공간의 좌표 체계 자체가 다릅니다. 그래서 질문을 새 모델로 임베딩해서 기존 모델로 저장된 문서 벡터와 비교하면, 유사도 계산 결과가 완전히 뒤틀립니다.
2. 잘못 마이그레이션하면 생기는 문제
가장 흔한 실수는 새 임베딩 모델을 코드에서만 바꾸고, DB에 저장된 기존 벡터는 그대로 두는 경우입니다.
[잘못된 상황]
기존 문서 벡터: text-embedding-3-small로 생성, 그대로 유지
신규 질문 벡터: text-embedding-3-large로 생성
→ 서로 다른 벡터 공간끼리 코사인 유사도를 계산
→ 검색 결과가 전혀 관련 없는 문서로 나오거나, 유사도 점수 자체가 무의미해짐
이 문제는 배포 직후 바로 터지지 않고, "요즘 챗봇 답변이 이상한데?"라는 문의가 들어오고 나서야 뒤늦게 발견되는 경우가 많아서 더 위험합니다.
3. 마이그레이션 기본 원칙 - 재임베딩
결론부터 말하면, 임베딩 모델을 바꿀 때는 예외 없이 기존 문서 전체를 새 모델로 다시 임베딩해야 합니다. 벡터만 변환하는 지름길은 없습니다.
[원본 텍스트는 그대로 보존되어 있어야 함]
documents 테이블: id, content, embedding, created_at
마이그레이션 절차
1. content(원본 텍스트)를 새 모델로 재임베딩
2. 새로운 embedding 컬럼 또는 새 테이블에 저장
3. 검증 후 전환
여기서 왜 지난 실습 글들에서 content 원본 텍스트를 항상 벡터와 함께 저장해두었는지가 중요해집니다. 원본 텍스트가 남아있어야 모델을 바꿀 때 처음부터 다시 임베딩할 수 있습니다. 벡터만 저장하고 원본 텍스트를 지웠다면, 마이그레이션 자체가 불가능해집니다.
4. 무중단으로 전환하기 - 컬럼 병행 운영
서비스를 멈추지 않고 전환하려면, 기존 벡터 컬럼을 유지한 채 새 모델용 컬럼을 별도로 추가해서 병행 운영하는 방식이 안전합니다.
ALTER TABLE documents
ADD COLUMN embedding_v2 VECTOR(3072);
@Service
@RequiredArgsConstructor
public class EmbeddingMigrationService {
private final JdbcTemplate jdbcTemplate;
private final EmbeddingModel newEmbeddingModel;
@Async("migrationExecutor")
public void migrateBatch(int batchSize) {
List<DocumentRow> rows = jdbcTemplate.query(
"SELECT id, content FROM documents WHERE embedding_v2 IS NULL LIMIT ?",
new Object[]{batchSize},
(rs, rowNum) -> new DocumentRow(rs.getLong("id"), rs.getString("content"))
);
for (DocumentRow row : rows) {
float[] newVector = newEmbeddingModel.embed(row.content());
jdbcTemplate.update(
"UPDATE documents SET embedding_v2 = ? WHERE id = ?",
new PGvector(newVector), row.id()
);
}
}
}
embedding_v2 IS NULL 조건으로 아직 처리되지 않은 문서만 배치 단위로 계속 처리하도록 만들면, 지난 성능 최적화 시리즈에서 다룬 Spring Batch의 청크 단위 처리와 같은 방식으로 대량 재임베딩을 끊어서 진행할 수 있습니다.
5. 검색 로직 전환
전체 문서의 재임베딩이 끝나기 전까지는, 검색 시 기존 컬럼과 신규 컬럼 중 무엇을 쓸지 분기해서 안전하게 넘어갑니다.
public List<Document> search(String question) {
long remaining = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM documents WHERE embedding_v2 IS NULL", Long.class);
if (remaining > 0) {
return legacyVectorStore.similaritySearch(question);
}
return newVectorStore.similaritySearch(question);
}
마이그레이션이 아직 진행 중이라면 기존 검색 경로를 그대로 사용하고, 전체 문서의 재임베딩이 끝난 시점에만 새 컬럼 기반 검색으로 전환합니다. 이렇게 하면 마이그레이션 도중에도 서비스는 끊김 없이 기존 방식으로 계속 응답할 수 있습니다.
6. 전환 전 검증하기
지난 글에서 다룬 RAGAS 평가를 여기서도 그대로 활용할 수 있습니다. 새 임베딩 모델로 전체 전환하기 전에, 동일한 평가 질문 세트로 기존 모델과 신규 모델의 지표를 비교합니다.
| 지표 | 기존 모델 (v1) | 신규 모델 (v2) |
|---|---|---|
| Context Precision | 0.81 | 0.88 |
| Context Recall | 0.76 | 0.85 |
| Faithfulness | 0.89 | 0.91 |
지표가 실제로 개선됐는지 확인하고 나서 전환해야, "최신 모델이니까 당연히 더 좋겠지"라는 가정만으로 전체 서비스를 옮기는 리스크를 피할 수 있습니다.
7. 전환 완료 후 정리
새 컬럼 기반 검색이 안정화되면, 기존 컬럼과 사용하지 않는 임베딩 클라이언트 설정을 정리합니다.
ALTER TABLE documents DROP COLUMN embedding;
ALTER TABLE documents RENAME COLUMN embedding_v2 TO embedding;
바로 삭제하기보다는, 일정 기간(예: 2주) 롤백 가능성을 열어둔 채 운영하다가 문제가 없으면 정리하는 순서를 권장합니다.
8. 정리
- 임베딩 모델을 바꾸면 벡터 공간 자체가 달라지므로, 기존 벡터를 재사용하지 않고 원본 텍스트로 반드시 재임베딩해야 합니다.
- 원본 텍스트를 항상 벡터와 함께 저장해두는 것이 마이그레이션 가능 여부를 결정하는 전제 조건입니다.
- 컬럼을 병행 운영하며 배치 단위로 재임베딩하고, RAGAS 지표로 검증한 뒤 전환하면 서비스 중단 없이 안전하게 모델을 교체할 수 있습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 17 - 대화형 RAG 챗봇 구현하기 (0) | 2026.07.14 |
|---|---|
| STEP 16 - Hybrid Search (0) | 2026.07.13 |
| STEP 14 - RAG 챗봇 트래픽 대응하기 (0) | 2026.07.10 |
| STEP 13 - Spring Boot에 RAG 연동하기 (0) | 2026.07.10 |
| STEP 12 - 프롬프트 엔지니어링 (0) | 2026.07.09 |