“무무의 선물상자”에 벡터 스토어(Qdrant)를 도입해 의미 유사도 기반 추천/중복 판별을 적용하려고 한다. 현재 인프라는 EC2 프리티어(t2.micro, 1 vCPU / 1GB RAM) + MySQL(EC2 내). 같은 인스턴스에 Spring Boot + Qdrant(Docker)를 함께 띄우면 메모리가 빠듯하다. 이 상태에서 서비스가 죽지 않게 버틸 수 있도록 제안된 방법이 바로 스왑(Swap)이다. 하지만 스왑은 만능이 아니다. 스왑을 켤지, 아니면 t3.small(2GB RAM) 등 상위 사양으로 올릴지 운영/비용/성능 관점에서 고민이 필요하다.
스왑은 운영체제에서 RAM이 부족할 때 디스크(EBS) 일부를 임시 메모리처럼 사용하는 영역이다. 즉, 자주 쓰이지 않는 메모리 페이지를 디스크로 옮겨 RAM 공간을 확보하고 필요할 때 다시 디스크에서 불러온다. 이 과정을 paging in/out이라고 하며 실제 RAM처럼 빠르진 않지만 갑작스러운 메모리 부족 상황에서 프로세스가 죽는 것을 막아주는 안전장치 역할을 한다.
쉽게 말하면 RAM은 빠른 작업 공간, 스왑은 창고 같은 보조 공간이다.
창고에 물건을 넣고 꺼내는 과정이 느리듯이 스왑은 RAM보다 수십~수백 배 느리다. 그래서 스왑에 의존하면 성능은 떨어지지만 없는 것보다는 서비스가 죽지 않는다는 점에서 운영 안정성 측면에서 의미가 있다.
t2.micro
같은 저메모리 환경에서 프로세스 생존성↑현재는 프리티어 환경에서 비용을 최소화하면서도 Qdrant를 붙여 품질을 검증해 보는 것이 목표다. 당장 인스턴스 사양을 올려버리면 매달 수만 원의 추가비용이 발생하는데 아직은 사용자가 많지 않고 데이터 규모도 크지 않다. 따라서 일단은 스왑으로 메모리 부족으로 인한 서비스 다운을 방지하는 것이 합리적이다. 특히 Qdrant는 러스트로 작성되어 메모리 효율이 좋지만 Spring Boot와 MySQL까지 같이 돌아가면 순간적인 메모리 피크는 언제든지 생길 수 있다. 이런 상황에서 스왑은 최소한의 안전망 역할을 해 줄 수 있다.
생성 (2GB 예시)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab
상태 확인
swapon -s
free -h
튜닝 (스왑 사용 성향 낮추기)
# 현재 swappiness 확인
cat /proc/sys/vm/swappiness
# 일시 변경 (10 권장: RAM 우선 사용, 억지로 스왑 사용 줄이기)
sudo sysctl vm.swappiness=10
# 영구 반영
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
vm.swappiness
낮을수록 RAM을 끝까지 쓰고 정말 부족할 때만 스왑 사용한다.
제거(롤백)
sudo swapoff /swapfile
sudo rm /swapfile
sudo sed -i '/\/swapfile swap swap defaults 0 0/d' /etc/fstab
mkdir -p ~/qdrant_storage
docker run -d --name qdrant \
-p 127.0.0.1:6333:6333 -p 127.0.0.1:6334:6334 \
-v ~/qdrant_storage:/qdrant/storage \
-e QDRANT__STORAGE__PERFORMANCE__MAX_SEARCH_THREADS=1 \
-e QDRANT__CLUSTER__ENABLED=false \
-e QDRANT__STORAGE__OPTIMIZERS__MAX_SEGMENT_SIZE=200000 \
--cpus="0.7" --memory="700m" --memory-swap="2g" \
qdrant/qdrant:latest
컬렉션(1536차원, Cosine)
curl -X PUT "http://localhost:6333/collections/products" \
-H "Content-Type: application/json" \
-d '{"vectors":{"size":1536,"distance":"Cosine"}}'
Spring Boot는 JVM 위에서 실행되므로 힙 메모리 크기를 제한하는 것이 중요하다. 프리티어(t2.micro)는 전체 메모리가 1GB밖에 없으므로 JVM에 힙을 과도하게 주면 다른 프로세스(Qdrant, MySQL)가 사용할 메모리가 모자라 금방 OOM이 난다.
힙 크기 제한
실행 옵션에서 -Xmx512m
또는 -Xmx768m
로 설정해 전체 1GB 중 절반~3/4만 JVM이 쓰도록 제한한다. 나머지는 OS, Docker(Qdrant), MySQL이 나눠 갖게 해 균형을 맞춘다.
Qdrant 쿼리 최적화
검색 시 limit=20~50
정도로 결과 건수를 제한하고 with_vectors=false
(벡터는 제외) + with_payload=true
(상품 메타만 반환) 옵션을 조합해 응답 크기를 최소화한다. 우리는 상품 id, 가격, 카테고리, 태그 정도만 필요하므로 불필요한 데이터는 받지 않는 것이 효율적이다.
임베딩 업서트(batch 처리)
처음 대량으로 데이터를 적재할 때는 한 번에 수천 건을 넣지 말고 200~500건 단위로 나눠서 upsert 해야 한다. 이렇게 하면 Qdrant 인덱스 빌드 시 메모리 피크가 발생하는 것을 막을 수 있다. 변경분 동기화는 이벤트 기반 또는 스케줄러로 작게, 자주 실행하는 것이 안정적이다.
JVM GC 튜닝
기본적으로 G1GC가 적절하지만 응답 지연을 줄이고 싶다면 -XX:+UseG1GC
를 명시하고 너무 큰 힙을 주지 않는 선에서 GC 효율을 확보하는 게 좋다.
폴백 전략
혹시라도 Qdrant 응답이 지연되거나 실패할 경우를 대비해 기존의 Jaccard/키워드 기반 검색을 폴백으로 두면 안정성을 높일 수 있다.
운영 중에는 스왑 사용률과 메모리 상태를 꾸준히 확인하는 것이 중요하다.
free -h
, vmstat 1
→ 메모리와 스왑 in/out 체크mem_used_percent
, swap_used
지표를 수집해 메모리와 스왑 사용량을 시각화스왑 사용량이 계속 높다면 이미 한계에 다다른 것이므로 업그레이드를 고려해야 한다. 또한 애플리케이션 레벨에서는 QPS, p95/p99 응답 시간, 에러율을 추적하여 사용자 체감 성능 저하가 없는지 점검해야 한다.
스왑은 어디까지나 임시방편이다. 순간적인 피크를 버티는 데는 유용하지만 지속적으로 스왑에 의존하면 결국 응답 속도가 느려지고 사용자 경험이 악화된다. 반면 상위 스펙 업그레이드는 비용이 들지만 안정적인 메모리 환경을 보장한다. 따라서 데이터 규모가 작고 테스트/검증 단계라면 스왑으로 버티는 것이 합리적이고 운영 트래픽이 늘어나거나 스왑 사용률이 높아지는 상황에서는 t3.small
이상으로 올리는 것이 정답이다. 즉, 비용을 아끼는 것과 안정성을 확보하는 것 사이의 트레이드오프이며 지표 기반으로 판단해야 한다.
현재는 프리티어(t2.micro
) 환경에서 비용을 아끼면서 빠르게 Qdrant의 효과를 검증하는 것이 목표다. 따라서 우선은 스왑 2GB 추가 + Qdrant 절약 세팅 + Spring Boot 메모리 제한으로 시작하는 것이 맞다. 이후 실제 사용자 트래픽이 늘어나고 추천 품질을 개선하기 위해 임베딩 데이터가 수만 건 이상으로 커진다면 그때는 t3.small(2GB RAM)이나 더 높은 사양으로 자연스럽게 업그레이드할 계획이다. 결론적으로 지금은 “스왑으로 버티고 지표를 보면서 시점을 잡아 업그레이드” 하는 전략이 합리적이다.
스왑은 성능 최적화가 아니라 서비스 생존성 확보용 안전망이라는 것을 다시 한 번 느꼈다. 작은 비용으로 당장 서비스를 죽지 않게 만들 수 있지만 장기적으로는 성능 저하라는 대가를 치르게 된다. 결국 인프라 설계에서는 단기적인 비용 절감과 장기적인 안정성 확보 사이에서 균형을 잡는 것이 중요하다. 이번 고민을 통해 단순히 기술적인 방법뿐 아니라 운영 환경과 비용까지 고려해야 비로소 현실적인 아키텍처 선택이 가능하다는 점을 배웠다.