Actix Web 및 Axum 애플리케이션에서의 견고한 상태 관리
Min-jun Kim
Dev Intern · Leapcell

Rust에서 견고하고 확장 가능한 웹 애플리케이션을 구축하려면 종종 공유 리소스를 효율적으로 관리해야 합니다. 데이터베이스 연결 풀, 애플리케이션 전체 구성 설정 또는 캐시든 이러한 구성 요소는 여러 동시 요청에 걸쳐 액세스 가능하고 안전하게 관리되어야 합니다. Actix Web 및 Axum과 같은 Rust 비동기 웹 프레임워크의 세계에서 이는 고유한 과제를 제시합니다. 경쟁 조건이나 성능 병목 현상을 도입하지 않고 이러한 중요한 데이터 조각을 어떻게 공유할 수 있을까요? 이 기사에서는 Actix Web 및 Axum 애플리케이션에서 공유 상태를 관리하는 다양한 전략을 살펴보고 기본 원칙, 실제 구현 및 적합한 사용 사례를 강조합니다. 이러한 기술을 숙달함으로써 개발자는 더 유지 관리하기 쉽고 성능이 뛰어나고 안정적인 Rust 웹 서비스를 구축할 수 있습니다.
공유 상태를 위한 핵심 개념
전략에 대해 자세히 알아보기 전에 Rust의 비동기 컨텍스트에서 공유 상태 관리를 이해하는 데 중요한 몇 가지 핵심 개념을 정의해 보겠습니다.
- 공유 상태: 애플리케이션의 여러 부분에서, 종종 동시에 액세스하고 잠재적으로 수정해야 하는 모든 데이터입니다. 예로는 데이터베이스 연결 풀, 애플리케이션 구성, 캐싱 계층 또는 지표 카운터가 있습니다.
- 동시성: 시스템이 여러 작업이나 요청을 동시에 처리하는 능력입니다. 웹 애플리케이션에서는 여러 사용자 요청을 동시에 서비스하는 것을 의미합니다.
- 스레드 안전성: Rust의 유형 시스템, 특히
Send
및Sync
트레잇이 여기서 중요한 역할을 하는 데이터 손상이나 예기치 않은 동작으로 이어지지 않고 여러 스레드에서 공유 데이터에 동시에 액세스하고 수정할 수 있음을 보장합니다. - 비동기 컨텍스트: I/O(네트워크 요청 또는 데이터베이스 쿼리 등)가 완료될 때까지 기다리는 동안 현재 스레드를 차단하지 않는 작업입니다. 모든 최신 Rust 웹 프레임워크는 비동기 런타임을 기반으로 구축됩니다.
Arc
(Atomic Reference Counted): 여러 소유자가 스레드 간에 값을 공유할 수 있도록 하는 스마트 포인터입니다. 마지막Arc
가 범위를 벗어나면 포함된 값이 삭제됩니다. 공유 소유권을 제공합니다.Mutex
(Mutual Exclusion Lock): 한 번에 하나의 스레드만 공유 리소스에 액세스할 수 있도록 보장하는 동기화 기본 요소입니다. 잠금을 통해 경쟁 조건을 방지하여 데이터 무결성을 보장합니다.RwLock
(Read-Write Lock): 여러 판독자가 리소스에 동시에 액세스할 수 있지만 한 번에 하나의 작성자만 액세스할 수 있도록 하는 동기화 기본 요소입니다. 판독이 쓰기보다 훨씬 빈번할 때Mutex
보다 더 높은 동시성을 제공할 수 있습니다.OnceCell
/Lazy
: 값을 정확히 한 번, 종종 지연되게 초기화한 다음 불변 액세스를 제공하는 유틸리티입니다. 시작 시 한 번 설정되는 전역 구성에 유용합니다.
공유 상태 관리 전략
Actix Web과 Axum 모두 Rust의 동시성 기본 요소를 크게 활용하여 공유 상태를 관리하는 관용적인 방법을 제공합니다.
1. Arc<Mutex<T>>
/ Arc<RwLock<T>>
패턴
이는 Rust에서 가변 공유 상태를 관리하는 가장 기본적이고 널리 사용되는 패턴입니다.
원칙:
공유 데이터 T
를 Mutex<T>
(또는 RwLock<T>
)로 래핑하여 변경을 위한 독점 액세스를 보장한 다음 Arc<Mutex<T>>
(또는 Arc<RwLock<T>>
)로 래핑하여 여러 소유권을 허용하고 스레드 간에 안전하게 공유합니다. 데이터에 액세스해야 할 때 Arc
를 복제한 다음 Mutex
(또는 RwLock
)를 잠가 가변 참조를 가져옵니다.
Arc<Mutex<T>>
: 쓰기가 빈번하거나 항상 독점 액세스가 필요한 경우 사용합니다.Arc<RwLock<T>>
: 읽기가 쓰기보다 훨씬 빈번한 경우 사용합니다. 동시 판독을 허용하기 때문입니다.
구현 (Axum 예제):
use std::{ sync::{Arc, Mutex}, collections::HashMap, }; use axum::{ extract::{State, Path}, routing::{post, get}, Json, Router, }; use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, Serialize, Deserialize)] struct User { id: u32, name: String, } // 공유 애플리케이션 상태 struct AppState { user_db: Arc<Mutex<HashMap<u32, User>>>, config_value: String, } #[tokio::main] async fn main() { let shared_state = Arc::new(AppState { user_db: Arc::new(Mutex::new(HashMap::new())), config_value: "Application Config".to_string(), }); let app = Router::new() .route("/users", post(create_user)) .route("/users/:id", get(get_user)) .with_state(shared_state); // 상태를 라우터에 주입 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(); } async fn create_user( State(state): State<Arc<AppState>>, // 공유 상태 추출 Json(payload): Json<User>, ) -> Json<User> { let mut db = state.user_db.lock().unwrap(); // 잠금 획득 db.insert(payload.id, payload.clone()); Json(payload) } async fn get_user( State(state): State<Arc<AppState>>, // 공유 상태 추출 Path(id): Path<u32>, ) -> Option<Json<User>> { let db = state.user_db.lock().unwrap(); // 잠금 획득 db.get(&id).cloned().map(Json) }
구현 (Actix Web 예제):
use actix_web::{ web, App, HttpServer, Responder, HttpResponse }; use std::{ sync::{Arc, Mutex}, collections::HashMap, }; use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, Serialize, Deserialize)] struct User { id: u32, name: String, } // 공유 애플리케이션 상태 struct AppState { user_db: Arc<Mutex<HashMap<u32, User>>>, config_value: String, } async fn create_user_actix(state: web::Data<AppState>, user: web::Json<User>) -> impl Responder { let mut db = state.user_db.lock().unwrap(); // 잠금 획득 db.insert(user.id, user.clone()); HttpResponse::Ok().json(user.0) } async fn get_user_actix(state: web::Data<AppState>, path: web::Path<u32>) -> impl Responder { let id = path.into_inner(); let db = state.user_db.lock().unwrap(); // 잠금 획득 match db.get(&id) { Some(user) => HttpResponse::Ok().json(user), None => HttpResponse::NotFound().finish(), } } #[actix_web::main] async fn main() -> std::io::Result<()> { let shared_state = web::Data::new(AppState { // Actix를 위해 상태를 web::Data로 래핑 user_db: Arc::new(Mutex::new(HashMap::new())), config_value: "Application Config".to_string(), }); HttpServer::new(move || { // move 클로저에서 AppState를 복제 App::new() .app_data(shared_state.clone()) // 앱과 데이터 공유 .service(web::resource("/users").route(web::post().to(create_user_actix))) .service(web::resource("/users/{id}").route(web::get().to(get_user_actix))) }) .bind(("127.0.0.1", 8080))? // 바인드 주소에 따옴표 추가 .run() .await }
애플리케이션:
이 패턴은 데이터베이스 연결 풀(예: sqlx::PgPool
), 애플리케이션 전체 캐시, 전역 카운터 또는 여러 요청 핸들러에서 공유하고 잠재적으로 수정해야 하는 기타 데이터를 관리하는 데 이상적입니다.
2. Arc<T>
를 사용한 불변 불변 상태
공유 상태가 초기화 후 실제로 불변한 경우(예: 시작 시 로드된 구성), Mutex
또는 RwLock
이 필요하지 않습니다.
원칙:
불변 데이터 T
를 Arc<T>
로 직접 래핑합니다. 데이터는 수정할 수 없으므로 경합 조건에 대해 걱정할 필요가 없으며 여러 스레드에서 잠금 오버헤드 없이 자유롭게 동시에 읽을 수 있습니다.
구현 (Axum 예제):
use std::sync::Arc; use axum::{ extract::State, routing::get, Router }; use serde::Serialize; #[derive(Debug, Clone, Serialize)] struct AppConfig { api_key: String, database_url: String, max_connections: u32, } // 공유 불변 애플리케이션 상태 struct AppState { config: Arc<AppConfig>, } #[tokio::main] async fn main() { let config = Arc::new(AppConfig { api_key: "my_secret_key".to_string(), database_url: "postgres://user:pass@host:port/db".to_string(), max_connections: 10, }); let shared_state = Arc::new(AppState { config }); let app = Router::new() .route("/config", get(get_app_config)) .with_state(shared_state); let listener = tokio::net::TcpListener::bind("127.0.0.1:3001") .await .unwrap(); println!("Listening on http://127.0.0.1:3001"); axum::serve(listener, app).await.unwrap(); } async fn get_app_config(State(state): State<Arc<AppState>>) -> axum::Json<AppConfig> { // 구성이 불변하므로 잠금이 필요하지 않습니다. axum::Json(state.config.as_ref().clone()) }
애플리케이션: 이것은 애플리케이션 구성, 시작 시 한 번 로드된 읽기 전용 데이터(예: 정적 매핑, 작은 조회 테이블) 또는 애플리케이션 수명 동안 변경되지 않는 것으로 보장되는 모든 데이터에 완벽합니다.
3. tokio::sync
기본 요소 사용
더 세분화되거나 비동기별 동시성 요구 사항의 경우 tokio::sync
는 잠금 및 채널의 비동기 버전을 제공합니다.
tokio::sync::Mutex
: 잠금을 기다리는 동안 작업이 실행자를 차단하지 않고 양보할 수 있도록 하는 비동기Mutex
입니다.tokio::sync::RwLock
: 유사한 동작을 가진 비동기RwLock
입니다.tokio::sync::Semaphore
: 동시 작업 수를 제한합니다.
원칙:
이러한 비동기 기본 요소는 표준 라이브러리 해당 요소와 유사하게 작동하지만 async/.await
컨텍스트에 완벽하게 맞습니다. 잠금이 경합 중일 때 런타임이 다른 작업을 예약할 수 있도록 하여 I/O 바운드 작업의 전반적인 처리량을 향상시킵니다.
구현 (Actix Web with tokio::sync::Mutex
):
use actix_web::{ web, App, HttpServer, Responder, HttpResponse }; use std::collections::HashMap; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use std::sync::Arc; // 공유 소유권을 위해 여전히 Arc가 필요합니다. #[derive(Debug, Clone, Serialize, Deserialize)] struct Item { id: u32, name: String, } // 비동기 Mutex를 사용한 공유 상태 struct AppState { item_cache: Arc<Mutex<HashMap<u32, Item>>> } async fn add_item_async(state: web::Data<AppState>, item: web::Json<Item>) -> impl Responder { let mut cache = state.item_cache.lock().await; // 잠금 대기 cache.insert(item.id, item.clone()); HttpResponse::Ok().json(item.0) } #[actix_web::main] async fn main() -> std::io::Result<()> { let shared_state = web::Data::new(AppState { item_cache: Arc::new(Mutex::new(HashMap::new())), // HashMap을 비동기 Mutex로 래핑 }); HttpServer::new(move || { // move 클로저에서 AppState를 복제 App::new() .app_data(shared_state.clone()) .service(web::resource("/items").route(web::post().to(add_item_async))) }) .bind(("127.0.0.1", 8081))? // 바인드 주소에 따옴표 추가 .run() .await }
애플리케이션:
공유 리소스 액세스가 async
작업을 포함하거나 많은 동시 비동기 작업에서 경합 지점이 될 수 있는 경우. sqlx
와 같은 라이브러리의 데이터베이스 연결 풀은 일반적으로 연결을 기다리는 미래를 반환하므로 tokio::sync
패턴이 자연스럽게 호환됩니다.
4. 프레임워크별 상태 관리 (Actix Web web::Data
/ Axum State
)
Actix Web과 Axum 모두 효율성을 위해 내부적으로 Arc
또는 참조 카운팅을 활용하는 방식으로 핸들러에 공유 상태를 주입하기 위한 자체 추상화를 제공합니다.
원칙:
프레임워크는 기본 Arc
복제 및 공유 로직을 처리하여 핸들러 내에서 상태에 액세스하는 것을 인체 공학적으로 만듭니다.
- Actix Web
web::Data<T>
: 애플리케이션 상태를Arc
로 래핑합니다.app_data()
와 함께web::Data
를 등록하면 Actix Web이 각 워커 스레드에 대해Arc
를 복제하고 각 요청 핸들러는 참조 카운트 포인터를 받습니다. - Axum
State<T>
: 애플리케이션 상태를 위한 Axum의 추출기입니다.with_state()
를 사용할 때 내부적으로Arc
로 래핑하므로 상태 개체가Clone
및Send + Sync + 'static
을 구현해야 합니다.
구현: 위의 Actix Web 및 Axum 예제 모두에서 이 사항을 이미 시연했습니다.
- Actix Web:
web::Data::new(AppState { ... })
및app_data(shared_state.clone())
전달. 핸들러는state: web::Data<AppState>
를 받습니다. - Axum:
with_state(shared_state)
여기서shared_state
는Arc<AppState>
입니다. 핸들러는State(state): State<Arc<AppState>>
를 받습니다.
애플리케이션: 이러한 각 프레임워크에서 애플리케이션 수준 상태를 핸들러에 전달하는 기본적이고 권장되는 방법입니다. 프레임워크의 아키텍처와 완벽하게 통합됩니다.
올바른 전략 선택
- 불변 구성:
Arc<YourConfigStruct>
를 사용하십시오. 읽기 전용 데이터에 대해 가장 간단하고 성능이 뛰어납니다. - 가변, 범용:
std::sync
의Arc<Mutex<T>>
또는Arc<RwLock<T>>
. 다양한 가변 공유 리소스에 대해 강력합니다. 읽기가 쓰기보다 훨씬 빈번한 경우RwLock
을 사용하고, 그렇지 않으면Mutex
가 더 간단하고 충분히 성능이 뛰어난 경우가 많습니다. - 가변, 비동기 인식:
Arc<tokio::sync::Mutex<T>>
또는Arc<tokio::sync::RwLock<T>>
. 애플리케이션이async/.await
에 크게 의존하고 잠금 경합이 차단을 유발할 수 있는 경우 이를 선호합니다. - 데이터베이스 연결 풀:
sqlx
와 같은 라이브러리는 연결을 내부적으로 관리하고Send + Sync + 'static
인 자체Pool
유형(예:sqlx::PgPool
)을 제공합니다. 일반적으로 이러한 풀을Arc
로 래핑(예:Arc<sqlx::PgPool>
)한 다음web::Data
또는State
를 통해 전달합니다.
// sqlx PgPool 예제 use sqlx::PgPool; use std::sync::Arc; struct AppState { db_pool: Arc<PgPool>, // PgPool을 둘러싼 Arc // 기타 공유 상태 } // ... 그런 다음 web::Data<AppState> 또는 State<Arc<AppState>> 사용
결론
효율적이고 정확한 웹 애플리케이션을 구축하는 데 공유 상태를 관리하는 것은 초석입니다. Rust는 강력한 유형 시스템과 동시성 기본 요소를 통해 이를 안전하게 달성할 수 있는 강력한 도구를 제공합니다. Arc
를 공유 소유권에 사용하고, Mutex
또는 RwLock
을 제어된 가변성(std
및 tokio::sync
버전 모두)에 사용하고, Actix Web의 web::Data
또는 Axum의 State
와 같은 프레임워크별 추상화를 활용함으로써 개발자는 매우 동시적이고 강력한 웹 서비스를 구성할 수 있습니다. 핵심은 공유 데이터의 특성(불변인지, 자주 읽는지, 자주 쓰는지)을 이해하고 성능을 희생하지 않고 스레드 안전성을 보장하기 위해 적절한 동기화 기본 요소를 선택하는 것입니다.