네트워크 대기가 대부분인 CLI 자동화 도구를 Python으로 만들었다가 Rust로 재작성하면서, 성능보다 안정성과 견고함, 테스트 가능성, 배포 편의성에서 얻은 이점을 정리한다.
HomeAboutServicesProjectsBlogContact
Smiling Dev
공유:
Python의 새로운 툴링을 정말 즐기고 있다는 말부터 하고 싶다. Astral 팀이 uv, Ruff, Typo로 해온 일은 툴링과 개발자 경험을 크게 끌어올렸다. 많은 경우 Python이 정답이다. 빠르고 “충분히 빠르다.” 대부분의 개발자는 Python에 능숙하다(아주 잘 알지 못하더라도). AI 도구도 빠르게 처리해 준다. 그렇다면 왜 Rust를 고려할까?
맥락은 이렇다. 나는 커맨드라인을 중심으로 돌아가는 한 회사의 새 도구를 만들고 있었다. 의도적으로 용도를 두루뭉술하게 말하겠지만, 이 도구는 데이터를 받아 변환하고 표준화한 다음, 특수한 형식으로 그 데이터를 업로드한다. 또한 데이터를 업로드하려면 보통 새 배치를 올리기 전에 API 호출로 애플리케이션 내부의 환경과 데이터를 미리 세팅해야 했다. 결국, 핵심은 가벼운 변환 몇 가지와, 기묘한 API 설계를 상대하기 위한 다수의 HTTP 호출이었다.
이 맥락에서는, 대부분이 네트워크를 기다리는 일이므로 Python의 성능이 Rust에 비해 유의미하게 다르지 않다.
이번 경우 내 이유는 신뢰성, 그리고 견고함의 감각을 찾는 것이었다.
툴링, 타입 힌트, 린터가 큰 도움이 되긴 하지만, 결국 애플리케이션 전반에 걸쳐 일정 수준의 복잡성이 떠오르기 시작한다. Pydantic과 assert 문으로 검증을 추가하는 건 모두 좋지만, 동시에 너무 많은 불변조건을 놓고 추론하기가 어렵다.
또한 Python의 예외 처리는 괜찮지만, Rust와 비교하면 다시 돌아가기 어렵다. Rust의 Result와 Option enum 기반 에러 처리는 경우에 따라 장황하고 귀찮을 수 있지만, 모든 에러를 아주 신경 쓰고 대부분에서 복구하려고 할 때는 정말 훌륭하다! 여기저기에 try-except 블록을 잔뜩 두고 싶지 않다. Python 함수는 어떻게 실패할 수 있고, 어떤 방식으로 실패할까? 코드를 보기만 해서는 대부분의 함수가 어떤 종류의 예외를 던질지에 대한 구체적인 정보를 얻을 수 없다. 그래서 많은 테스트가 필요하다.
테스트도 또 하나의 영역이었다. 나는 Python이 여기서 빛날 거라고 생각했다. 모킹은 쉽다—데이터베이스, API, 설정, 그 밖의 모든 것을 모킹할 수 있다. 처음에는 Rust에서 테스트가 조금 더 까다로웠다. API나 클라이언트 입력을 다루는 struct와 함수의 경우, 그걸 어떻게 모킹해야 하는지 명확하지 않았다. mockito도 있지만, Reddit 글을 좀 읽다 보니 trait가 종종 해답이라는 것을 알게 되었다. fn get_posts(&client: &MyClient, data: PostUpload) -> Result<> 같은 함수를 만들기보다는 trait를 쓰는 편이 낫다:
trait HttpGet {
fn get(data: serde_json::Value) -> Result<>;
}
struct MyClient {}
impl HttpGet for MyClient {
fn get(data: serde_json::Value) -> Result<> {
reqwest.get()?
}
}
fn get_posts<T: HttpGet>(client: impl HttpGet, data: serde_json::Value) {
client.get(data)?
}
그럼 뭐가 다를까? 이제 테스트 모듈에서 MockClient를 만들고, 모킹 클라이언트가 내가 원하는 응답을 반환하게 할 수 있다. 매우 명확하고, 테스트 가능하며, 런타임에서 import를 모킹하거나 덮어쓰지 않는다. Python의 몽키 패칭도 끝이다. import 문자열을 충분히 많이 잘못 적거나, 나를 헷갈리게 했던 pytest autouse fixture 같은 것들로 더 이상 고생하지 않아도 된다.
테스트는 예상 밖이었다. 하지만 타입 시스템의 엄격함에 더해, 파일 하단의 인-파일 테스트 블록이 있는 덕분에, 테스트가 매우 유용했고 추론하기도 쉬웠다. 그 결과 수백 개의 테스트에 덜 집착하게 되었고(내 Python CLI 도구에는 800개 이상이 있었던 것 같다), 대신 각 서브모듈의 동작을 “고정(lock down)”하는 데 도움이 되는 몇 개의 탄탄한 테스트에 더 집중하게 되었다. 통합 테스트는 여전히 중요하지만, Python보다 범위가 덜 넓은 편이었다. Python이 불명확하게 남겨둔 모든 엣지 케이스를 억지로 다 스트레스 테스트하려는 것이 아니기 때문이다.
Python 코드 구축에는 약 10주가 걸렸다. 그 다음 추가 요청과 기능이 더 들어와서 2주가 더 들었다. 변경이 있을 때마다 더 많은 테스트가 필요했고, 정말로 동작할지 확신이 서지 않았다. Python 코드 총 라인 수:
테스트 파일 포함: 23,598
테스트 파일 제외: 11,166
Python 코드를 Rust로 재작성하는 데는 1주가 걸렸다. 물론 먼저 Python으로 한 번 해본 것이 속도를 크게 올려줬다. 우리가 쓰는 내부 API 시스템의 특이점을 이미 배웠고, 직렬화 요구사항에도 한 번 당해본 적이 있었다. 단순히 옮겨올 수 있는 테스트 파일과 체크도 많이 갖고 있었다.
Rust 코드 라인 수: 20,055
테스트 블록 제외 라인 수: 14,946
Rust 바이너리를 배포하는 일은 Python 빌드보다 훨씬 쉽다. 사용자들에게는 저장소를 클론하고 uv를 설치한 다음 Python 환경을 설정하라는 안내가 필요했다. pull 할 수 있는 Docker 이미지도 만들었지만, 모든 엔지니어가 프라이빗 Docker 이미지를 pull 하기 위한 artifactory 토큰을 설정해 둔 것은 아니었다.
반면 Rust에서는 크로스 컴파일을 동작하게 만들 수 있었다. 이제 ARM 64 macOS, Windows x86, Linux로 컴파일된다. 정말 훌륭하다. 나는 사용자들에게 저장소의 artifacts에서 바이너리를 직접 다운로드하라고 권장하지만, Docker를 선호하는 사람들을 위해 작은 MUSL Linux Docker 이미지도 빌드해 두었다.
또 다른 좋은 점은 Rust 바이너리가 매우 빠르게 시작된다는 것이다. 성능이 결정적 요인이 아니었다고 말했지만, CLI 도구가 즉시 시작하는 건 기분이 좋다. 반응이 또렷하고 단단하게 느껴진다!
이 글 공유:
공유:
info@smiling.devGitHubFacebookLinkedIn
최신 기술 인사이트와 프로젝트 업데이트를 받아보세요.
구독
구독해 주셔서 감사합니다! 이메일을 확인해 확인을 완료해 주세요.
© 2026 Smiling Dev Consulting. All rights reserved.