Axum의 타워 스택을 통과하는 요청의 여정 해부하기
Emily Parker
Product Engineer · Leapcell

소개
현대 웹 개발의 활기찬 생태계에서 웹 프레임워크가 들어오는 요청을 어떻게 처리하는지 이해하는 것은 강력하고 확장 가능하며 유지보수 가능한 애플리케이션을 구축하는 데 매우 중요합니다. Rust는 성능과 안전성에 중점을 두고 있어 매력적인 솔루션을 제공하며, Axum은 Tokio와 고도로 확장 가능한 Tower 에코시스템을 기반으로 구축된 강력하고 사용하기 쉬운 웹 프레임워크로 두드러집니다. Axum은 훌륭한 개발자 경험을 제공하지만, 실제 마법은 종종 타워 서비스 스택 내부의 배경에서 일어납니다. 성능을 최적화하거나 복잡한 문제를 디버그하거나 사용자 정의 미들웨어를 구현하려는 개발자에게는 피상적인 이해만으로는 충분하지 않습니다. 이 글은 Axum의 타워 서비스 스택을 통과하는 요청의 전체 수명 주기를 깊이 파고들어 기본 메커니즘을 명확히 하고 잠재력을 최대한 활용할 수 있도록 지원합니다.
요청 처리 여정 분석
요청의 여정을 추적하기 전에 Axum의 요청 처리를 뒷받침하는 핵심 용어에 대한 공통된 이해를 확립해 보겠습니다.
핵심 용어
- Tower: 모듈식이고 재사용 가능한 네트워크 서비스를 구축하기 위한 일련의 트레잇을 제공하는 기본 라이브러리입니다. 핵심에는
Service트레잇,Layer트레잇,ServiceBuilder가 있습니다. Service<Request>트레잇: Tower의 기본 구성 요소입니다. 요청을 받아 응답으로 해결되는 future를 반환하는 비동기 함수를 나타냅니다. 주요 메서드는call(&mut self, req: Request) -> Self::Future입니다.Layer트레잇: 기존Service를 래핑하여 새로운 동작을 추가하거나 요청/응답을 수정하는 미들웨어와 같은 구성 요소입니다.Layer의service메서드는 내부Service를 받아 이를 래핑하는 새Service를 반환합니다.ServiceBuilder: 여러Layer를 체인으로 연결하여 복잡한 서비스 스택을 구축하는 데 도움이 되는 유형입니다. 레이어를 순차적으로 적용하는 편리한 API를 제공합니다.- Axum: Tokio와 Tower를 기반으로 구축된 웹 프레임워크입니다. 라우팅, 요청에서 데이터 추출, 상태 처리, 응답 생성에 대한 사용하기 쉬운 유틸리티를 제공하며, 서비스 처리를 위해 Tower를 활용합니다.
- Handler: Axum에서 다양한 추출기를 인수로 받아 응답으로 변환될 수 있는 유형을 반환하는 함수 또는 메서드입니다. 핸들러는 본질적으로 특수화된 서비스입니다.
- Extractor:
FromRequestParts또는FromRequest트레잇을 구현하는 유형으로, Axum이 들어오는 요청의 특정 부분(예: 경로 매개변수, 쿼리 문자열, 요청 본문)을 구문 분석하여 핸들러에서 사용할 수 있도록 합니다.
요청의 오디세이
HTTP 요청이 Axum 애플리케이션에 도착하면 일련의 서비스와 레이어를 통과하는 예측 가능하고 세밀하게 조정된 여정을 시작합니다. 이 프로세스를 단계별로 분석해 보겠습니다.
1. 서버 수신 및 MakeService
가장 먼저 Axum 애플리케이션은 Hyper와 같은 서버를 사용하여 일반적으로 TCP 포트에 바인딩됩니다. Hyper 또는 다른 HTTP 서버는 들어오는 각 연결에 대해 새 Service를 생성할 방법이 필요합니다. 여기서 MakeService 트레잇이 사용됩니다. Axum의 Router는 본질적으로 MakeService를 구현하므로 Hyper가 모든 새 연결에 대해 새 Router 서비스를 인스턴스화할 수 있습니다.
// 서버가 MakeService를 사용하는 방법의 단순화된 예 use tower::make::MakeService; use tower::Service; use hyper::Request; // http Request 유형에 Hyper를 가정 async fn serve_connection<M>(make_service: &M) where M: MakeService<(), hyper::Request<hyper::body::Incoming>>, { let mut service = make_service.make_service(()).await.unwrap(); // 클라이언트로부터 들어오는 요청이라고 가정 let request = Request::builder().uri("/").body(hyper::body::Incoming::empty()).unwrap(); let response = service.call(request).await.unwrap(); println!("Response: {:?}", response.status()); }
2. 루트 Router 서비스
Router는 Axum 애플리케이션의 중앙 오케스트레이터입니다. 본질적으로 내부적으로 라우팅 트리를 포함하는 큰 Service입니다. Router에서 service.call(request)가 호출되면 먼저 들어오는 요청의 경로 및 메서드와 등록된 경로를 일치시키려고 시도합니다.
3. 타워 Layer 사전 처리
요청이 특정 핸들러에 도달하기 전에 일반적으로 Layer (미들웨어) 스택을 통과합니다. 이러한 레이어는 Axum Router를 구성할 때 ServiceBuilder를 사용하여 적용됩니다. 각 Layer는 내부 서비스를 래핑하여 로깅, 인증, 압축, 오류 처리 또는 상태 관리와 같은 기능을 추가합니다.
로깅 및 인증 레이어가 포함된 예를 살펴보겠습니다.
use axum::{ routing::get, Router, response::IntoResponse, http::{Request, StatusCode}, extract::FromRef, }; use tower::{Layer, Service}; use std::task::{Poll, Context}; use std::future::Ready; use std::{pin::Pin, future::Future}; // 간단한 인증 미들웨어 #[derive(Clone)] struct AuthMiddleware<S> { inner: S, } impl<S, B> Service<Request<B>> for AuthMiddleware<S> where S: Service<Request<B>, Response = axum::response::Response> + Send + 'static, S::Future: Send + 'static, B: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request<B>) -> Self::Future { let auth_header = req.headers().get("Authorization"); if let Some(header_value) = auth_header { if header_value == "Bearer mysecrettoken" { // 인증 성공, 내부 서비스로 전달 let fut = self.inner.call(req); Box::pin(async move { fut.await }) } else { // 잘못된 토큰 Box::pin(async move { Ok(StatusCode::UNAUTHORIZED.into_response()) }) } } else { // 인증 헤더 없음 Box::pin(async move { Ok(StatusCode::UNAUTHORIZED.into_response()) }) } } } // AuthMiddleware를 생성하는 레이어 struct AuthLayer; impl<S> Layer<S> for AuthLayer { type Service = AuthMiddleware<S>; fn service(&self, inner: S) -> Self::Service { AuthMiddleware { inner } } } async fn hello_world() -> String { "Hello, authorized world!".to_string() } #[tokio::main] async fn main() { let app = Router::new() .route("/", get(hello_world)) .layer(AuthLayer); // 사용자 정의 AuthLayer 적용 let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap(); }
이 예에서 AuthLayer는 hello_world 핸들러를 명시적으로 래핑합니다. /에 대한 요청이 들어오면 먼저 AuthMiddleware를 통과합니다. 인증에 실패하면 미들웨어는 즉시 UNAUTHORIZED 응답을 반환하여 요청이 hello_world에 도달하는 것을 방지합니다. 성공하면 AuthMiddleware가 내부 서비스(이 경우 hello_world 핸들러)를 호출하고 해당 응답이 반환됩니다.
4. 경로 일치 및 핸들러 선택
요청이 전역 레이어를 통과하면 Router가 라우팅 로직을 수행합니다. 요청의 경로 및 HTTP 메서드와 일치하는 경로를 찾으면 Router는 해당 경로와 연결된 특정 핸들러 함수를 식별합니다.
5. 핸들러의 추출기 체인
실제로 핸들러 함수가 실행되기 전에 Axum의 강력한 추출기 시스템이 작동합니다. 핸들러 서명의 각 인수(예: Path, Query, Json, State)에 대해 Axum은 해당 추출기를 호출합니다. 각 추출기는 본질적으로 Request를 처리하고 필요한 데이터 유형을 생성하는 Service입니다. 이 프로세스는 핸들러에 정의된 인수 순서대로 왼쪽에서 오른쪽으로 순차적으로 발생합니다.
use axum::{ extract::{Path, Query, Json, State}, response::{IntoResponse, Response}, routing::get, Router, }; use serde::Deserialize; use std::sync::Arc; async fn handler_with_extractors( Path(user_id): Path<u32>, Query(params): Query<QueryParams>, Json(payload): Json<UserPayload>, State(app_state): State<Arc<AppState>>, ) -> Response { println!("User ID: {}", user_id); println!("Query Params: {:?}", params); println!("Payload: {:?}", payload); println!("App State: {:?}", app_state); format!("Processed user {} with state {}", user_id, app_state.name).into_response() } #[derive(Deserialize, Debug)] struct QueryParams { name: String, age: u8, } #[derive(Deserialize, Debug)] struct UserPayload { email: String, } #[derive(Debug)] struct AppState { name: String, } #[tokio::main] async fn main() { let app_state = Arc::new(AppState { name: "My Awesome App".to_string(), }); let app = Router::new() .route("/users/:user_id", get(handler_with_extractors)) .with_state(app_state); // ... (이전 예와 유사한 서버 설정은 생략됨) }
handler_with_extractors에서 요청 부분은 다음과 같은 순서로 처리됩니다.
Path(user_id): URL 경로에서user_id를 추출합니다.Query(params): 쿼리 매개변수를QueryParams로 역직렬화합니다.Json(payload): 요청 본문(존재하고 유효한 JSON인 경우)을UserPayload로 역직렬화합니다.State(app_state): 공유 애플리케이션 상태를 검색합니다.
추출기 중 하나라도 실패하면(예: 잘못된 JSON, 누락된 경로 세그먼트) Axum은 자동으로 적절한 오류 응답(예: 400 Bad Request, 404 Not Found)을 생성하고 요청 처리를 조기 종료하여 핸들러가 호출되지 않도록 합니다. 이 오류는 존재하는 경우 외부 오류 처리 레이어에 의해 일반적으로 처리됩니다.
6. 핸들러 실행 및 응답 생성
마지막으로 모든 추출기가 성공하면 추출된 인수로 핸들러 함수가 호출됩니다. 그런 다음 핸들러는 애플리케이션별 로직을 수행하고, 데이터베이스와 상호 작용하고, 다른 서비스를 호출하고, 궁극적으로 IntoResponse를 구현하는 값을 구성합니다. Axum은 이 값을 받아 완전한 HTTP 응답으로 변환합니다.
7. 타워 Layer 후처리
핸들러가 응답을 생성한 후, 요청이 통과한 것과 동일한 레이어 스택을 통해 응답이 역으로 이동합니다. 각 레이어는 응답을 검사하거나 수정할 기회를 얻습니다. 예를 들어, 로깅 레이어는 응답 상태 코드를 기록할 수 있고, 압축 레이어는 응답 본문을 압축할 수 있습니다.
흐름은 중첩된 호출 집합으로 시각화할 수 있습니다.
+-------------------------------------------------+
| 서버 연결 |
| +---------------------------------------------+
| | 전역 타워 레이어 1 |
| | +-----------------------------------------+
| | | 전역 타워 레이어 2 |
| | | +-------------------------------------+
| | | | Axum 라우터 서비스 |
| | | | +---------------------------------+
| | | | | 경로 일치 및 핸들러 선택 |
| | | | | +-----------------------------+
| | | | | | 추출기 1 서비스 | -- 요청
| | | | | +-------------------------+
| | | | | | 추출기 2 서비스 | -- 요청
| | | | | | +---------------------+
| | | | | | | ... | -- 요청
| | | | | | | +-----------------+
| | | | | | | | 실제 핸들러 | -- 요청 -> 응답
| | | | | | | +-----------------+
| | | | | | | ... | <- 응답
| | | | | +-------------------------+
| | | | | 추출기 2 출력 | <- 응답
| | | +---------------------------------+
| | | Axum 라우터 서비스 출력 | <- 응답
| | +-------------------------------------+
| | 전역 타워 레이어 2 출력 | <- 응답
| +---------------------------------------------+
| 전역 타워 레이어 1 출력 | <- 응답
+-------------------------------------------------+
다이어그램의 각 "Service"는 호출될 때 Request를 받아 Response로 해결되는 Future를 반환합니다. Layer는 "내부" 서비스를 래핑하여 순서를 결정합니다.
결론
Axum 애플리케이션의 타워 서비스 스택을 통과하는 요청의 여정은 모듈식 구성 요소의 세심하게 조정된 춤입니다. 초기 서버 수신부터 최종 응답까지 각 Layer와 Service는 자신의 부분을 기여하여 강력하고 유연한 처리 파이프라인을 만듭니다. 이 복잡한 흐름을 이해함으로써 개발자는 Axum 및 Tower의 검증된 패턴을 기반으로 한 강력하고 고도로 구성 가능한 엔진이 있음을 인식하고 자신감을 가지고 Axum 애플리케이션을 디버그, 최적화 및 확장하는 데 필요한 명확성을 얻습니다. 본질적으로 비동기적이고 조합 가능한 Tower Service 트레잇은 Axum의 효율적이고 복원력 있는 요청 처리의 진정한 주역입니다.