Scale-Up vs Scale-Out: 왜 모든 언어가 어느 곳에서는 승리하는가

발행: (2026년 4월 18일 AM 09:18 GMT+9)
19 분 소요
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역하고자 하는 전체 텍스트를 제공해 주시겠어요?
텍스트를 주시면 요청하신 대로 한국어로 번역해 드리겠습니다.

Introduction

저는 “성능”이라는 이유로 Go에서 Rust로 핵심 서비스를 다시 작성한 팀과 함께 일했습니다. 6개월이 지난 뒤, 서비스는 30 % 빨라졌지만 팀은 힘들어했고 기능 개발 속도는 거의 정체되었습니다. 반면 여전히 Go를 사용하던 경쟁 팀은 네 가지 새로운 기능을 출시했습니다.

우리는 결국 사후 분석을 진행했습니다. 그 서비스는 4코어 머신에서 초당 약 2,000개의 요청을 처리했으며 CPU 사용률은 약 20 % 수준이었습니다. Rust의 추가 속도는 전혀 도움이 되지 않았습니다—병목 현상은 하위 데이터베이스 지연이었으니까요. 우리가 감당한 비용은 unsafe 코드를 작성하고, 빌림 검사기와 싸우며, 팀이 학습 곡선을 극복하도록 도와주는 동안 출시하지 못한 모든 기능이었습니다.

그 사건을 통해 제가 일찍 알았으면 좋았을 질문을 깨달았습니다: 실제로 무엇을 확장하고 있는가, 그리고 그 언어가 올바른 종류의 확장을 제공하고 있는가?

TL;DR

언어 벤치마크는 하나의 축, 즉 요청당 성능에만 최적화합니다. 실제 시스템은 여러 축을 가집니다 — 처리량, 지연 시간, 동시성, 개발자 생산성, 운영 복잡성, 메모리 효율성. Rust, Go, Java, Python은 “가장 빠른” 것을 놓고 경쟁하는 것이 아니라, 여러분이 확장하려는 대상에 대한 서로 다른 가정에 대한 다른 답변입니다. 순위표가 아니라 적합성에 따라 선택하세요.

Scaling Strategies

Scale‑up (Vertical Scaling)

  • 한 대의 머신이 더 많은 일을 하게 합니다.
  • 더 빠른 CPU, 더 많은 RAM, 특수 하드웨어, 작업당 비용 감소.

Scale‑out (Horizontal Scaling)

  • 머신을 더 추가합니다.
  • 저렴한 범용 하드웨어, 더 높은 동시성, 병렬로 실행되는 많은 작업.

이것은 단순히 인프라 결정만이 아닙니다. 선택한 언어와 생태계에 반영됩니다. Scale‑up에 최적화된 언어(Rust, C++)는 Scale‑out에 최적화된 언어(Go, Elixir) 혹은 개발자 생산성을 위해 설계된 언어(Python, Ruby)와 다른 우선순위를 가집니다.

Source:

Language Fit Map

축이 섞여서 생기는 큰 혼란입니다.

  • “Rust는 Go보다 빠르다”는 마이크로벤치마크 수준의 연산당 성능에서는 사실이지만, 워크로드가 I/O‑바운드 서비스‑간 트래픽이라면 무관합니다.
  • “Python은 느리다”는 계산‑바운드 루프에서는 사실이지만, 95 %의 시간을 PostgreSQL 대기에 쓰는 500 QPS API에서는 무관합니다.

Scale‑up‑oriented Languages

이 언어들은 머신당 처리량이 병목이고 엔지니어링 비용을 감당할 수 있을 때 지배적입니다. 문제가 많은 사람들이 주장하는 것보다 좁지만, 실제로 존재합니다:

  • 고주파 트레이딩 엔진 — 마이크로초 단위가 중요하고, GC 일시 정지는 용납될 수 없으며, 모든 캐시 라인이 중요합니다.
  • 추론 엔진 — llm.cpp, vllm, mistral.rs. 메모리 레이아웃, SIMD, 맞춤 커널.
  • 데이터베이스 및 스토리지 엔진 — ScyllaDB, TiKV, Foundation 내부. 영원히 살아야 하고 메모리 누수가 없어야 하는 상태 머신.
  • 네트워크 데이터 플레인 — Cloudflare의 Pingora, 엣지 프록시.
  • 게임 엔진, 오디오/비디오 인코딩, 임베디드 시스템.

패턴: 하나의 박스, 수년간 강하게 밀어붙임. 버그가 시간이 지남에 따라 복합적으로 쌓이기 때문에 메모리 안전성이 중요합니다. 코어당 처리량이 곱셈으로 나타나기 때문에 성능도 중요합니다.

비용: 모든 커밋이 느려집니다. 리팩터링이 비싸고, 온보딩은 주가 아니라 개월 단위로 측정됩니다. 컴파일 시간은 그 자체이며, 서비스가 존재하는 매일 이 비용을 지불합니다.

Scale‑out‑oriented Languages (Go)

Go는 특정한 Sweet spot을 잡습니다: 저비용 동시성, 예측 가능한 성능, 빠른 배포, 그리고 쉬운 채용. 이것이 바로 Scale‑out 언어입니다.

  • 코어당 수천 개의 goroutine, 2 KB 스택, 사용자 공간 컨텍스트 스위칭. “하나 더 추가하는 대기자”의 비용은 거의 제로에 가깝습니다.
  • 표준 라이브러리가 백엔드 작업의 ~80 %를 커버 — HTTP 서버, JSON, SQL, crypto.
  • 컴파일이 충분히 빨라 흐름을 유지할 수 있으며, 반복 루프가 동적 언어와 비슷하게 느껴집니다.
  • 미니멀리즘이 공격적입니다. 한 사람이 주말에 전체 언어를 읽을 수 있습니다. 신규 입사자는 며칠 안에 생산성을 발휘합니다.

단점: 연산당 성능. Go의 GC는 괜찮지만 눈에 보이지 않을 정도는 아닙니다. 제로‑카피 제네릭 코드는 Rust보다 작성하기 어렵습니다. 타입 시스템이 Rust가 방지하는 전체 버그 클래스를 막지는 못합니다.

Go의 베팅: “당신이 가장 흔히 겪을 문제는 ‘코드 2배로 늘리면서 동시 작업을 10배 처리해야 한다’는 것이지, ‘이 루프를 5 % 더 빠르게 해야 한다’는 것이 아니다.” 대부분의 백엔드 서비스에선 이 베팅이 맞습니다.

JVM‑oriented Languages (Java / Kotlin)

워크로드가 Scale‑out이면서 Go가 제공하지 못하는 런타임 유연성이 필요할 때 JVM이 정답입니다:

  • 핫 경로를 AOT보다 더 최적화할 수 있는 성숙한 JIT.
  • 풍부한 프로파일링 및 모니터링 (JFR, async‑profiler, flight recorder) 덕분에 배포 후 튜닝이 가능.
  • 25 년 동안 축적된 라이브러리 생태계는 사실상 모든 분야에 대한 성숙한 라이브러리를 제공합니다.
  • Kotlin을 얹으면 현대적인 문법과 코루틴을 생태계를 떠나지 않고 사용할 수 있습니다.

단점: 시작 시간, 메모리 오버헤드, 운영 복잡성 (GC 튜닝이 실제 작업), 가끔씩 발생하는 버전‑특정 버그. 많은 시장에서 채용이 Go보다 어렵습니다.

Java의 베팅: “10년 뒤에도 이 서비스를 운영하고 싶으며, 그때 런타임을 튜닝할 수 있기를 원한다.” 대규모 엔터프라이즈와 깊은 인프라를 가진 조직에선 이 베팅이 결실을 맺습니다. 첫 세 서비스를 출시하는 스타트업에게는 오버헤드가 너무 큽니다.

Team‑focused Languages (Python / Ruby)

이 언어들은 Scale‑up도 Scale‑out도 최적화하지 않지만, 팀을 최적화합니다.

  • 작성이 빠르고, 읽기 쉽고, 디버깅이 빠릅니다.
  • 데이터, ML, 스크립팅, DSL을 위한 방대한 라이브러리.
  • CS 전공자, 데이터 과학자, 분석가에게 온보딩이 쉽습니다.
  • 프로토타입‑→‑프로덕션 경로가 어느 곳보다 짧습니다.

단점: 코어당 처리량, 동시성 (GIL은 실제 문제), 메모리 사용량. Python과 Ruby는 100 K QPS 서비스를 위한 언어가 아닙니다.

하지만 많은 실제 기업들은 100 K QPS 서비스가 필요하지 않습니다. 그들은 무언가를 작동시키고, 사용자 앞에 내놓고, 반복하고 싶어합니다. 현재 문제점이 “우리는 …” 라는 상황이라면…

to ship the next feature this week,” Python might be the right answer even if a Rust version would technically run faster.

Python의 베팅: 처리량은 아직 제약이 아니다. Time‑to‑shipped‑feature가 제약이다. 대부분의 기업에게 대부분의 경우, 이는 맞다.

의사결정 프레임워크

스케일‑업/스케일‑아웃을 넘어, 원시 성능보다 더 많은 프로젝트를 좌우하는 몇 가지 축이 있습니다:

  • “기능을 구현해서 금요일까지 프로덕션에 올릴 수 있다”는 “이 서비스가 2배 빠르다”보다 대부분의 경우에 더 큰 가치를 가집니다. 이를 측정하세요. 현재 스택이 한 줄의 코드를 배포하는 데 이틀이 걸린다면, 처리량이 문제가 아니라 속도가 문제입니다.
  • 스케일‑업은 스케일‑아웃보다 운영 비용이 저렴합니다. 한 대의 머신, 하나의 프로세스, 하나의 로그. 스케일‑아웃은 더 나은 중복성을 제공하지만 일관성, 순서 보장, 부분 실패, 카오스 엔지니어링 등 분산 시스템 문제도 동반합니다. 팀이 세 명이라면, 20노드 클러스터의 운영 복잡성이 언어 선택으로 절약되는 시간보다 더 많이 소모될 수 있습니다.
  • 클라우드 규모에서는 메모리가 비쌉니다. Rust 서비스가 2 GB에 들어가고 Java 서비스는 8 GB가 필요하다면, 인스턴스당 4배의 절감 효과가 있습니다. 이를 수천 개 인스턴스로 곱하면 “작업당 성능”은 흥미로운 지표가 아니라 GB당 비용이 중요한 지표가 됩니다.
  • 귀하의 시장에서 가장 깊은 인재 풀을 가진 언어가 보통 새로운 시스템에 적합한 선택이며, 다른 조건이 동일할 경우 그렇습니다. 사소한 기술적 개선이 6개월짜리 채용 파이프라인을 정당화하지 못합니다.
  • 일부 언어는 온보딩이 쉬운 편(Go, Python)이며 깊이 있는 활용이 오래 걸립니다. 다른 언어는 온보딩이 가파른 편(Rust, Haskell)이며 ramp‑up 이후에만 생산성을 발휘합니다. 장기 운영되는 시스템에 senior 팀이 있다면 가파른 진입 장벽도 괜찮습니다. 하지만 빠르게 움직이는 팀이라면 가파른 진입 장벽은 비용이 됩니다.

Real‑World Evolution

  1. Start small – Python이나 Ruby를 선택하고, 작업을 구축하여 프로덕션에 배포합니다. 직원 10명, 하나의 코드베이스, 속도가 빠릅니다.
  2. Grow – 모놀리식이 갈라집니다. 일부 서비스는 동시성 및 운영 단순성을 위해 Go로 다시 작성됩니다. 성능이 중요한 몇몇 서비스는 Rust로 작성됩니다. 데이터 인프라는 JVM 위에 구축됩니다 (Kafka, Spark, Flink). 몇몇 내부 도구는 팀이 익숙하고 잘 동작하기 때문에 Python으로 유지됩니다.
  3. Mature – 5년이 지나면 스택은 다중 언어가 됩니다. 누구도 이를 후회하지 않습니다. 후회하는 것은 단일 언어 스택을 편안한 영역을 넘어 사용하려고 6개월을 보낸 시간입니다 — Python 팀이 “그냥 async를 더 많이 쓰자”고 주장하고, Rust 팀이 Go로 구현될 수 있었던 코드를 빌린 체커와 싸우고, Java 팀이 스택 트레이스가 400줄이나 되는 이유를 설명하는 상황 말이죠.

Pattern: 서비스에 맞는 언어를 선택하고, 언어에 맞는 서비스를 만들지 않습니다.

누군가 “이 새로운 것을 X로 만들자”고 제안하면, 저는 다음을 묻습니다:

  1. 예상 트래픽 프로파일은 무엇이며, 요청당 작업 형태는 어떠한가?
  2. 스케일‑업이 제한되는가 (머신당 처리량) 아니면 스케일‑아웃이 제한되는가 (동시 작업)?
  3. 누가 이를 작성할 것이며, 얼마나 빨리 생산성을 확보해야 하는가?
  4. 누가 이를 운영할 것이며, 그들의 도구 사용 편안함은 어느 정도인가?
  5. 기존 생태계와 연동되는가 (JVM 데이터 플랫폼, Rust 보안 인프라 등)?
  6. 얼마나 오래 살아야 하는가?

이 질문들에 대한 답변은 제가 보는 시스템의 약 80 %에서 다음 세 언어 중 하나로 귀결됩니다: Go, Rust, 혹은 (데이터와 연관된 작업을 위해) JVM 위의 Kotlin. Python은 여전히 도구와 glue 역할로 등장합니다. 그 외의 선택은 상황에 따라 달라집니다.

결론

벤치마크는 도움이 되지 않는다. 연산당 마이크로벤치마크는 실제로 아무도 묻지 않는 질문에 답한다. 올바른 질문은 이 시스템에 어떤 축이 중요한지, 그리고 어떤 언어의 선택이 그 축과 일치하는가이다.

나는 아직도 엔지니어들이 Rust와 Go 중 어느 것이 “더 나은가”에 대해 논쟁하는 모습을 본다. 두 언어 모두 좋은 언어다. 두 언어 모두 설계되지 않은 문제에 대해서는 나쁜 선택이다. 의미 있는 질문은 여러분이 어떤 종류의 규모에 비용을 지불하고 있는가이며, 솔직한 답은 거의 항상 혼합된 형태이며 시간이 지남에 따라 진화한다.

내가 처음에 언급한 Rust 재작성은 Rust가 나쁜 언어라서 나쁜 결정이었던 것이 아니다. 우리가 규모 확장에 제한이 있었기 때문이 아니라, 하위 데이터베이스에 제한이 있었기 때문에 나쁜 결정이었다. 어떤 언어도 그것을 해결할 수 없었다.

당신이 어떤 규모를 구매하고 있는지 알고, 의도적으로 그 규모를 구매하라.

추가 읽을거리

  • Why Go Handles Millions of Connections: User‑Space Context Switching, Explained — the design decision behind Go’s scale‑out bet.
  • Go’s Concurrency Is About Structure, Not Speed — what you actually get with Go, and what you don’t.
  • NATS vs Kafka vs MQTT: Same Category, Very Different Jobs — applying the same fit‑vs‑benchmark thinking to messaging.
0 조회
Back to Blog

관련 글

더 보기 »

지구의 날을 위한 활력

제가 만든 History는 브라우저에 달력 날짜별로 저장됩니다; 각 섹션 옆의 사진은 실제 번들된 이미지입니다. 선택적인 Gemini API route는 따뜻한 코치를 추가할 수 있습니다.