슬기로운 개발자생활/Rust

Rust와 Axum을 활용한 웹 백엔드 개발 - 비동기 프로그래밍

개발자 소신 2024. 9. 23. 18:01
반응형

비동기 프로그래밍은 프로그램이 동시에 여러 작업을 처리할 수 있도록 해줍니다. 이는 특히 네트워크 요청, 파일 I/O 등 대기 시간이 긴 작업에서 효율적인 자원 활용을 가능하게 합니다. Rust는 안전성과 성능을 겸비한 비동기 프로그래밍 모델을 제공합니다. 이번 글에서는 Rust에서의 비동기 프로그래밍에 대해 알아보고, asyncawait 키워드를 사용하여 비동기 코드를 작성하는 방법을 배워보겠습니다.


1. 비동기 프로그래밍 이해하기

1.1 동기와 비동기의 차이점

  • 동기(synchronous) 프로그래밍: 작업이 순차적으로 실행되며, 이전 작업이 완료되어야 다음 작업을 시작할 수 있습니다.
  • 비동기(asynchronous) 프로그래밍: 작업이 동시에 진행될 수 있으며, 하나의 작업이 완료되기를 기다리지 않고 다른 작업을 수행할 수 있습니다.

1.2 비동기 프로그래밍의 필요성 및 장점

  • 효율적인 자원 활용: CPU가 대기 시간 없이 작업을 계속 수행할 수 있습니다.
  • 높은 응답성: 응용 프로그램이 사용자 입력에 빠르게 반응할 수 있습니다.
  • 확장성: 더 많은 작업을 동시에 처리할 수 있습니다.

2. asyncawait 키워드

2.1 async 함수 정의

  • async 키워드를 함수 앞에 붙여 비동기 함수를 정의합니다.
  • 비동기 함수는 Future를 반환합니다.
async fn my_async_function() {
    // 비동기 작업 수행
}

2.2 await를 통한 Future 완료 대기

  • await 키워드를 사용하여 Future의 완료를 비동기적으로 기다립니다.
async fn main() {
    my_async_function().await;
}

2.3 비동기 함수의 반환 타입

  • 비동기 함수는 컴파일러에 의해 Future를 반환하는 것으로 변환됩니다.
  • 반환 타입을 명시하고 싶다면 -> impl Future<Output = T> 형태로 지정할 수 있습니다.
use std::future::Future;

fn my_async_function() -> impl Future<Output = ()> {
    async {
        // 비동기 작업 수행
    }
}

3. Future 이해하기

3.1 Future의 개념과 역할

  • Future는 아직 완료되지 않은 값을 나타내는 객체입니다.
  • 비동기 작업의 결과를 나타내며, 작업이 완료되면 값을 반환합니다.

3.2 Future 트레이트의 동작 방식

  • Future 트레이트는 poll 메서드를 정의하며, 이 메서드는 작업의 진행 상황을 확인합니다.
  • 일반적으로 poll은 실행기(executor)에 의해 호출되며, 개발자가 직접 호출하지 않습니다.

4. 비동기 런타임 설정

4.1 비동기 런타임의 필요성

  • 비동기 함수는 Future를 반환하며, 이를 실행하려면 실행기(executor)가 필요합니다.
  • 실행기는 Futurepoll하여 작업을 진행시킵니다.

4.2 Tokio 런타임 소개

  • Tokio는 Rust에서 가장 널리 사용되는 비동기 런타임입니다.
  • 고성능 네트워킹 라이브러리를 포함하며, 비동기 코드를 실행하는 데 필요한 실행기를 제공합니다.

4.3 Tokio 설치 및 기본 설정

  • Cargo.tomltokio 의존성을 추가합니다.
[dependencies]
tokio = { version = "1.0", features = ["full"] }
  • 비동기 main 함수를 정의하기 위해 #[tokio::main] 어트리뷰트를 사용합니다.
#[tokio::main]
async fn main() {
    // 비동기 코드 실행
}

5. 비동기 함수 작성하기

5.1 동기 함수와의 차이점 이해

  • 동기 함수는 즉시 값을 반환하지만, 비동기 함수는 Future를 반환합니다.
  • 비동기 함수 내부에서 다른 비동기 함수를 호출할 때는 반드시 await 키워드를 사용해야 합니다.

5.2 비동기 함수에서 에러 처리 방법

  • 비동기 함수에서도 Result 타입을 사용하여 에러를 처리할 수 있습니다.
async fn fetch_data() -> Result<String, reqwest::Error> {
    let response = reqwest::get("https://example.com").await?;
    let body = response.text().await?;
    Ok(body)
}

5.3 비동기 코드 예제 실습

  • 간단한 HTTP 요청을 보내고 응답을 받는 비동기 함수를 작성해봅니다.
use reqwest;

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let response = reqwest::get("https://www.rust-lang.org").await?;
    let body = response.text().await?;
    println!("응답 본문:\n{}", body);
    Ok(())
}

6. 비동기 트레이트

6.1 트레이트에서 비동기 함수 사용의 제한 사항

  • Rust의 현재 안정 버전에서는 트레이트의 메서드에 async를 직접 사용할 수 없습니다.

6.2 async-trait 크레이트를 통한 해결 방법

  • async-trait 크레이트를 사용하면 트레이트 메서드에서 비동기 함수를 정의할 수 있습니다.
  • Cargo.toml에 의존성 추가:
[dependencies]
async-trait = "0.1"
  • 사용 예시:
use async_trait::async_trait;

#[async_trait]
trait AsyncTrait {
    async fn perform(&self);
}

struct MyStruct;

#[async_trait]
impl AsyncTrait for MyStruct {
    async fn perform(&self) {
        // 비동기 작업 수행
    }
}

7. 비동기를 활용한 동시성 프로그래밍

7.1 태스크 생성 및 실행 (tokio::spawn)

  • tokio::spawn을 사용하여 비동기 태스크를 생성하고 실행할 수 있습니다.
#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // 비동기 작업
    });

    // 태스크 완료 대기
    handle.await.unwrap();
}

7.2 여러 비동기 작업의 동시 실행

  • 여러 비동기 작업을 동시에 실행하여 성능을 향상시킬 수 있습니다.

7.3 join! 매크로를 통한 병렬 처리

  • futures 크레이트의 join! 매크로를 사용하여 여러 Future를 동시에 실행합니다.
  • Cargo.toml에 의존성 추가:
[dependencies]
futures = "0.3"
  • 사용 예시:
use futures::join;

async fn task_one() {
    // 작업 1
}

async fn task_two() {
    // 작업 2
}

#[tokio::main]
async fn main() {
    let ((), ()) = join!(task_one(), task_two());
}

8. 비동기 I/O 작업

8.1 파일 읽기/쓰기의 비동기 처리

  • tokio의 파일 시스템 모듈을 사용하여 비동기 파일 I/O를 수행할 수 있습니다.
use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut file = File::open("foo.txt").await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    println!("파일 내용: {}", contents);
    Ok(())
}

8.2 네트워킹에서의 비동기 처리

  • tokio의 네트워킹 모듈을 사용하여 비동기 TCP 서버나 클라이언트를 구현할 수 있습니다.
use tokio::net::TcpListener;
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (mut socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            // 소켓으로부터 데이터 읽기
            match socket.read(&mut buf).await {
                Ok(0) => return, // 연결 종료
                Ok(n) => {
                    // 에코(Echo) 서버 구현
                    if let Err(e) = socket.write_all(&buf[..n]).await {
                        eprintln!("데이터 전송 실패: {}", e);
                    }
                }
                Err(e) => {
                    eprintln!("데이터 수신 실패: {}", e);
                }
            }
        });
    }
}

8.3 async를 지원하는 표준 라이브러리와 크레이트 소개

  • 표준 라이브러리: 현재 Rust 표준 라이브러리는 일부 비동기 기능만 제공합니다.
  • 주요 크레이트:
    • tokio: 비동기 런타임 및 네트워킹, 파일 I/O 지원
    • async-std: 다른 인기 있는 비동기 런타임
    • reqwest: 비동기 HTTP 클라이언트
    • hyper: 고성능 HTTP 라이브러리

9. 주의사항 및 모범 사례

9.1 비동기 코드에서의 소유권과 라이프타임 이슈

  • 비동기 함수에서는 소유권과 라이프타임에 주의해야 합니다.
  • 비동기 함수 내부에서 참조자를 반환하려면 라이프타임을 명시하거나 Arc 같은 스마트 포인터를 사용할 수 있습니다.

9.2 블로킹 코드 피하기

  • 비동기 코드에서는 블로킹 함수를 사용하면 전체 스레드가 멈출 수 있으므로 피해야 합니다.
  • 필요한 경우 spawn_blocking을 사용하여 별도의 스레드에서 블로킹 작업을 수행합니다.
tokio::task::spawn_blocking(|| {
    // 블로킹 작업
});

9.3 비동기 코드 디버깅 팁

  • RUST_BACKTRACE=1 환경 변수를 설정하여 백트레이스를 활성화합니다.
  • tokiotracing 기능을 활용하여 로그를 남길 수 있습니다.

결론

비동기 프로그래밍은 Rust에서 고성능의 효율적인 프로그램을 작성하는 데 필수적인 기술입니다. 이번 글에서는 asyncawait의 기본 개념부터 비동기 런타임인 Tokio를 사용하여 비동기 코드를 작성하는 방법까지 살펴보았습니다. 비동기 프로그래밍을 잘 활용하면 네트워크 요청, 파일 I/O 등 대기 시간이 긴 작업에서 프로그램의 성능과 응답성을 크게 향상시킬 수 있습니다.

 

연습 과제:

  1. reqwest 크레이트를 사용하여 여러 웹사이트에서 동시에 데이터를 가져오는 프로그램을 작성해보세요.
  2. 비동기 TCP 서버를 구현하여 클라이언트로부터 메시지를 받아 에코하는 서버를 만들어보세요.
  3. tokio::fs 모듈을 사용하여 여러 파일을 동시에 읽고 처리하는 프로그램을 작성해보세요.

참고 자료


Note: 이 글은 Rust에서의 비동기 프로그래밍을 소개하기 위한 것으로, 더 깊은 이해를 위해서는 공식 문서와 추가 자료를 참고하시기 바랍니다.

반응형