Rust로 넘어가는 python 개발자

Python은 백엔드 API를 빠르게 만들고, 데이터를 수집하고, 머신러닝 모델을 서빙하기에 여전히 가장 생산성이 높은 언어 중 하나라고 생각한다. FastAPI나 Flask로 API를 띄우고, pandas나 requests 같은 라이브러리로 필요한 기능을 빠르게 구현할 수 있다는 점은 Python의 엄청난 장점이다.

하지만 프로젝트의 규모가 커지고 트래픽이 늘어나면서, Python만으로 처리하기 애매한 구간도 보이기 시작했다. 특히 CPU를 많이 쓰는 데이터 처리, 지연 시간이 중요한 API, 메모리 사용량을 줄이고 싶은 백엔드 컴포넌트에서는 더 낮은 레벨의 언어가 필요하다는 생각이 들었다.

그래서 내가 관심을 갖게 된 언어가 Rust다. Rust는 C/C++에 가까운 성능을 목표로 하면서도, 소유권과 타입 시스템을 통해 많은 메모리 관련 실수를 컴파일 단계에서 잡아준다. Python을 대체한다기보다는, Python이 잘하는 영역은 그대로 두고 병목이 되는 부분을 Rust로 보완해보고 싶었다.

내가 Rust로 넘어가려는 3가지 이유

1. CPU-bound 작업에서의 성능

Python은 I/O 중심의 API 서버나 데이터 처리 스크립트를 빠르게 만들기에는 훌륭하다. 하지만 CPython 기본 환경에서는 CPU를 많이 쓰는 순수 Python 코드를 여러 스레드로 병렬 실행하기 어렵다. GIL 때문에 여러 스레드를 만들어도 한 프로세스 안에서 Python 바이트코드가 동시에 여러 코어에서 실행되기 어렵기 때문이다.

물론 Python에도 해결책은 있다. 멀티프로세싱을 쓰거나, worker를 여러 개 띄우거나, NumPy처럼 내부에서 C로 최적화된 라이브러리를 활용할 수 있다. 하지만 애플리케이션의 특정 구간이 계속 병목이 된다면, 그 부분을 Rust로 작성하는 것은 충분히 매력적인 선택지가 된다.

2. 낮은 메모리 사용량과 배포 효율

Python 애플리케이션은 인터프리터와 런타임, 프레임워크, 여러 의존성을 함께 사용한다. 그래서 작은 API 서버라도 생각보다 메모리를 많이 사용할 때가 있다.

반면 Rust는 컴파일된 네이티브 바이너리로 실행된다. GC가 없고 런타임 의존성이 작기 때문에, 같은 기능을 더 작은 메모리 풋프린트로 구현할 수 있는 경우가 많다. 모든 상황에서 서버비가 극적으로 줄어든다고 말할 수는 없지만, 많은 인스턴스를 띄워야 하거나 메모리 제한이 빡빡한 환경에서는 분명한 장점이 될 수 있다.

3. 컴파일러가 주는 강한 안정감

Python은 빠르게 작성할 수 있는 대신, 타입 오류나 일부 버그를 실행 중에야 발견하는 경우가 많다. 타입 힌트와 mypy, pyright 같은 도구를 쓰면 어느 정도 보완할 수 있지만, 언어 자체가 강제하는 수준은 Rust와 다르다.

Rust는 소유권, 빌림, 라이프타임, 타입 시스템을 통해 많은 실수를 컴파일 단계에서 잡아준다. 물론 Rust라고 해서 런타임 에러가 전혀 없는 것은 아니다. 파일이 없을 수도 있고, 네트워크 요청이 실패할 수도 있고, 로직 버그가 생길 수도 있다. 하지만 적어도 메모리 안전성, 데이터 경합, 잘못된 소유권 사용 같은 문제를 배포 전에 발견할 가능성이 크게 올라간다.

Rust로 넘어가면서 마주친 장애물 3가지

Python에서 편하게 사용하던 방식이 Rust에서는 그대로 통하지 않았다. Rust 컴파일러는 처음에는 사사건건 시비를 거는 깐깐한 직장 상사처럼 느껴졌다. 하지만 시간이 지나면서 이 깐깐함이 단순한 괴롭힘이 아니라, 런타임에 터질 수 있는 문제를 미리 막아주는 장치라는 생각이 들기 시작했다.

1. 변수는 기본적으로 불변이다

Python에서는 변수를 만들고 필요할 때마다 값을 자유롭게 바꿀 수 있다.

price = 1000
price = 500

Rust에서는 기본적으로 변수가 불변이다.

let price = 1000;
price = 500; // 컴파일 에러

값을 바꾸고 싶다면 mut를 명시해야 한다.

let mut price = 1000;
price = 500;

처음에는 mut를 빼먹어서 자주 컴파일 에러를 만난다. 하지만 이 규칙 덕분에 “이 값은 바뀌면 안 되는 값인가?”를 선언 단계에서 한 번 더 생각하게 된다. 의도치 않은 데이터 변경을 줄이는 데 도움이 된다.

2. 데이터에는 소유권이 있다

Python에서는 객체를 함수에 넘겨도 대부분 큰 고민 없이 다시 사용할 수 있다. 메모리 관리는 가비지 컬렉터가 알아서 처리해준다.

Rust에서는 값마다 소유자가 있다. 특히 String처럼 힙에 데이터를 가진 타입은 함수에 넘기는 순간 소유권이 이동할 수 있다.

fn print_data(data: String) {
    println!("{}", data);
}

fn main() {
    let my_text = String::from("Hello");

    print_data(my_text);

    println!("{}", my_text); // 컴파일 에러
}

my_text의 소유권이 print_data 함수로 이동했기 때문에, 함수 호출 이후에는 더 이상 사용할 수 없다.

이럴 때는 소유권을 넘기지 않고 빌림을 사용한다.

fn print_data(data: &str) {
    println!("{}", data);
}

fn main() {
    let my_text = String::from("Hello");

    print_data(&my_text);

    println!("{}", my_text);
}

&를 붙이면 값을 소유하는 것이 아니라 잠깐 빌려 쓰게 된다. 이 개념이 Rust 초반 학습에서 가장 어렵지만, 동시에 Rust가 메모리 안전성을 지키는 핵심이기도 하다.

3. 에러는 타입으로 드러난다

Python에서는 예외가 날 것 같은 코드를 try-except로 감싸고 처리한다.

try:
    with open("config.json", "r") as f:
        data = f.read()
except Exception as e:
    print("설정 파일을 읽지 못했습니다. 기본값을 사용합니다.")
    data = "{}"

Rust에는 Python식 예외 처리보다 ResultOption을 중심으로 한 에러 처리 방식이 더 자주 쓰인다. 실패할 수 있는 함수는 성공과 실패를 타입으로 표현한다.

use std::fs;

fn main() {
    let data = match fs::read_to_string("config.json") {
        Ok(content) => content,
        Err(error) => {
            println!("설정 파일을 읽지 못했습니다. 기본값을 사용합니다. 원인: {}", error);
            "{}".to_string()
        }
    };

    println!("{}", data);
}

처음에는 파일 하나 읽는 코드도 길어져서 답답하다. 하지만 실패 가능성이 코드에 명확히 드러난다는 장점이 있다. 에러를 무시하는 것이 아니라, 기본값을 쓸지, 호출자에게 전파할지, 프로그램을 중단할지 선택해야 한다.

마무리

Rust는 Python보다 어렵다. 특히 불변성, 소유권, 빌림, Result 기반 에러 처리는 Python 개발자에게 낯설다. 하지만 그 어려움은 단순한 문법 장벽이라기보다, 더 안전하고 예측 가능한 프로그램을 만들기 위한 장치에 가깝다.

나는 Python을 버리기 위해 Rust를 배우는 것이 아니다. Python이 잘하는 빠른 개발과 생태계의 장점은 그대로 가져가고, 성능과 메모리 효율, 안정성이 중요한 구간에서 Rust를 함께 쓰기 위해 배우고 있다.

앞으로 더 공부하면서 Python 개발자가 Rust를 배울 때 어디서 막히는지, 그리고 그 장벽을 어떻게 넘을 수 있는지 계속 정리해보려 한다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다