벡터 DB 스케일링 - 샤딩과 파티셔닝
지난 글에서 LLM 비용을 모니터링하고 절감하는 전략을 다뤘습니다. 이번에는 비용이 아니라 규모의 문제를 다룹니다. 문서 수가 계속 늘어나 단일 pgvector 인스턴스로는 감당이 안 되는 시점이 오면, 벡터 DB를 어떻게 확장해야 하는지 정리합니다.
1. 단일 인스턴스의 한계는 언제 오는가
지난 pgvector 실습과 HNSW 인덱스 글에서 다룬 구조는 문서 수가 수십만~수백만 건 수준까지는 충분히 버팁니다. 하지만 그 이상 늘어나면 다음과 같은 신호가 나타납니다.
- 인덱스 크기가 메모리 용량을 초과해 디스크 스왑 발생
- HNSW 인덱스 구축(재구축) 시간이 지나치게 길어짐
- 검색 쿼리의 P99 응답 시간이 서서히 늘어남
- 벡터 INSERT가 몰릴 때 인덱스 갱신 부하로 쓰기 지연 발생
지난 부하 테스트 글에서 다룬 모니터링 지표 중 "DB 커넥션 풀 대기 시간"이 꾸준히 늘어난다면, 단일 인스턴스 확장(Vertical Scaling)만으로는 한계에 다다랐다는 신호일 수 있습니다.
2. 확장 전에 먼저 확인할 것 - 진짜 스케일링이 필요한가
샤딩은 운영 복잡도를 크게 높이는 결정이기 때문에, 다음을 먼저 점검하고 나서 도입을 검토하는 것이 순서입니다.
| 점검 항목 | 확인 내용 |
|---|---|
| 청킹 전략 재검토 | 8편에서 다룬 청크 크기가 너무 작아 불필요하게 청크 수가 많아진 건 아닌지 |
| 오래된 문서 정리 | 더 이상 쓰이지 않는 문서가 벡터 DB에 그대로 쌓여있지 않은지 |
| 인덱스 파라미터 튜닝 | HNSW의 m, ef_construction 값이 데이터 규모에 맞게 조정됐는지 |
| 수직 확장 여력 | 인스턴스 스펙(RAM, CPU)을 더 키울 여지가 남아있는지 |
이런 것들로 해결되지 않을 만큼 데이터가 늘어난 경우에만 샤딩·파티셔닝을 고려하는 것이 합리적입니다.
3. 파티셔닝 - 하나의 DB 안에서 데이터 분할하기
샤딩(여러 서버로 분산)보다 먼저 고려할 수 있는 것이 파티셔닝입니다. 같은 DB 인스턴스 안에서 테이블을 논리적으로 쪼개는 방식입니다. 지난 접근 제어 글에서 다룬 테넌트 구조를 활용하면 자연스러운 분할 기준이 됩니다.
CREATE TABLE documents (
id BIGSERIAL,
tenant_id BIGINT NOT NULL,
content TEXT,
embedding VECTOR(1536),
created_at TIMESTAMP DEFAULT now()
) PARTITION BY LIST (tenant_id);
CREATE TABLE documents_tenant_1 PARTITION OF documents FOR VALUES IN (1);
CREATE TABLE documents_tenant_2 PARTITION OF documents FOR VALUES IN (2);
CREATE INDEX ON documents_tenant_1 USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON documents_tenant_2 USING hnsw (embedding vector_cosine_ops);
테넌트별로 파티션을 나누면, 검색 쿼리가 WHERE tenant_id = 1 조건과 함께 들어올 때 PostgreSQL이 자동으로 해당 파티션만 스캔합니다(Partition Pruning). 각 파티션의 HNSW 인덱스도 더 작아져서 구축·검색 속도가 함께 개선됩니다.
4. 시간 기준 파티셔닝 - 오래된 데이터 분리하기
문서가 시계열 성격(뉴스, 로그, 티켓 등)을 띤다면, 시간 단위로도 파티셔닝할 수 있습니다.
CREATE TABLE documents_archive (
id BIGSERIAL,
content TEXT,
embedding VECTOR(1536),
created_at TIMESTAMP
) PARTITION BY RANGE (created_at);
CREATE TABLE documents_2026_q2 PARTITION OF documents_archive
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
CREATE TABLE documents_2026_q3 PARTITION OF documents_archive
FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');
최근 데이터가 검색에 훨씬 자주 쓰인다면, 최신 파티션만 항상 메모리에 올라가 있도록 운영하고 오래된 파티션은 조회 빈도가 낮은 콜드 스토리지 성격으로 분리할 수 있습니다.
5. 샤딩 - 여러 서버로 분산하기
파티셔닝으로도 감당이 안 되는 규모라면, 여러 물리 서버(인스턴스)로 데이터를 나누는 샤딩이 필요합니다. 애플리케이션 레이어에서 어떤 샤드로 요청을 보낼지 라우팅하는 로직을 직접 구현하는 것이 일반적입니다.
@Component
public class VectorShardRouter {
private final Map<Integer, VectorStore> shardMap;
public VectorShardRouter(List<VectorStore> shards) {
this.shardMap = IntStream.range(0, shards.size())
.boxed()
.collect(Collectors.toMap(i -> i, shards::get));
}
public VectorStore resolve(Long tenantId) {
int shardIndex = (int) (tenantId % shardMap.size());
return shardMap.get(shardIndex);
}
}
@Service
@RequiredArgsConstructor
public class ShardedSearchService {
private final VectorShardRouter shardRouter;
public List<Document> search(Long tenantId, String question) {
VectorStore targetStore = shardRouter.resolve(tenantId);
return targetStore.similaritySearch(SearchRequest.query(question).withTopK(5));
}
}
테넌트 ID를 해시해서 특정 샤드로 매핑하는 방식입니다. 이렇게 하면 각 샤드는 전체 데이터의 일부만 담당하기 때문에, 인덱스 크기와 검색 부하가 샤드 수만큼 분산됩니다.
6. 샤딩 도입 시 함께 고민해야 할 문제들
샤딩은 확장성을 얻는 대신 다음과 같은 복잡도를 함께 떠안게 됩니다.
| 문제 | 설명 |
|---|---|
| 샤드 간 검색 (Cross-shard Search) | 특정 테넌트에 한정되지 않은 전역 검색이 필요하면, 모든 샤드에 쿼리를 보내고 결과를 합쳐야 함 |
| 리샤딩 | 샤드 하나에 데이터가 쏠리면 재분배가 필요한데, 이 작업 자체가 대규모 마이그레이션임 |
| 트랜잭션 일관성 | 여러 샤드에 걸친 작업의 원자성 보장이 어려워짐 |
| 운영 복잡도 | 모니터링, 백업, 장애 대응 지점이 샤드 수만큼 늘어남 |
public List<Document> globalSearch(String question) {
return shardRouter.getAllShards().parallelStream()
.flatMap(store -> store.similaritySearch(
SearchRequest.query(question).withTopK(5)).stream())
.sorted(Comparator.comparingDouble(Document::getSimilarityScore).reversed())
.limit(5)
.toList();
}
전역 검색이 자주 필요한 서비스라면, 애초에 테넌트 기준 샤딩이 맞는 선택인지부터 다시 검토해야 합니다. 지난 접근 제어 글에서 다룬 것처럼 대부분의 RAG 검색이 테넌트 범위 안에서만 이뤄진다면 이 문제는 크게 줄어듭니다.
7. 확장 전략 선택 가이드
| 상황 | 권장 접근 |
|---|---|
| 데이터 규모가 크지만 아직 여유 있음 | 인덱스 튜닝, 청킹 재검토, 수직 확장 |
| 테넌트별 데이터가 명확히 분리됨 | 파티셔닝 (같은 인스턴스 내) |
| 시계열 데이터이며 최신 데이터 위주로 검색됨 | 시간 기준 파티셔닝 |
| 단일 인스턴스로 물리적 한계에 도달 | 샤딩 (애플리케이션 레벨 라우팅) |
8. 정리
- 벡터 DB 확장은 파티셔닝(같은 인스턴스 내 분할)부터 검토하고, 그래도 부족할 때 샤딩(여러 서버 분산)으로 넘어가는 순서가 안전합니다.
- 테넌트 기준 파티셔닝은 지난 접근 제어 구조와 자연스럽게 맞물려서, 파티션 프루닝으로 검색 성능까지 함께 개선됩니다.
- 샤딩은 확장성을 주는 대신 전역 검색, 리샤딩, 운영 복잡도라는 비용이 따르므로, 실제로 필요한 시점인지 먼저 데이터로 확인하고 결정해야 합니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 30 - 문서 라이프사이클 관리 (0) | 2026.07.27 |
|---|---|
| STEP 29 - RAG 시스템 재해 복구(DR) 전략 (0) | 2026.07.27 |
| STEP 27 - RAG 시스템 비용 모니터링 (0) | 2026.07.24 |
| STEP 26 - 버전 관리와 카나리 배포 (0) | 2026.07.23 |
| STEP 25 - RAG 감사 로그 (1) | 2026.07.23 |