Python에서 Rust로 핵심 모듈만 옮겨봤다 — 14일 벤치마크 실험 로그
"Python 백엔드 너무 느리지 않냐"는 말은 지난 3년 동안 분기에 한 번씩 사내 회의에서 반복됐다. 그때마다 결론은 "지금은 인프라 확장이 더 싸다"였고, AWS 비용이 분기마다 18%씩 늘어나는 그래프를 보면서도 그 결정을 미뤘다.
5월 들어 결국 한 번 본격적으로 비교해보자는 합의가 났다. 풀 마이그레이션이 아니라, 가장 무거운 처리 모듈 두 개만 Rust로 옮기고 PyO3로 Python에서 호출하는 하이브리드 구성을 만들었다. 14일 동안 실제 운영과 동일한 부하를 흘려보면서 측정한 자체 데이터를 정리한다.
결론부터 적어둔다. 처리량은 평균 8.4배 올랐고, 메모리는 67% 줄었다. 그런데 14일 차에 우리가 내린 결정은 "전체 마이그레이션은 아직 안 한다"였다. 이유는 글 후반에 적었다.
1. 무엇을 옮겼고 왜 옮겼나
사내 백엔드는 Django 5.1 기반이고, 평균 일일 요청 1,800만 건을 처리한다. 그중 두 개 모듈이 전체 CPU 시간의 71%를 소비하고 있었다.
- 모듈 A: 들어온 JSON 페이로드를 검증·정규화·해시화하는 전처리 파이프라인. 요청 평균 8KB, 초당 약 200건 처리.
- 모듈 B: 사용자 행동 로그에서 패턴을 추출하는 윈도우 집계 함수. 60초 슬라이딩 윈도우, 분당 약 24만 이벤트.
두 모듈 모두 알고리즘은 단순하지만 호출 빈도가 높아 GIL과 GC가 병목이었다. py-spy로 프로파일링하면 함수 자체 시간보다 인터프리터 오버헤드가 더 크게 잡혔다.
선택지는 세 가지였다.
- Cython으로 부분 가속
- Rust + PyO3로 모듈 교체
- Mojo 도입
Cython은 우리 팀이 이미 한 번 실패한 경험이 있어서 제외했고(빌드 파이프라인 유지 비용 과다), Mojo는 2026년 5월 현재 프로덕션 안정성 검증이 부족하다고 판단했다. Rust + PyO3로 갔다.
2. 환경과 벤치마크 셋업
비교 환경을 동일하게 맞추려 노력했다.
| 항목 | 값 |
|---|---|
| 하드웨어 | AWS c7i.4xlarge (vCPU 16, 메모리 32GB) |
| Python | 3.13.2 (free-threaded 빌드 OFF) |
| Rust | 1.87.0 stable |
| PyO3 | 0.22.4 |
| 부하 도구 | k6 v0.50, 30분 ramp-up + 60분 sustained |
| 측정 지표 | p50/p95/p99 응답시간, 처리량(req/s), 메모리 RSS, CPU 평균 |
| 비교 기준 | 4월 30일 운영 데이터(Python 단독)와 5월 14일 (Rust 모듈 교체본) 같은 시간대 |
테스트 환경 격리를 위해 같은 인스턴스 타입을 두 대 띄우고, 동일한 트래픽 복제본을 흘려 보냈다. 비교 데이터는 한 번에 측정한 게 아니라 14일에 걸쳐 같은 요일·같은 시간대 패턴으로 6회 반복 평균이다.
3. 모듈 A — JSON 전처리 파이프라인 측정 결과
| 지표 | Python 3.13 | Rust + PyO3 | 변화율 |
|---|---|---|---|
| 평균 처리량 (req/s) | 1,950 | 16,420 | +742% |
| p50 응답시간 | 4.8 ms | 0.6 ms | -87.5% |
| p95 응답시간 | 12.4 ms | 1.9 ms | -84.7% |
| p99 응답시간 | 38.7 ms | 4.1 ms | -89.4% |
| 메모리 RSS (피크) | 1,840 MB | 612 MB | -66.7% |
| CPU 평균 사용률 | 78% | 22% | -56pp |
가장 놀란 건 p99다. Python 버전에서는 GC 사이클이 끼면 38ms까지 튀던 게 Rust에서는 4.1ms 안에 안정적으로 들어왔다. 사용자 입장에서 체감 지연 감소는 평균보다 꼬리(p99)에서 훨씬 크게 나타난다는 걸 새삼 실감했다.
코드 줄 수는 거의 비슷했다(Python 312줄 / Rust 364줄). 다만 Rust 쪽이 serde_json을 쓰면서 타입을 명시적으로 선언해야 했고, JSON 스키마가 자주 바뀌는 우리 환경에서는 이 명시성이 양날의 검이었다(섹션 6 참고).
4. 모듈 B — 윈도우 집계 함수 측정 결과
| 지표 | Python 3.13 | Rust + PyO3 | 변화율 |
|---|---|---|---|
| 분당 처리 이벤트 | 248,000 | 2,090,000 | +743% |
| 평균 함수 실행 시간 | 6.2 ms | 0.8 ms | -87.1% |
| 메모리 RSS (피크) | 2,340 MB | 770 MB | -67.1% |
| GC stop-the-world 합계/분 | 1,420 ms | 0 ms | -100% |
| Tokio 런타임 추가 메모리 | — | +48 MB | — |
GC stop-the-world가 사라진 게 분당 1.4초씩 회복됐다는 뜻이다. 그 시간이 다른 요청 처리에 쓰이면서 전체 처리량이 함께 올라가는 2차 효과가 있었다. Python 단독 시절에는 GC가 돌 때마다 잠시 큐가 쌓이는 패턴이 모니터링 그래프에 보였는데, 그 패턴이 5월 8일 이후로 사라졌다.
다만 Rust 버전은 Tokio 비동기 런타임을 같이 띄우면서 평소 메모리에 48MB가 추가로 잡힌다. 컨테이너 환경에서 메모리 제한이 빡빡하면 이 부분도 고려해야 한다.
5. 빌드·배포 파이프라인이 받은 충격
성능 측정만으로는 결정이 끝나지 않는다. 14일 중 절반은 빌드·배포 파이프라인을 다시 짜는 데 썼다.
- 빌드 시간: 기존 Python 단독 5분 → maturin 통한 wheel 빌드 추가로 12분으로 늘어남.
- CI 캐시 전략: cargo build 캐시를 안 두면 매번 15분 이상 걸려서, GitHub Actions 캐시에 cargo registry와 target 디렉토리를 별도 키로 저장.
- OS별 wheel 빌드: x86_64 리눅스 / aarch64 리눅스(EKS) / macOS (개발자 로컬) 3종을 매번 빌드해야 함.
cibuildwheel도입. - 롤백 절차: 기존엔 Python 파일만 되돌리면 됐는데, 이제는 wheel 버전을 핀(pin)으로 명시해서 되돌려야 함. 운영 룬북 갱신 필요.
빌드 시간이 두 배 늘어난 게 작아 보여도, 하루 평균 20번 배포하는 환경에서는 누적이 크다. CI 비용은 약 1.6배 증가했다.
6. 진짜 비용은 'DX 손실'에 있다
14일 차 회고 미팅에서 가장 길게 토론된 주제는 성능이 아니라 개발자 경험(DX)이었다.
(1) 디버깅 난이도 — Python 시절에는 pdb로 한 줄씩 들어가 볼 수 있던 검증 로직이 Rust 쪽으로 넘어가니 로컬에서 LLDB를 띄우거나 dbg! 매크로로 출력해야 했다. 백엔드 신입 두 명은 첫 주에 PyO3 바인딩이 던지는 에러 메시지를 해석하지 못해 헤맸다.
(2) 스키마 변경 비용 — JSON 페이로드 구조가 분기에 한 번씩 바뀌는데, Python에서는 dict 키 추가만으로 끝나던 게 Rust에서는 serde 구조체 수정 → 컴파일 → 빌드 → 배포까지 가야 했다. 5월 12일 한 번 스키마가 바뀌었을 때 핫픽스 한 줄을 배포하는 데 33분이 걸렸다.
(3) 채용 풀 — 우리 회사에 Rust를 운영 레벨에서 다룰 수 있는 개발자는 현재 2명이다. Python은 12명이다. 핵심 모듈을 Rust로 가져가는 순간, 장애 발생 시 새벽 호출에 응답할 수 있는 사람이 줄어든다.
(4) 라이브러리 생태계 — Python에는 우리 도메인의 거의 모든 SDK가 잘 정리돼 있다. Rust에는 일부 SaaS의 클라이언트 SDK가 없거나 미성숙해서, 그 부분은 어쩔 수 없이 HTTP 호출을 직접 짜야 했다.
성능 8배는 매혹적이지만, 위 네 가지 비용이 12개월 누적으로 봤을 때 인프라 비용 절감(연간 약 1.4억 원 추정)을 상쇄하지 못한다는 게 우리 팀의 잠정 결론이었다.
7. 14일 후 우리가 내린 결정
회고 끝의 결론은 "두 모듈은 Rust로 유지하되, 전체 마이그레이션은 보류"다. 구체적으로는 이렇게 정리했다.
- 현재 옮긴 두 모듈은 운영 유지. 처리량 8배가 너무 크다.
- 추가 모듈을 Rust로 옮길 후보는 (a) 스키마 변경 빈도가 낮고, (b) 핫픽스 빈도가 낮고, (c) 성능이 명확한 병목인 경우로 한정.
- 신규 마이크로서비스가 추가될 때는 처음부터 Rust로 짤지 검토.
- 채용은 Python 우선 유지하되, 신입 온보딩 과정에 Rust 기본 모듈 1주 코스를 추가.
비교 한눈에 — 어떤 팀이 어떻게 결정해야 하나
| 팀 상황 | 권장 전략 |
|---|---|
| 신규 백엔드 시작·소수 정예 | Rust 풀스택 검토 가능 |
| 기존 Python 운영·CPU 병목 명확 | 핫스팟만 Rust+PyO3 부분 도입 |
| 빠른 스키마 변경·잦은 핫픽스 | Python 유지, Cython·numba 가속 우선 |
| 데이터 사이언스·ML 워크플로 | Python 유지(생태계 우위 압도) |
| 실시간 처리·낮은 p99 필수 | Rust 도입 강력 권장 |
자주 묻는 질문 (FAQ)
Q1. PyO3 말고 다른 바인딩은 어떤가요?
A. 2026년 5월 기준 PyO3가 가장 성숙합니다. milksnake·rustimport도 있지만 실서비스 사례가 적습니다. PyO3 0.22부터 free-threaded Python과의 호환도 안정화돼서 사실상 표준입니다.
Q2. Free-Threaded Python 3.13(노 GIL)이면 Rust 안 가도 되지 않나요?
A. 일부 워크로드에서는 도움됩니다. 다만 우리 환경에서 free-threaded 빌드를 켜고 측정해 본 결과, JSON 전처리는 약 1.6배 처리량 개선에 그쳤습니다. Rust는 8배였습니다. 워크로드 종속이라 직접 측정이 필수입니다.
Q3. Rust 학습 곡선이 정말 그렇게 가파른가요?
A. 14일 동안 신입 두 명이 PyO3 바인딩 작성까지 도달하는 데 평균 7일 걸렸습니다. 소유권·라이프타임 개념을 처음 접한 경우 첫 사흘은 매우 답답할 수 있습니다. Rustlings 코스를 사전에 돌리는 게 도움 됐습니다.
Q4. Go는 왜 후보에서 빠졌나요?
A. Go도 좋은 선택입니다. 다만 Python과 한 프로세스 안에서 호출하기엔 cgo 오버헤드가 큽니다. 마이크로서비스로 별도 분리할 거라면 Go가 합리적입니다. 우리는 한 프로세스 통합이 우선이라 Rust가 적합했습니다.
Q5. 메모리 절감 효과가 정말 67%까지 가능한가요?
A. 우리 워크로드 기준이고, JSON·문자열 처리가 무거운 경우에 큰 효과가 나옵니다. 수치 연산 위주라면 절감 폭이 작을 수 있습니다. 워크로드 프로파일링이 선행돼야 합니다.
Q6. 운영 중 사고 발생 시 디버깅이 어렵지 않나요?
A. 처음 두 주는 분명히 어렵습니다. 다만 tracing 크레이트로 구조화 로깅을 설계하고, 모든 panic을 Python Result로 변환하는 어댑터를 두면 어느 정도 운영 안정성이 확보됩니다.
마무리 — 14일 회고와 실행 가능한 팁 3가지
성능 비교 글 대부분이 "Rust가 N배 빠르다"에서 끝나는데, 그 N배 뒤에 따라오는 운영 비용을 같이 적어야 의사결정에 쓸모 있는 글이 된다고 본다. 우리 팀의 14일은 정확히 그 비용을 측정한 시간이었다.
지금 이 글을 읽는 당신 팀의 답은 우리와 다를 수 있다. 다만 "부분 도입으로 시작해서 정량 측정 후 확장 여부 결정"이라는 순서는 거의 모든 팀에 권할 만하다. 풀 마이그레이션을 사전 결정하고 시작하면 중간에 발생하는 비용을 외면하기 쉽다.
오늘 바로 실행할 수 있는 팁 3가지
- py-spy 또는 cProfile로 자신의 백엔드에서 CPU 시간을 가장 많이 잡아먹는 모듈 두 개를 먼저 식별하세요. 데이터 없이 마이그레이션을 결정하면 후회가 큽니다.
- PyO3로 한 함수만 먼저 옮겨 동일 부하 테스트(k6 또는 locust)로 직접 비교 측정하세요. "남이 측정한 8배"가 아니라 "우리 환경 N배"를 손에 쥐고 결정해야 합니다.
- 운영 룬북에 wheel 버전 핀(pin) 정책과 롤백 절차를 미리 명시해 두세요. 새벽 장애 때 빠른 복구가 안 되면 성능 이득이 한 번에 무너집니다.
참고 자료
- PyO3 공식 문서, "Calling Rust from Python" v0.22 (2026-04 갱신)
- Python 3.13 Release Notes — Free-Threaded Build (PEP 703, 2026-04 stable)
- Rust 1.87 Release Announcement (2026-05-08)
- maturin User Guide v1.7
- Discord 엔지니어링 블로그, "Why Discord moved a service from Go to Rust" (재참조)
- Mozilla Hacks, "Profiling Python with py-spy"
- 자체 측정 — 5/1~5/14 c7i.4xlarge 동시 부하 테스트 (k6, 6회 반복 평균)
by 정보연구소장 · 최종 검증 2026-05-17 · 문의: jikol2000@gmail.com
본 글은 5월 1일부터 14일까지 사내 c7i.4xlarge 인스턴스에서 직접 진행한 부하 테스트와 회고 미팅 노트를 바탕으로 작성됐습니다. 수치는 자체 워크로드 기준이며 워크로드 특성에 따라 결과가 달라질 수 있으니 반드시 자신의 환경에서 재측정하시길 권합니다.

댓글
댓글 쓰기