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의 효율적이고 복원력 있는 요청 처리의 진정한 주역입니다.