iwinv VM 한 대로 어디까지 가능한가 — 6개월 측정치
iwinv 2vCPU/2GB RAM VM 한 대로 결제·정산·자동화 서비스를 6개월 운영한 실측 데이터와, 한계선을 알고 버티는 운영 전략을 정리한다.
결제·정산 운영팀이 단일 VM에 집착하는 이유
결제와 정산을 다루는 작은 팀에게 인프라 비용은 단순한 지출 항목이 아니다. 서비스 하나가 추가될 때마다 VM을 한 대씩 늘리면 고정비가 선형으로 올라가고, 그 압박은 결국 제품 개발 속도를 잠식한다. HEDVION은 정산 자동화 API, 결제 연동 레이어, 운영 대시보드를 국내 클라우드 iwinv의 2vCPU / 2GB RAM / SSD 50GB VM 한 대 위에서 6개월째 운영하고 있다. 이 선택은 절약을 위한 것이기도 하지만, 동시에 "이 사양이 실제 production을 얼마나 소화하는가"를 직접 검증하는 실험이기도 했다.
iwinv는 국내 소형 클라우드 중 가격 대비 스펙이 합리적인 편이다. 그러나 합리적인 스펙이 "실운영 가능한 스펙"을 보증하지는 않는다. 6개월간 쌓인 측정치는 "소화 가능하지만, 조건이 있다"는 명확한 답을 내놓는다. 그 조건을 모르고 운영하면 예고 없이 서비스가 멈추고, 알고 운영하면 생각보다 많은 것을 이 사양 안에서 처리할 수 있다.
6개월 실측 데이터: 숫자로 읽는 한계선
메모리는 평상시 1.4~1.6GB를 유지한다. 그러나 "평상시"의 정의가 중요하다. 배포 직후 JVM 워밍업 구간에서 1.9GB까지 치솟는 피크가 반복적으로 관측됐다. 2GB RAM만으로는 이 순간을 버티지 못한다. swap 2GB를 추가한 이유가 바로 이것이다. swap이 없었다면 OOM 킬러가 Java 프로세스를 종료했을 것이고, "배포했더니 앱이 죽어 있는" 상황이 반복됐을 것이다. swap은 임시 완충재이지 영구 해결책이 아니다. 워밍업 중 swap 사용량이 200MB 이상 올라가는 순간이 관측됐는데, 이는 디스크 I/O 레이턴시가 메모리 레이턴시보다 수백 배 느리다는 점에서 응답 시간에 간접적 영향을 준다.
CPU는 평균 515% 점유로 여유가 있어 보인다. 그러나 정산 배치가 실행되는 새벽 시간대에 5070%까지 올라간다. 2vCPU 환경에서 70%는 사실상 코어 하나가 포화 상태라는 의미다. 이 구간에 배포나 인덱스 재구성이 겹치면 API 응답 지연이 측정 가능한 수준으로 발생한다. 스토리지는 6개월 동안 로그 누적으로 약 18GB를 소비했다. 50GB 디스크에서 18GB는 36%다. logrotate 없이 1년을 운영했다면 디스크 풀로 인한 서비스 중단이 거의 확실했다. 네트워크는 일 20GB 제한 이내에서 운영되고 있으나, 대용량 정산 파일 전송이나 미디어 파일 서빙이 추가되는 시점에는 CDN 분리가 필수 조건이 된다.
결제·정산 서비스 특성이 이 스펙과 만나는 지점
결제·정산 서비스는 트래픽 패턴이 예측 가능하다는 특수성이 있다. 소비자 대상 커머스처럼 갑작스러운 트래픽 버스트가 없고, 정산 배치는 정해진 시간에 돌며, API 호출도 파트너사 수가 고정돼 있다. 이 예측 가능성이 단일 VM 운영을 현실적으로 가능하게 하는 핵심 조건이다. 불특정 다수를 향한 커머스 서비스였다면 이 사양은 훨씬 빨리 한계에 부딪혔을 것이다.
반면 리스크가 집중되는 구간도 명확하다. MySQL에 정산 데이터가 누적되면서 슬로우 쿼리가 발생하면 CPU와 메모리를 동시에 잡아먹는 이중 압박이 생긴다. 6개월간 인덱스 최적화를 두 차례 진행했는데, 이 작업 자체가 피크 시간을 피해야 하는 제약을 수반했다. 또한 Redis를 캐시와 세션 저장소로 동시에 쓰고 있어, Redis가 재시작되는 순간 Java 앱의 세션이 날아가는 의존성 문제가 잠재해 있다. Docker Compose의 depends_on만으로는 이 순서 문제를 완전히 해결할 수 없었고, 명시적인 healthcheck 조건을 추가해서 보완해야 했다.
단일 VM 운영의 핵심 패턴 세 가지
배포 타이밍 고정: JVM 워밍업 메모리 스파이크와 정산 배치가 겹치는 구간은 시스템이 가장 취약해지는 순간이다. 배포 시간을 트래픽 최저 구간(새벽 3~5시 또는 점심 직후)으로 고정하고, 배포 직후 30분간 배치 스케줄을 의도적으로 지연시키는 배포 훅을 추가했다. 이 단순한 조치만으로 배포 관련 장애가 사라졌다.
Docker Compose 헬스체크를 빠뜨리지 말 것: docker compose down && docker compose up -d 한 줄 재시작은 편리하지만, depends_on만으로는 기동 순서 보장이 안 된다. 컨테이너가 "Running" 상태여도 MySQL이 아직 커넥션을 받을 준비가 안 됐을 수 있다. 각 서비스에 healthcheck를 추가하고 condition: service_healthy로 의존성을 걸어야 "컨테이너는 올라왔는데 앱은 죽어 있는" 상황을 방지할 수 있다.
모니터링은 비용이 0에 가까운 것으로: Prometheus + Grafana를 붙이면 그 자체가 200~400MB를 상시 점유한다. 2GB RAM 환경에서 이는 사치다. df -h, free -h, nginx 접근 로그 파싱을 cron으로 돌려 Slack에 쏘는 15줄짜리 스크립트가 수백만 원짜리 모니터링 솔루션보다 빠른 대응을 가능하게 했다. 실제로 디스크 사용률 80% 경보를 이 방식으로 먼저 받아 로그 정리 조치를 취했다. 장애가 아닌 경보로 대응한 것이다.
어디서 막히는가: 트레이드오프와 한계
솔직하게 말하면, 이 사양은 성장하는 서비스를 위해 설계된 것이 아니다. 동시 접속 100명을 넘어가거나 일 정산 건수가 10만 건을 초과하기 시작하면 MySQL I/O 병목이 먼저 나타난다. 이 시점의 첫 번째 선택지는 DB를 별도 VM으로 분리하는 것이다. DB 분리만으로도 Java 앱의 응답 지연이 절반 이하로 줄어들 것으로 추정한다. 단, DB 분리는 VM 비용이 두 배가 된다는 현실적 트레이드오프를 수반한다.
두 번째 병목은 JVM이다. Spring 기반 앱은 메모리 효율이 좋지 않다. JVM 힙을 512MB로 제한하면 GC 빈도가 올라가고, 512MB 이상을 허용하면 다른 컨테이너가 압박받는다. GraalVM Native Image로 컴파일하면 힙을 절반 이하로 줄일 수 있지만, 빌드 시간이 10배 이상 길어지고 일부 리플렉션 의존 라이브러리가 동작하지 않는 트레이드오프가 있다. 지금 당장 전환하기보다 다음 사양 업그레이드 시점에 함께 검토할 항목으로 분류해두었다. 6개월의 결론은 "2vCPU / 2GB는 팀 내부 도구와 예측 가능한 소규모 B2B 서비스의 안정적인 범위"라는 것이다.
지금 바로 써먹을 수 있는 실행 시사점
비슷한 환경을 운영하고 있다면, 우선순위 순서로 체크할 것들이다.
오늘 당장: logrotate 설정 확인. /var/log 및 각 컨테이너 로그 경로에 7일 보관, 압축 설정이 없다면 지금 추가한다. 디스크 풀로 인한 서비스 중단은 예고 없이 온다.
이번 주 안에: Docker Compose healthcheck + condition: service_healthy 추가. 단순 depends_on으로 운영 중이라면 재시작 시마다 순서 문제를 언제든 만날 수 있다. 서비스당 10줄 미만의 설정 변경으로 재시작 안정성이 달라진다.
한 달 안에: 디스크 사용률(임계 80%)과 swap 사용률(임계 50%)을 감지하는 경보 스크립트를 Slack 또는 카카오워크와 연동한다. 서버를 직접 들여다보지 않아도 이상 징후를 먼저 받는 구조가 핵심이다.
분기 단위 검토: 정산 건수 또는 슬로우 쿼리 발생 빈도를 3개월마다 리뷰한다. DB 분리가 필요한 시점을 미리 인지하고 준비하는 것이, 장애 이후에 급하게 이전하는 것보다 비용이 훨씬 낮다. 단일 VM 전략의 핵심은 "작게 운영한다"가 아니라 "한계를 알고 운영한다"는 것이다. 한계선을 지속적으로 측정하는 것 자체가 운영의 일부다.
— by mings
* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.