비동기 처리

Pt J·2026년 3월 24일
post-thumbnail

비동기 처리

지금까지 우리가 작성했던 예제 코드는
비동기 처리를 하는 FastAPI에서 동기 처리를 하는 Rust 코드를 호출해
Rust 연산이 끝날 때까지 Python 이벤트 루프가 멈출 위험이 있다.

Rust에서 무거운 작업을 실행할 경우 async 로 비동기로 처리하고
이를 Python의 await 와 연결하여 이 문제를 해결할 수 있다.

작업공간 생성 및 구조 확인

~/workspace$ mkdir asynchronous && cd asynchronous
~/workspace/asynchronous$ python3 -m venv venv
~/workspace/asynchronous$ source venv/bin/activate
~/workspace/asynchronous$ pip install maturin fastapi uvicorn
~/workspace/asynchronous$ maturin init
~/workspace/asynchronous$ # 선택지 중 기본값인 PyO3 선택
~/workspace/asynchronous$ # Cargo.toml과 src/lib.rs가 자동 생성된다
~/workspace/asynchronous$ # Python 코드는 직접 생성해 주어야 한다
~/workspace/asynchronous$ mkdir app && touch app/main.py
~/workspace/asynchronous$ tree -I venv
.
├── app
│   └── main.py
├── Cargo.toml
├── pyproject.toml
└── src
    └── lib.rs

Cargo.toml 파일을 열어 라이브러리 이름을 수정해 주겠다.
비동기 처리를 위한 takio 크레이트와 pyo3_async_runtimes 크레이트도 추가해 준다.

가장 최신 takio 크레이트는 1.50 버전인데 1.0으로 작성해도 충분하다.
Cargo.toml 는 1.0이라고 적어놓은 버전을 "1.0 이상 2.0 미만의 최신 버전"으로 인식한다.
takio는 1.0 버전 이후로 매우 엄격한 하위 호환성을 지키고 있는 크레이트이므로
1.0 버전 이후의 어떤 버전을 내려받더라도 문제 없이 작동할 것이다.

pyo3_async_runtimes 크레이트는 pyo3와 tokio 사이의 다리 역할을 하는데
pyo3 버전이 올라갈 때마다 그것에 맞춰 변경점을 반영하기 때문에
pyo3와 버전을 맞춰주는 게 좋다.

pyo3의 features 항목에는 "extension-module"을 추가하지 않아도
Maturin이 자동으로 추가해서 Python 모듈로 생성할 수 있지만
비동기 크레이트들에는 features 항목을 적어주어야 한다.

Cargo.toml

[package]
name = "asynchronous"
version = "0.1.0"
edition = "2024"

[lib]
name = "rust_engine"
crate-type = ["cdylib"]

[dependencies]
pyo3 = "0.28.0"
tokio = { version = "1.50", features = ["full"] }
pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] }

코드 작성

Rust 코드

pyo3_async_runtimes를 사용하여 Rust의 Future
Python이 이해할 수 있는 Awaitable 객체(Coroutine)로 변환한다.

future_into_py 함수가 Rust의 tokio 런타임에서 실행되는 Future
Python의 asyncio 루프로 던져주기 때문에 Python은
Rust가 일을 끝낼 때까지 기다리지 않고 다른 HTTP 요청을 받을 수 있다.

async 블록 내에서 await 을 사용하면 비동기 처리를 할 수 있다.

src/lib.rs

use pyo3::prelude::*;
use std::time::Duration;
use tokio::time::sleep;

#[pyfunction]
fn async_compute(py: Python<'_>, seconds: u64) -> PyResult<Bound<'_, PyAny>> {
    pyo3_async_runtimes::tokio::future_into_py(py, async move {
        sleep(Duration::from_secs(seconds)).await;

        let result = format!("코어에서 {}초간 비동기 연산을 무사히 마쳤습니다!", seconds);
      
        Ok(result)
    })
}

#[pymodule]
fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(async_compute, m)?)?;

    Ok(())
}

Python 코드

Rust에서 작성한 비동기 함수를
Python에서 async def 를 통해 정의한 함수처럼 사용할 수 있다.

app/main.py

from fastapi import FastAPI
import rust_engine
import time

app = FastAPI()

@app.get("/")
def read_root():
    return {
        "status": "200",
        "info": "비동기 작업 중에도 응답할 수 있습니다."
    }

@app.get("/async-rust/{seconds}")
async def run_rust_task(seconds: int):
    start = time.time()

    message = await rust_engine.async_compute(seconds)

    end = time.time()

    return {
        "result": message,
        "duration": f"{end - start:.2f}s",
        "engine": "Rust (via pyo3-async-runtimes)"
    }

빌드 및 실행

Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
컴파일 후 pip list 명령어를 사용해 보면 Cargo.toml 파일에 작성한 패키지 이름을 확인할 수 있다.

uvicorn 라이브러리를 통해 FastAPI를 실행한다.

~/workspace/asynchronous$ maturin develop
~/workspace/asynchronous$ uvicorn app.main:app --reload

curl 명령어 또는 브라우저를 통해 다음과 같은 테스트를 해볼 수 있다.

  • http://127.0.0.1:8000/async-rust/10

전달한 숫자만큼의 시간이 지나기 전에 다른 창을 열고 다시 접속해 보면
대기 없이 접속되는 것을 확인할 수 있다.

Swagger UI를 통해 /async-rust 를 실행한 후
로딩 중인 동안 / 를 실행해 보는 식으로 테스트할 수도 있다.

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

0개의 댓글