Rust Tower 입문: Service 트레이트와 Layer로 미들웨어 합성하기
Rust로 HTTP 서버를 짜다 보면 신기한 일이 벌어지는데요. Hyper, Axum, Tonic 같은 프레임워크가 다 다르게 생겼는데, 한 번 만든 미들웨어를 그 사이에 그대로 옮겨 써도 작동합니다. 비밀은 이들이 공유하는 한 가지 추상화에 있는데요. 바로 Tower입니다.
이번 글에서는 Tower의 두 핵심 트레이트인 Service와 Layer를 살펴보고, ServiceBuilder로 미들웨어를 깔끔하게 합성하는 방법까지 정리해보겠습니다. 빌트인 미들웨어 활용은 Tower 미들웨어 실전에서, 직접 미들웨어를 만드는 방법은 Tower Layer 직접 만들기에서 별도로 다룹니다.
Tower가 푸는 문제
비동기 함수의 본질은 단순합니다. “요청을 받아서 응답을 만들어 돌려준다”는 한 줄짜리 흐름인데요.
async fn(Request) -> Result<Response, Error>
웹 핸들러도, gRPC 메서드도, 데이터베이스 쿼리도 결국 이 모양입니다. 그런데 각 라이브러리가 이 한 줄을 자기만의 트레이트로 다시 정의해두면, 미들웨어를 만들 때마다 라이브러리별 버전이 따로 필요해지죠.
Tower는 이 한 줄을 표준화한 트레이트 하나(Service)로 박아두고, 그 위에서 동작하는 도구들을 모두가 공유하자는 발상입니다. 그 결과 한 번 만든 타임아웃이나 재시도 로직을 Hyper, Axum, Tonic 어디서든 그대로 끼울 수 있게 되었죠.
Service 트레이트
Service 트레이트의 핵심을 단순화하면 다음과 같습니다.
trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
fn call(&mut self, req: Request) -> Self::Future;
}
call은 익숙합니다. 요청을 받아 응답 future를 돌려주는, 비동기 함수의 일반화된 버전이죠. 흥미로운 건 poll_ready인데요. 호출자가 “이 서비스 지금 요청 받을 준비 됐어?”를 물어보고, 서비스는 “준비 됐다” 또는 “잠깐, 기다려”로 답합니다.
여기서 Future가 왜 poll되는 값인지 감이 잘 안 온다면 Rust Future란 무엇인가를 먼저 읽어보셔도 좋습니다. Tower의 Service는 그 future 모델을 요청/응답 처리에 맞게 일반화한 형태입니다.
이 분리가 백프레셔(backpressure)의 토대가 됩니다. 서비스가 부하 한계에 도달했을 때 poll_ready가 Pending을 돌려주면, 호출자는 자연스럽게 멈춰서 기다릴 수 있죠. call만 있으면 모든 요청을 일단 받아 안에서 큐에 쌓는 식으로 동작할 수밖에 없는데, poll_ready가 있어서 처음부터 받지 않는 옵션이 생깁니다.
service_fn으로 빠르게 시작하기
매번 Service를 직접 구현하기는 번거롭기 때문에, Tower는 함수를 바로 서비스로 감싸주는 service_fn 헬퍼를 제공하는데요.
use tower::{service_fn, ServiceExt, BoxError};
#[tokio::main]
async fn main() -> Result<(), BoxError> {
let service = service_fn(|req: String| async move {
Ok::<_, BoxError>(format!("Hello, {req}!"))
});
let response = service.oneshot("World".to_string()).await?;
println!("{response}"); // Hello, World!
Ok(())
}
Cargo.toml에는 tower = { version = "0.5", features = ["util"] } 정도만 추가해두면 됩니다. oneshot은 ServiceExt가 제공하는 확장 메서드로, poll_ready → call 흐름을 한 번에 처리해주죠. 직접 호출할 때는 거의 항상 이 패턴을 씁니다.
Layer로 미들웨어 합성하기
Service가 “요청 처리 한 단계”라면, Layer는 “한 단계를 다른 한 단계로 감싸는 어댑터”입니다.
trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
}
타임아웃 미들웨어를 예로 들면, TimeoutLayer는 “어떤 서비스든 받아서 그 위에 시간 제한을 씌운 새 서비스로 만들어 돌려주는” 어댑터입니다. 사용자 입장에서는 “이 서비스에 5초 타임아웃을 추가해줘”처럼 선언적으로 끼울 수 있죠.
use tower::timeout::TimeoutLayer;
use std::time::Duration;
let timeout_layer = TimeoutLayer::new(Duration::from_secs(5));
let service = timeout_layer.layer(inner_service);
여러 레이어를 직접 손으로 쌓으면 타입이 금세 복잡해지는데요. 다음 섹션에서 보겠지만 ServiceBuilder가 이 부분을 깔끔하게 만들어줍니다.
ServiceBuilder로 깔끔하게 쌓기
ServiceBuilder는 빌더 패턴으로 여러 레이어를 한 줄씩 쌓아 합성합니다.
use tower::{ServiceBuilder, ServiceExt, BoxError, service_fn};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), BoxError> {
let service = ServiceBuilder::new()
.timeout(Duration::from_secs(30))
.concurrency_limit(10)
.rate_limit(5, Duration::from_secs(1))
.service_fn(|req: String| async move {
Ok::<_, BoxError>(format!("Response: {req}"))
});
let response = service.oneshot("Hello".to_string()).await?;
println!("{response}");
Ok(())
}
여기서 한 가지 헷갈리기 쉬운 지점이 적용 순서인데요. ServiceBuilder는 적은 순서대로 바깥쪽에서 안쪽으로 감쌉니다. 위 코드는 요청이 들어오면 timeout → concurrency_limit → rate_limit → service_fn 순으로 통과합니다. 응답은 반대 순서로 거슬러 나오죠.
이 순서가 중요한 이유는 의미가 달라지기 때문입니다. 예를 들어 timeout을 rate_limit 안쪽에 두면 rate limit으로 대기하는 시간도 timeout에 포함되지만, 바깥쪽에 두면 대기 시간을 빼고 실제 처리 시간만 잰 격이 됩니다. 같은 미들웨어 조합이라도 어떤 순서로 쌓느냐에 따라 동작이 달라지니, 한 번 짚어두면 좋습니다.
각 미들웨어의 구체적인 동작과 옵션은 Tower 미들웨어 실전에서 다룹니다.
생태계: 왜 Hyper도 Axum도 Tonic도 Tower인가
Tower의 진짜 가치는 미들웨어 한 번 만들면 어디서든 통한다는 점입니다.
Hyper는 저수준 HTTP 라이브러리인데 핵심 핸들러 타입이 Service입니다. Axum은 Hyper 위에 라우팅을 얹은 웹 프레임워크이고 라우터와 핸들러가 모두 Service입니다. Tonic은 gRPC 구현체이고 역시 Service로 만들어집니다.
이게 같은 tower::ServiceBuilder로 만든 미들웨어를 세 프레임워크 모두에 그대로 쓸 수 있다는 뜻인데요. tower-http에서 제공하는 압축, CORS, 트레이싱 같은 미들웨어가 Axum/Hyper/Tonic에 다 통하는 이유가 여기 있습니다.
프레임워크에 실제로 연결하는 예제는 Tower 미들웨어 실전에서 Axum, Tonic 코드와 함께 볼 수 있습니다.
마치며
Tower는 “비동기 요청 처리”라는 단순한 그림을 표준 트레이트로 박아두고, 그 위에서 도구들을 공유하게 만들었습니다. Service로 처리 단계를 표현하고, Layer로 단계를 감싸 확장하고, ServiceBuilder로 깔끔하게 합성하는 흐름이 핵심입니다.
다음 단계로는 Tower 미들웨어 실전에서 빌트인 미들웨어를 둘러보시거나, Tower Layer 직접 만들기에서 자기만의 미들웨어를 작성하는 방법을 살펴보시면 좋습니다. Rust 비동기 기초가 궁금하시다면 Rust 비동기 런타임 Tokio도 함께 보시기 바랍니다.
더 자세한 내용은 Tower 공식 문서를 참고하세요.
This work is licensed under
CC BY 4.0