
프로젝트 규모가 커졌을 때
모든 Rust 코드를 src/lib.rs 파일 하나에서 관리하게 될 경우
스파게티 코드가 될 확률이 높으며 관리하기 어려워진다.
따라서 기능에 따라 파일을 나누어 모듈 단위로 관리할 필요가 있다.
~/workspace$ mkdir modularization && cd modularization ~/workspace/modularization$ python3 -m venv venv ~/workspace/modularization$ source venv/bin/activate ~/workspace/modularization$ pip install maturin fastapi uvicorn ~/workspace/modularization$ maturin init ~/workspace/modularization$ # 선택지 중 기본값인 PyO3 선택 ~/workspace/modularization$ # Cargo.toml과 src/lib.rs가 자동 생성된다 ~/workspace/modularization$ # Python 코드는 직접 생성해 주어야 한다 ~/workspace/modularization$ mkdir app && touch app/main.py ~/workspace/modularization$ # Rust 모듈을 작성할 파일도 생성해 주어야 한다 ~/workspace/modularization$ touch src/math_ops.rs src/data_ops.rs ~/workspace/modularization$ tree -I venv . ├── app │ └── main.py ├── Cargo.toml ├── pyproject.toml └── src ├── data_ops.rs ├── lib.rs └── math_ops.rs
Cargo.toml 파일을 열어 라이브러리 이름을 수정해 주겠다.
우리가 작성할 모듈 중 하나는 병렬 처리 연산을 수행할 것이므로
병렬 처리를 위한 Rayon 크레이트도 추가해 준다.
Cargo.toml[package] name = "modularization" version = "0.1.0" edition = "2024" [lib] name = "rust_engine" crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.0" rayon = "1.11"
다른 파일에 작성된 함수는 #[pyfunction] 속성뿐만 아니라
pub 키워드도 있어야 src/lib.rs 에서 정상적으로 등록할 수 있다.
그 외에는 src/lib.rs 에서 작성하던 것과 크게 다르지 않다.
src/math_ops.rsuse pyo3::prelude::*; use rayon::prelude::*; #[pyfunction] pub fn parallel_sum(data: Vec<i32>) -> PyResult<i64> { Ok(data.par_iter().map(|&x| x as i64).sum()) }
src/data_ops.rsuse pyo3::prelude::*; use pyo3::exceptions::PyValueError; use std::collections::HashMap; #[pyfunction] pub fn get_stats(data: Vec<i32>) -> PyResult<HashMap<String, i64>> { if data.is_empty() { return Err(PyValueError::new_err("데이터 비어 있습니다.")); } let sum: i32 = data.iter().sum(); let avg = sum as i64 / data.len() as i64; let mut map = HashMap::new(); map.insert("sum".to_string(), sum as i64); map.insert("average".to_string(), avg); Ok(map) }
다른 파일에서 작성한 코드를 src/lib.rs 에서 모듈로 불러올 땐
불러오고자 하는 파일 경로 앞에 mod 키워드를 사용한다.
모듈 이름과 함수 이름 사이에 :: 을 붙여
해당 모듈 내에 정의된 함수를 사용할 수 있다.
src/lib.rsuse pyo3::prelude::*; mod math_ops; mod data_ops; #[pymodule] fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(math_ops::parallel_sum, m)?)?; m.add_function(wrap_pyfunction!(data_ops::get_stats, m)?)?; Ok(()) }
이번에도 정수 값을 담는 List 를 속성으로 가진 DataInput 클래스를 만들어 사용할 것이다.
이것은 데이터 검증 라이브러리 pydantic 의 BaseModel 클래스를 상속받아 생성한다.
app/main.pyfrom fastapi import FastAPI, HTTPException from pydantic import BaseModel import rust_engine app = FastAPI() class NumbersInput(BaseModel): values: list[int] @app.post("/compute") def run_all(data: NumbersInput): try: p_sum = rust_engine.parallel_sum(data.values) stats = rust_engine.get_stats(data.values) return { "parallel_sum": p_sum, "statistics": stats } except ValueError as e: raise HTTPException(status_code=400, detail=str(e))
Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
병렬 처리가 포함된 코드는 성능 최적화를 위해 --release 를 붙여 컴파일한다.
컴파일 후 pip list 명령어를 사용해 보면 Cargo.toml 파일에 작성한 패키지 이름을 확인할 수 있다.
uvicorn 라이브러리를 통해 FastAPI를 실행한다.
~/workspace/modularization$ maturin develop --release ~/workspace/modularization$ uvicorn app.main:app --reload
curl 명령어 또는 브라우저를 통해 테스트를 해볼 수 있다.
이 예제의 경우 GET 메서드가 아닌 POST 메서드로 통신하므로
브라우저를 통한 테스트 시 Swagger UI를 사용해야 한다.
FastAPI의 경우 다음과 같은 주소로 Swagger UI가 내장되어 있다.
http://127.0.0.1:8000/docs이 예제는 병렬 처리 연산이 핵심은 아니므로
따로 클라이언트 파일은 생성하지 않고
Swagger UI를 통해 테스트를 진행해도 충분하다.