Rust의 Axum을 이용한 모듈식 웹 API 구축
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
끊임없이 진화하는 백엔드 개발 환경에서 강력하고 확장 가능하며 유지 관리가 가능한 웹 API를 구축하는 것이 가장 중요합니다. 성능, 안전성 및 동시성에 중점을 둔 Rust는 이 도메인에서 강력한 선택으로 부상했습니다. 그러나 Rust의 원시적인 힘은 특히 복잡한 애플리케이션 로직을 조정할 때 더 높은 학습 곡선을 동반하는 경우가 많습니다. 이것이 바로 Axum과 같은 최신 웹 프레임워크가 빛을 발하는 곳입니다. 강력한 Tokio 런타임과 다목적 Tower 생태계를 기반으로 구축된 Axum은 Rust에서 웹 서비스를 구성할 수 있는 높은 수준의 인체 공학적인 방법을 제공합니다. API 엔드포인트를 구성하고, 공유 상태를 효율적으로 관리하며, 강력한 미들웨어를 일관된 방식으로 통합할 수 있도록 모듈성의 개념을 채택합니다. 이 문서는 Axum을 사용하여 모듈식 웹 API를 구축하는 과정을 안내하고, 라우팅을 효과적으로 처리하고, 애플리케이션 상태를 공유하고, Tower 서비스에서 제공하는 확장성을 활용하는 방법을 시연합니다.
핵심 개념 이해
구현에 들어가기 전에 Axum으로 API를 구축하는 데 중심이 되는 몇 가지 기본 개념을 명확히 합시다.
- Axum: Rust용 웹 애플리케이션 프레임워크입니다.
tokio
(비동기 런타임) 및hyper
(HTTP 라이브러리) 위에 구축되었으며 미들웨어 및 서비스 구성을 위해tower
생태계를 많이 활용합니다. - 라우팅: URL 경로 및 HTTP 메서드를 기반으로 들어오는 HTTP 요청을 적절한 핸들러 함수로 전달하는 메커니즘입니다. Axum은 경로를 정의하는 선언적이고 타입 안전한 방법을 제공합니다.
- 상태 관리: 웹 애플리케이션에서는 종종 다른 요청 핸들러 간에 데이터를 공유해야 할 수 있습니다(예: 데이터베이스 연결, 구성, 캐시). Axum은 애플리케이션 전체 및 요청 범위의 상태를 관리하기 위한 강력한 메커니즘을 제공합니다.
- Tower 서비스: Tower는 강력한 네트워크 애플리케이션을 구축하기 위한 모듈식 재사용 가능한 구성 요소 라이브러리입니다. Axum에서 핸들러는 본질적으로
Tower.Service
구현이며 미들웨어는 서비스를 래핑하는Tower.Layer
입니다. 이 아키텍처는 구성 및 재사용을 촉진합니다. - 미들웨어: 서버와 핸들러 사이에 위치하여 요청을 사전 처리하거나 응답을 사후 처리할 수 있는 함수 또는 서비스입니다. 일반적인 용도에는 인증, 로깅, 오류 처리 및 속도 제한이 포함됩니다.
모듈식 웹 API 구축
사용자 목록을 관리하는 간단한 API를 구축하여 모듈식 라우팅, 상태 공유 및 Tower 서비스 적용을 시연해 보겠습니다.
프로젝트 설정
먼저 새 Rust 프로젝트를 만듭니다.
car go new axum_modular_api cd axum_modular_api
Cargo.toml
에 필요한 종속성을 추가합니다.
[dependencies] axum = { version = "0.7", features = ["macros"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tracing = "0.1" tracing-subscriber = "0.3"
애플리케이션 상태 정의
사용자 관리 API의 경우 사용자를 저장할 방법이 필요합니다. Arc<Mutex<...>>
로 래핑된 간단한 Vec<User>
는 인메모리 상태의 좋은 시작점입니다.
src/models.rs
파일을 만듭니다.
// src/models.rs use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub id: u32, pub name: String, pub email: String, } pub type AppState = Arc<Mutex<Vec<User>>>; pub fn initialize_state() -> AppState { Arc::new(Mutex::new(vec![ User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() }, User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() }, ])) }
모듈식 라우팅
더 나은 유지 관리를 위해 라우트를 별도의 모듈로 구성합니다. src/routes/mod.rs
및 src/routes/users.rs
파일을 만듭니다.
src/routes/users.rs
: 이 모듈에는 사용자 관련 모든 엔드포인트가 포함됩니다.
// src/routes/users.rs use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use serde_json::json; use crate::models::{AppState, User}; pub fn users_router() -> Router<AppState> { Router::new() .route("/", get(list_users).post(create_user)) .route("/:id", get(get_user).put(update_user).delete(delete_user)) } async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> { let users = state.lock().unwrap(); Json(users.clone()) } async fn get_user(State(state): State<AppState>, Path(id): Path<u32>) -> Result<Json<User>, StatusCode> { let users = state.lock().unwrap(); if let Some(user) = users.iter().find(|u| u.id == id) { Ok(Json(user.clone())) } else { Err(StatusCode::NOT_FOUND) } } async fn create_user(State(state): State<AppState>, Json(mut new_user): Json<User>) -> impl IntoResponse { let mut users = state.lock().unwrap(); let next_id = users.iter().map(|u| u.id).max().unwrap_or(0) + 1; new_user.id = next_id; users.push(new_user.clone()); (StatusCode::CREATED, Json(new_user)) } async fn update_user( State(state): State<AppState>, Path(id): Path<u32>, Json(updated_user): Json<User>, ) -> impl IntoResponse { let mut users = state.lock().unwrap(); if let Some(user) = users.iter_mut().find(|u| u.id == id) { user.name = updated_user.name; user.email = updated_user.email; (StatusCode::OK, Json(user.clone())) } else { (StatusCode::NOT_FOUND, Json(json!({"message": "User not found"}))) } } async fn delete_user(State(state): State<AppState>, Path(id): Path<u32>) -> StatusCode { let mut users = state.lock().unwrap(); let initial_len = users.len(); users.retain(|u| u.id != id); if users.len() < initial_len { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND } }
src/routes/mod.rs
: 이 모듈은 하위 라우터를 다시 내보내며 잠재적으로 공통 라우트를 포함합니다.
// src/routes/mod.rs pub mod users;
메인 애플리케이션 구성
이제 src/main.rs
에서 모든 것을 함께 가져옵니다. 애플리케이션 상태를 초기화하고, 라우터를 구성하고, tracing
을 사용하여 기본 로깅을 추가합니다.
// src/main.rs mod models; mod routes; use axum::{ routing::get, Router, }; use tower_http::trace::{ self, TraceLayer, }; tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use std::time::Duration; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "axum_modular_api=debug,tower_http=debug".into()) знаходжу ) .with(tracing_subscriber::fmt::layer()) .init(); let app_state = models::initialize_state(); // 라우트로 애플리케이션 빌드 let app = Router::new() .route("/", get(|| async { "Hello, Modular Axum API!" })) // /users 경로 아래에 사용자 라우터 마운트 .nest("/users", routes::users::users_router()) .with_state(app_state) // Tower 서비스 (미들웨어) 추가 .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().include_headers(true)) .on_request(trace::DefaultOnRequest::new().level(tracing::Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(tracing::Level::INFO).latency_300_ms(Duration::from_millis(300))), ); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
API 실행
cargo run
을 사용하여 이 API를 실행할 수 있습니다.
car go run
그런 다음 curl
과 같은 도구를 사용하여 상호 작용할 수 있습니다.
- 모든 사용자 가져오기:
curl http://localhost:3000/users
- ID로 사용자 가져오기:
curl http://localhost:3000/users/1
- 새 사용자 POST:
curl -X POST -H "Content-Type: application/json" -d '{"name": "Charlie", "email": "charlie@example.com"}' http://localhost:3000/users
- 사용자 업데이트 PUT:
curl -X PUT -H "Content-Type: application/json" -d '{"name": "Alice Smith", "email": "alice.smith@example.com"}' http://localhost:3000/users/1
- 사용자 삭제:
curl -X DELETE http://localhost:3000/users/2
주요 기능 설명
-
모듈식 라우팅:
src/routes/users.rs
의users_router()
함수는axum::Router
를 반환합니다. 이 라우터는 사용자 관련 모든 논리를 캡슐화합니다.main.rs
에서.nest("/users", routes::users::users_router())
을 사용하여 이 하위 라우터를/users
경로 아래에 마운트합니다. 이렇게 하면 명확한 계층 구조가 생성되고main.rs
파일을 더 깔끔하게 유지할 수 있습니다.Router<AppState>
유형 시그니처는AppState
가 해당 라우터 내의 모든 라우트에 일관되게 전달되도록 합니다.
-
상태 공유:
AppState
를Arc<Mutex<Vec<User>>>
로 정의합니다.Arc
는 상태의 여러 소유자를 허용하고Mutex
는 안전한 동시 액세스를 처리합니다.- 메인
Router
의with_state(app_state)
메서드는AppState
를 애플리케이션에 주입합니다. - 핸들러 함수에서는
State(state): State<AppState>
를 추출기로 사용하여 공유 상태를 검색합니다. 이는 타입 안전하고 관용적인 Axum입니다.
-
Tower 서비스 및 미들웨어:
- 요청/응답 로깅을 추가하기 위해
tower_http::trace::TraceLayer
를 사용했습니다. 이는 TowerLayer
의 강력한 예입니다. Router
의.layer(...)
메서드는with_state
후와.layer
이전에 정의된 모든 경로에 이 미들웨어를 적용합니다.with_state
전에 적용되면 미들웨어가 상태에 액세스할 수 없습니다.- Tower 서비스를 체인으로 연결하여 인증, 속도 제한, CORS, 압축 등을 위한 복잡한 미들웨어 파이프라인을 코어 비즈니스 로직을 어지럽히지 않고 구성할 수 있습니다.
- 요청/응답 로깅을 추가하기 위해
애플리케이션 시나리오
이 모듈식 접근 방식은 다음과 같은 경우에 유용합니다.
- 대규모 API: API가 성장함에 따라 별도의 라우팅 모듈로 관심사를 분리하면
main.rs
가 모놀리식 파일이 되는 것을 방지할 수 있습니다. - 팀 협업: 다른 팀이나 개발자가 상당한 충돌 없이 별도의 API 모듈에서 작업할 수 있습니다.
- 유지 관리성: API의 한 영역에 대한 변경이 관련 없는 부분에 영향을 미칠 가능성이 적습니다.
- 테스트 용이성: 개별 라우터와 해당 핸들러를 격리하여 테스트할 수 있습니다.
결론
Rust의 Axum을 사용하여 모듈식 웹 API를 구축하면 복잡성을 관리하고 확장성 및 유지 관리성을 보장하는 강력하고 체계적인 방법을 제공합니다. Axum의 선언적 라우팅, 강력한 상태 관리 및 tower
생태계의 구성 가능한 서비스를 효과적으로 활용하여 개발자는 고성능, 타입 안전 및 쉽게 확장 가능한 백엔드 시스템을 구축할 수 있습니다. 이 접근 방식은 개발을 간소화할 뿐만 아니라 깔끔한 아키텍처를 육성하여 Rust 웹 애플리케이션을 즐겁게 구축하고 유지 관리할 수 있게 합니다.