10. 비동기 트레이트: 추상화의 완성 🟡
학습 목표:
- 트레이트 내부에서
async fn을 안정적으로 사용하기까지의 역사와 RPITIT 기술을 이해합니다.- 정적 디스패치와 동적 디스패치(
dyn) 환경에서 비동기 트레이트를 다루는 법을 배웁니다.- 멀티스레드 환경을 위한
Send제약 조건 해결사, **trait_variant**를 익힙니다.- Rust 1.85에서 안정화된 **비동기 클로저(
async Fn)**의 활용법을 파악합니다.
역사적 배경: 왜 트레이트 비동기는 어려웠나?
오랜 기간 Rust에서는 트레이트 안에 async fn을 직접 쓸 수 없었습니다. 비동기 함수는 내부적으로 이름 없는 복잡한 Future 타입을 반환하는데, 트레이트 시스템이 이를 일반화해서 다루기에 기술적 한계가 있었기 때문입니다.
드디어 해결되었습니다! (Rust 1.75+)
이제 특별한 크레이트 없이도 트레이트 내에 async fn을 선언할 수 있습니다.
1. 정적 디스패치 (RPITIT)
가장 권장되는 방식입니다. 컴파일 타임에 타입을 확정하므로 오버헤드가 전혀 없는 '제로 비용 추상화'를 실현합니다.
#![allow(unused)] fn main() { trait DataStore { async fn get(&self, key: &str) -> Option<String>; } // ✅ 제네릭과 함께 쓰면 성능 저하 없이 작동합니다. async fn lookup<S: DataStore>(store: &S, key: &str) { if let Some(val) = store.get(key).await { println!("검색 결과: {val}"); } } }
2. 동적 디스패치와 Send 문제
만약 Vec<Box<dyn DataStore>>처럼 동적으로 타입을 갈아 끼워야 한다면(dyn), 컴파일러는 퓨처의 크기를 알 수 없어 에러를 냅니다. 또한, 멀티스레드 실행기(Tokio)에서 쓰려면 퓨처가 Send여야 한다는 제약도 따라붙습니다.
해결사: trait_variant
Rust 팀에서 만든 이 도구는 Send 버전의 트레이트를 자동으로 생성해 줍니다.
#![allow(unused)] fn main() { // Cargo.toml: trait-variant = "0.1" #[trait_variant::make(SendDataStore: Send)] trait DataStore { async fn get(&self, key: &str) -> Option<String>; } // 이제 'SendDataStore'를 사용하면 dyn 디스패치와 tokio::spawn이 모두 가능해집니다. }
3. 비동기 클로저 (Rust 1.85+)
콜백 함수나 미들웨어를 짤 때 고대하던 기능입니다. 비동기 블록을 반환하는 일반 클로저의 구질구질한 문법을 한 줄로 정리해 줍니다.
#![allow(unused)] fn main() { // 1.85 이전: 어설픈 우회책 let fetcher = move || async move { reqwest::get(url).await }; // 1.85 이후: 네이티브 비동기 클로저 let fetcher = async move || { reqwest::get(url).await }; }
💡 실무 팁: async-trait 크레이트는 이제 졸업하세요
과거에는 #[async_trait] 매크로가 필수였지만, 이는 모든 퓨처를 강제로 힙(Heap)에 할당(Box::pin)하는 오버헤드가 있었습니다. 최신 Rust 프로젝트라면 성능을 위해 **네이티브 async fn**과 정적 디스패치를 우선적으로 고려하세요.
🏋️ 연습 문제: 캐시 서비스 설계하기
도전 과제: get과 set 메서드를 가진 비동기 Cache 트레이트를 설계하고, 다음 두 가지 방식으로 구현해 보세요.
HashMap을 사용하는 메모리 캐시- 네트워크 지연(20ms)을 시뮬레이션하는 가짜 외부 캐시
🔑 정답 및 힌트 보기
트레이트에 `async fn get(...)`과 `async fn set(...)`을 선언합니다. 메모리 캐시는 `tokio::sync::Mutex📌 요약
- Rust 1.75부터 트레이트 내 **
async fn**이 정식 지원됩니다. - **
trait_variant**를 쓰면dyn디스패치와Send문제를 쉽게 풀 수 있습니다. - 비동기 클로저는 1.85부터 더 깔끔한 콜백 설계를 도와줍니다.
- 성능이 민감한 구간에선
dyn보다 제네릭을 통한 정적 디스패치를 쓰세요.