슬기로운 개발자생활/Rust
Rust와 Axum을 활용한 웹 백엔드 개발 - 비동기 프로그래밍
개발자 소신
2024. 9. 23. 18:01
반응형
비동기 프로그래밍은 프로그램이 동시에 여러 작업을 처리할 수 있도록 해줍니다. 이는 특히 네트워크 요청, 파일 I/O 등 대기 시간이 긴 작업에서 효율적인 자원 활용을 가능하게 합니다. Rust는 안전성과 성능을 겸비한 비동기 프로그래밍 모델을 제공합니다. 이번 글에서는 Rust에서의 비동기 프로그래밍에 대해 알아보고, async
와 await
키워드를 사용하여 비동기 코드를 작성하는 방법을 배워보겠습니다.
1. 비동기 프로그래밍 이해하기
1.1 동기와 비동기의 차이점
- 동기(synchronous) 프로그래밍: 작업이 순차적으로 실행되며, 이전 작업이 완료되어야 다음 작업을 시작할 수 있습니다.
- 비동기(asynchronous) 프로그래밍: 작업이 동시에 진행될 수 있으며, 하나의 작업이 완료되기를 기다리지 않고 다른 작업을 수행할 수 있습니다.
1.2 비동기 프로그래밍의 필요성 및 장점
- 효율적인 자원 활용: CPU가 대기 시간 없이 작업을 계속 수행할 수 있습니다.
- 높은 응답성: 응용 프로그램이 사용자 입력에 빠르게 반응할 수 있습니다.
- 확장성: 더 많은 작업을 동시에 처리할 수 있습니다.
2. async
와 await
키워드
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)가 필요합니다. - 실행기는
Future
를poll
하여 작업을 진행시킵니다.
4.2 Tokio 런타임 소개
- Tokio는 Rust에서 가장 널리 사용되는 비동기 런타임입니다.
- 고성능 네트워킹 라이브러리를 포함하며, 비동기 코드를 실행하는 데 필요한 실행기를 제공합니다.
4.3 Tokio 설치 및 기본 설정
Cargo.toml
에tokio
의존성을 추가합니다.
[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
환경 변수를 설정하여 백트레이스를 활성화합니다.tokio
의tracing
기능을 활용하여 로그를 남길 수 있습니다.
결론
비동기 프로그래밍은 Rust에서 고성능의 효율적인 프로그램을 작성하는 데 필수적인 기술입니다. 이번 글에서는 async
와 await
의 기본 개념부터 비동기 런타임인 Tokio
를 사용하여 비동기 코드를 작성하는 방법까지 살펴보았습니다. 비동기 프로그래밍을 잘 활용하면 네트워크 요청, 파일 I/O 등 대기 시간이 긴 작업에서 프로그램의 성능과 응답성을 크게 향상시킬 수 있습니다.
연습 과제:
reqwest
크레이트를 사용하여 여러 웹사이트에서 동시에 데이터를 가져오는 프로그램을 작성해보세요.- 비동기 TCP 서버를 구현하여 클라이언트로부터 메시지를 받아 에코하는 서버를 만들어보세요.
tokio::fs
모듈을 사용하여 여러 파일을 동시에 읽고 처리하는 프로그램을 작성해보세요.
참고 자료
- Asynchronous Programming in Rust (공식 문서)
- Tokio 공식 문서
- Rust Standard Library API Documentation - Future
- async-trait 크레이트
Note: 이 글은 Rust에서의 비동기 프로그래밍을 소개하기 위한 것으로, 더 깊은 이해를 위해서는 공식 문서와 추가 자료를 참고하시기 바랍니다.
반응형