모듈화

프로젝트 규모가 커졌을 때
모든 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"

코드 작성

Rust 코드

다른 파일에 작성된 함수는 #[pyfunction] 속성뿐만 아니라
pub 키워드도 있어야 src/lib.rs 에서 정상적으로 등록할 수 있다.
그 외에는 src/lib.rs 에서 작성하던 것과 크게 다르지 않다.

src/math_ops.rs

use 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.rs

use 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.rs

use 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(())
}

Python 코드

이번에도 정수 값을 담는 List 를 속성으로 가진 DataInput 클래스를 만들어 사용할 것이다.
이것은 데이터 검증 라이브러리 pydanticBaseModel 클래스를 상속받아 생성한다.

app/main.py

from 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를 통해 테스트를 진행해도 충분하다.

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글