본문 바로가기
카테고리 없음

Rust 비동기 프로그래밍 | async/await 완전 입문

by mystory55801 2025. 6. 7.

Rust에서 비동기 프로그래밍이란?

Rust는 성능과 안전성을 동시에 제공하는 언어로 유명하지만, 비동기 프로그래밍(async programming)도 강력하게 지원합니다. Rust의 비동기 프로그래밍은 non-blocking 방식으로 동작하며, 효율적인 I/O 처리에 최적화되어 있습니다.

Rust에서는 async/await 문법을 사용하여 비동기 코드를 동기 코드처럼 읽기 쉽게 작성할 수 있습니다. 이 글에서는 Rust에서 비동기 코드를 작성하는 방법과 주요 개념들을 단계별로 설명합니다.

async/await 기본 문법

Rust에서 비동기 함수를 정의하려면 async fn을 사용합니다. 비동기 함수는 Future를 반환하며, 실행을 위해 .await가 필요합니다.


async fn say_hello() {
    println!("Hello from async function!");
}

#[tokio::main]
async fn main() {
    say_hello().await;
}

이 예제에서는 say_hello()가 Future를 반환하고, .await를 통해 실제 실행됩니다. Tokio는 Rust에서 가장 널리 사용되는 비동기 런타임 중 하나로, #[tokio::main]을 사용해 비동기 main 함수를 실행할 수 있게 해줍니다.

Future란 무엇인가요?

Rust에서 async fn은 내부적으로 Future라는 트레잇을 구현합니다. Future는 아직 완료되지 않은 비동기 작업을 나타내며, .await를 호출할 때까지 실행되지 않습니다.

  • lazy evaluation: Future는 생성만 해도 아무 일도 하지 않음
  • poll 기반 실행: 런타임이 Future를 polling 하면서 진행

use std::future::Future;

async fn compute() -> i32 {
    42
}

fn get_future() -> impl Future<Output = i32> {
    compute()
}

이처럼 async fn의 반환 타입은 impl Future이며, 명시적으로 사용하면 추상화된 타입을 더 명확히 이해할 수 있습니다.

async 블록과 비동기 클로저

Rust에서는 비동기 블록도 사용할 수 있습니다. async { }는 즉시 실행 가능한 Future를 생성합니다.


#[tokio::main]
async fn main() {
    let future = async {
        println!("Inside async block");
        123
    };

    let result = future.await;
    println!("결과: {}", result);
}

이 기능은 동적으로 비동기 동작을 정의하거나 클로저로 전달할 때 매우 유용합니다.

비동기 함수에서 다른 비동기 함수 호출

Rust에서는 비동기 함수 내에서 자유롭게 다른 비동기 함수를 호출할 수 있습니다. 중요한 점은 반드시 .await를 붙여야 실제 실행이 된다는 것입니다.


async fn fetch_data() -> String {
    "데이터".to_string()
}

async fn process() {
    let data = fetch_data().await;
    println!("가져온 데이터: {}", data);
}

fetch_data().await가 없으면 Future만 생성되고 실행되지 않기 때문에 주의해야 합니다.

Tokio와 async-std: 비동기 런타임 비교

Rust는 기본적으로 비동기 실행을 위한 런타임을 제공하지 않기 때문에, Tokioasync-std 같은 서드파티 런타임을 사용해야 합니다.

  • Tokio: 고성능, 멀티스레드 지원, 실무에서 널리 사용됨
  • async-std: Rust 표준 라이브러리 스타일에 가깝고 학습하기 쉬움

# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }

#[tokio::main] 매크로를 사용하면, 비동기 main 함수가 자동으로 런타임 위에서 실행됩니다.

spawn으로 병렬 비동기 작업 실행

여러 개의 비동기 작업을 병렬로 실행하려면 tokio::spawn을 사용할 수 있습니다.


use tokio::task;

async fn task1() {
    println!("Task 1 실행 중");
}

async fn task2() {
    println!("Task 2 실행 중");
}

#[tokio::main]
async fn main() {
    let handle1 = task::spawn(task1());
    let handle2 = task::spawn(task2());

    handle1.await.unwrap();
    handle2.await.unwrap();
}

spawn은 새로운 비동기 태스크를 생성하고, JoinHandle을 반환합니다. 이 핸들을 통해 결과를 기다릴 수 있습니다.

select! 매크로로 비동기 작업 경쟁 처리

Tokio의 select! 매크로를 사용하면, 여러 비동기 작업 중 가장 먼저 완료된 작업을 선택할 수 있습니다.


use tokio::time::{sleep, Duration};
use tokio::select;

#[tokio::main]
async fn main() {
    let task1 = sleep(Duration::from_secs(3));
    let task2 = sleep(Duration::from_secs(1));

    select! {
        _ = task1 => println!("task1 완료"),
        _ = task2 => println!("task2 완료"),
    }
}

select!경쟁 상황 처리에 매우 유용하며, 네트워크 요청 등에서 사용됩니다.

결론: Rust의 async/await는 안전하면서도 강력하다

Rust의 async/await 기능은 복잡한 비동기 처리를 간결하고 안전하게 만들어 줍니다. Future, await, 런타임 개념만 정확히 이해하면 고성능 네트워크 애플리케이션부터 파일 I/O까지 다양한 비동기 작업을 쉽게 처리할 수 있습니다.

초보자라면 Tokio를 기반으로 간단한 비동기 코드를 작성하며 개념을 익히는 것이 좋습니다. Rust의 메모리 안전성과 비동기의 조합은 고성능 서버 프로그래밍에 최적입니다.