30억 개 벡터 유사도 검색 삽질기: OOM과 Jeff Dean, 그리고 진짜 엔지니어링의 문제
최근 재미있는 아티클을 하나 읽었다. Vicki Boykis가 작성한 글인데, Jeff Dean과 누군가가 30억 개의 벡터를 쿼리하는 문제로 나눈 대화에서 이른바 ‘Nerd-snipe’를 당해 직접 구현에 뛰어든 내용이다.
요즘 RAG나 LLM 기반 애플리케이션 때문에 Vector DB가 유행하면서 다들 너무나 쉽게 벡터 검색을 이야기한다. 하지만 막상 그 스케일이 30억 개(3 Billion)가 되면 이야기는 완전히 달라진다. 15년 넘게 엔지니어링을 해오면서 수많은 스케일링 문제를 겪었지만, 이 글은 우리가 기술적 난제에 부딪혔을 때 흔히 저지르는 실수와 진짜 해결해야 할 문제가 무엇인지를 아주 정확하게 짚어내고 있다.
30억 개 벡터의 무게와 Naive한 접근
문제의 정의는 단순하다. 768차원(n)을 가진 30억 개의 문서 벡터가 있고, 1,000개의 쿼리 벡터와 유사도(Dot product)를 비교해야 한다.
저자는 먼저 파이썬으로 아주 순진한(Naive) 이중 루프를 작성했다. 3,000개의 샘플 벡터로 테스트했을 때 약 2초가 걸렸다. 이를 numpy의 벡터화(Vectorization) 연산으로 개선하여 0.017초로 줄였고, 데이터 타입을 float32로 변환해 0.0045초까지 최적화했다.
# 벡터화 연산을 통한 최적화 예시
def get_dot_products_vectorized(vectors_file:np.array, query_vectors:np.array):
dot_products = vectors_file @ query_vectors.T
return dot_products.flatten()
이런 식의 CPU 레벨 최적화는 훌륭하다. 하지만 샘플 사이즈를 30억 개로 늘리는 순간, 완전히 다른 종류의 물리적 한계에 부딪히게 된다.
8.6TB의 벽과 OOM(Out of Memory)
30억 개의 벡터를 메모리에 올리려면 얼마나 많은 RAM이 필요할까?
- 계산식: 3,000,000,000 * 768 * 4 bytes (float32)
- 결과: 약 8.6 TB
당연하게도 로컬 머신에서는 즉각적인 OOM이 발생한다. 솔직히 말해서, 이 지점에서 많은 엔지니어들은 본능적으로 아키텍처를 복잡하게 만들기 시작한다. “데이터를 청크(Chunk) 단위로 쪼개서 제너레이터를 쓰자”, “Memory-mapped file(mmap)을 활용하자”, “Rust나 C++로 포팅하고 SimSIMD 같은 라이브러리를 써서 하드웨어 가속을 받자” 혹은 “Spark로 MapReduce 클러스터를 띄우자” 같은 아이디어들이 쏟아진다.
물론 기술적으로는 모두 맞는 말이다. 하지만 Principal 레벨의 엔지니어라면 여기서 타이핑을 멈추고 한 걸음 물러서야 한다.
진짜 문제는 코드가 아니라 ‘요구사항’이다
저자가 깨달은 가장 중요한 통찰은 이것이다. 기술적인 구현 이전에 요구사항 자체가 너무 모호하다는 것이다. 30억 개의 벡터를 쿼리한다는 것은 비즈니스 맥락에 따라 완전히 다른 시스템 아키텍처를 요구한다.
- 정확도: Exact Match(정확한 내적 연산)가 필요한가, 아니면 ANN(Approximate Nearest Neighbor)으로 충분한가?
- 빈도: 이 쿼리는 1회성(One-off) 배치 작업인가, 아니면 실시간으로 계속 들어오는 운영 환경의 쿼리인가?
- 하드웨어: GPU를 사용할 수 있는 환경인가? 데이터는 이미 메모리에 있는가, 아니면 디스크에서 스트리밍해야 하는가?
- 압축: float32를 int8이나 이진 벡터로 양자화(Quantization)하여 정확도를 희생하고 속도와 메모리를 얻을 수 있는가?
이 질문들에 대답하지 않고 Rust로 SIMD 최적화 코드를 짜는 것은, 목적지도 모르고 페라리를 시속 300km로 모는 것과 같다.
Hacker News의 반응과 실무적인 해결책
Hacker News 스레드에서도 이 점을 정확히 지적하는 의견들이 많았다. 한 유저의 코멘트가 특히 인상 깊었다.
“이 작업이 얼마나 ‘1회성’인지에 따라 정답이 달라집니다. 1회성이라면 디스크에서 Sequential Read를 하는 것이 맞습니다. 그게 아니라면 ANN을 위해 데이터를 인덱싱해야 하는데, 이는 전체 데이터셋에 대해 수많은 쿼리를 수행하는 것과 같은 비용이 듭니다.”
실무적으로 완벽한 조언이다. 만약 단 한 번의 분석을 위한 것이라면, 8.6TB의 데이터를 순차적으로 읽으면서(Streaming) 메모리 사용량을 통제하고, 밤새 배치 스크립트를 돌려놓고 퇴근하는 것이 가장 훌륭한 엔지니어링이다. 분산 시스템을 구축하는 인건비와 시간이 스크립트 실행 시간보다 훨씬 비싸기 때문이다.
반면 실시간 서비스라면 디스크 기반의 벡터 검색을 지원하는 도구를 찾아야 한다. HN 유저가 추천한 usearch 같은 라이브러리는 RAM에 모든 것을 올리지 않고도 디스크 상의 벡터를 효율적으로 검색할 수 있게 해준다. 무작정 값비싼 RAM을 수 테라바이트씩 증설하거나 복잡한 분산 캐시를 구성하기 전에, 이런 디스크 최적화 도구를 검토하는 것이 시니어의 역할이다.
결론 (Verdict)
이 에피소드는 우리에게 아주 중요한 교훈을 준다. 시스템의 병목 현상을 마주했을 때, 우리는 종종 코드를 최적화하거나 새로운 프레임워크를 도입하는 것으로 문제를 해결하려 한다.
하지만 시스템 설계에서 가장 어려운 것은 기술적 난이도가 아니라 요구사항의 경계를 명확히 하는 것이다. 30억 개의 벡터를 쿼리하는 가장 최적의 알고리즘은, 어쩌면 그 쿼리가 진짜로 필요한지, 그리고 어떤 형태의 결과를 기대하는지 프로덕트 오너에게 다시 물어보는 것일지도 모른다.
기술적 깊이와 문제 해결의 본질을 동시에 보여준 훌륭한 아티클이었다. 당장 내일 출근해서 아키텍처 리뷰를 할 때, “이게 정말 필요한가요?”라는 질문을 한 번 더 던져봐야겠다.
References
- Original Article: https://vickiboykis.com/2026/02/21/querying-3-billion-vectors/
- Hacker News Thread: https://news.ycombinator.com/item?id=47231871