Rust 웹 애플리케이션에서 상태 공유하기
James Reed
Infrastructure Engineer · Leapcell

소개
견고하고 확장 가능한 웹 서비스를 구축하려면 종종 여러 스레드 또는 비동기 작업 전반에 걸쳐 공유 리소스를 관리해야 합니다. Rust의 엄격한 소유권 규칙과 스레드 안전성 강조는 이러한 도전을 특히 흥미롭고 신중한 형태로 만듭니다. Arc<Mutex<T>> 패턴은 Rust에서 동시 프로그래밍의 기본적인 초석이지만, actix-web과 같은 웹 프레임워크는 자체적인 편리한 추상화를 도입합니다. 이 글에서는 다중 스레드 웹 애플리케이션 컨텍스트에서 상태 공유의 미묘한 차이를 탐구하고, 특히 Arc<Mutex<T>>의 직접적인 사용과 actix-web의 web::Data<T>를 비교하고 실제 예제로 적용을 설명합니다. 이러한 패턴을 이해하는 것은 Rust에서 고성능의 스레드 안전 웹 서비스를 구축하는 모든 사람에게 매우 중요합니다.
동시 상태 관리를 위한 핵심 개념
비교를 자세히 살펴보기 전에 Rust에서 동시 상태 관리를 뒷받침하는 핵심 구성 요소를 간략하게 정의해 보겠습니다.
std::sync::Arc<T>(Atomic Reference Counted): 이 스마트 포인터는 값의 공유 소유권을 제공합니다. 여러Arc인스턴스가 동일한 데이터를 가리킬 수 있으며, 데이터는 이를 가리키는 마지막Arc가 삭제될 때만 삭제됩니다. 결정적으로Arc는 공유 데이터를 스레드 간에 안전하게 전달할 수 있도록 합니다. 종종 내부 가변성 유형과 함께 사용됩니다.std::sync::Mutex<T>(Mutual Exclusion): 공유 데이터를 동시 액세스로부터 보호하기 위한 원시 타입입니다.Mutex내부의 데이터에 액세스하려면 스레드는 먼저 잠금을 획득해야 합니다. 잠금을 다른 스레드가 이미 보유하고 있는 경우 현재 스레드는 잠금이 해제될 때까지 차단됩니다. 이를 통해 한 번에 하나의 스레드만 데이터를 수정할 수 있어 경쟁 조건을 방지할 수 있습니다.actix_web::web::Data<T>: 이것은actix-web의Arc<T>에 대한 편리한 래퍼입니다. 이를 통해actix-web애플리케이션에 공유 애플리케이션 상태를 등록할 수 있으므로 핸들러에서 추출기로 자동으로 사용할 수 있습니다. 본질적으로Data<T>는 웹 애플리케이션에 맞춰진 편의 기능이 추가된Arc<T>입니다.- 핸들러 및 미들웨어:
actix-web에서 핸들러는 HTTP 요청에 응답하는 함수이며, 미들웨어 함수는 핸들러 전이나 후에 요청과 응답을 처리할 수 있습니다. 둘 다 종종 공유 애플리케이션 상태에 액세스해야 합니다.
리소스 공유: Arc<Mutex<T>> vs. actix_web::web::Data<T>
핵심적으로 actix_web::web::Data<T>와 수동으로 관리되는 Arc<Mutex<T>>는 동일한 기본 목적을 수행합니다. 즉, actix-web 애플리케이션 내에서 공유되고 스레드 안전한 상태 액세스를 제공합니다. 주요 차이점은 통합 및 편의성에 있습니다.
직접적인 Arc<Mutex<T>> 접근 방식
Arc<Mutex<T>>를 직접 관리할 때 공유 데이터 구조를 명시적으로 래핑하고 Arc의 복사본을 필요한 곳에 전달합니다. 이것은 최대의 유연성을 제공하지만 특히 서버 설정 시 약간 더 장황할 수 있습니다.
다른 요청이 증가하거나 감소시킬 수 있는 간단한 카운터를 고려해 보세요.
use std::sync::{Arc, Mutex}; use actix_web::{web, App, HttpServer, Responder, HttpResponse}; // 우리의 공유 애플리케이션 상태 struct AppState { counter: Mutex<i32>, } async fn increment_counter(data: web::Data<Arc<AppState>>) -> impl Responder { let mut counter = data.counter.lock().unwrap(); *counter += 1; HttpResponse::Ok().body(format!("Counter: {}", *counter)) } async fn get_counter(data: web::Data<Arc<AppState>>) -> impl Responder { let counter = data.counter.lock().unwrap(); HttpResponse::Ok().body(format!("Counter: {}", *counter)) } #[actix_web::main] async fn main() -> std::io::Result<()> { let app_state = Arc::new(AppState { counter: Mutex::new(0), }); HttpServer::new(move || { App::new() .app_data(web::Data::new(Arc::clone(&app_state))) // Arc<AppState> 등록 .route("/increment", web::post().to(increment_counter)) .route("/get", web::get().to(get_counter)) }) .bind(("127.0.0.1", 8080))? .run() .await }
이 예제에서 AppState는 Mutex<i32>를 포함합니다. Arc<AppState>를 생성하고 app_data를 호출할 때 명시적으로 복사합니다. 그런 다음 핸들러는 추출기로 web::Data<Arc<AppState>>를 예상합니다. 이것은 완벽하게 작동하며 Arc<Mutex<T>>의 원시 성능을 보여줍니다.
actix_web::web::Data<T> 접근 방식
actix_web::web::Data<T>는 Arc<T>(또는 심지어 Arc<Mutex<T>> 직접)를 래핑하고 추출기로 제공하여 패턴을 단순화합니다. web::Data::new(my_state)를 사용할 때 actix-web은 내부적으로 Arc 생성 및 복사를 처리하여 설정을 더 깔끔하게 만듭니다.
web::Data<T>를 보다 관용적인 방식으로 사용하여 이전 예제를 리팩토링해 보겠습니다.
use std::sync::Mutex; use actix_web::{web, App, HttpServer, Responder, HttpResponse}; // 공유 애플리케이션 상태 - 명시적인 Arc는 필요하지 않습니다. Data가 처리합니다. struct AppState { counter: Mutex<i32>, } async fn increment_counter_data(data: web::Data<AppState>) -> impl Responder { let mut counter = data.counter.lock().unwrap(); *counter += 1; HttpResponse::Ok().body(format!("Counter: {}", *counter)) } async fn get_counter_data(data: web::Data<AppState>) -> impl Responder { let counter = data.counter.lock().unwrap(); HttpResponse::Ok().body(format!("Counter: {}", *counter)) } #[actix_web::main] async fn main() -> std::io::Result<()> { // 상태를 직접 web::Data에 전달할 수 있습니다. 내부적으로 Arc로 래핑합니다. let app_state = web::Data::new(AppState { counter: Mutex::new(0), }); HttpServer::new(move || { App::new() .app_data(app_state.clone()) // Data<AppState> 복사 .route("/increment", web::post().to(increment_counter_data)) .route("/get", web::get().to(get_counter_data)) }) .bind(("127.0.0.1", 8080))? .run() .await }
주요 차이점을 주목하세요:
main에서AppState는web::Data::new()에 의해 직접 래핑되며, 이는 내부적으로Arc를 암묵적으로 사용합니다.app_data(app_state.clone())줄은 이제 원시Arc가 아닌web::Data인스턴스 자체를 복사합니다.- 핸들러는 단순히
web::Data<AppState>를 취하므로 액세스가 간단합니다.
어떤 것을 선택해야 할 때
- 일반적인 애플리케이션 상태에는
web::Data<T>사용: 이것은 모든 핸들러와 공유되어야 하는 구성, 데이터베이스 연결 풀 또는 기타 전역 상태를 주입하는 권장되고 가장 편리한 방법입니다.Arc상용구 코드를 추상화합니다. - 사용자 정의 오브젝트 계층 또는 복잡한 시나리오 내의 직접적인
Arc<Mutex<T>>: 직접actix-web핸들러가 아니지만 여전히 상태를 공유하고 Rust의 동시성 모델을 준수해야 하는 내부 구성 요소 또는 서비스가 있는 경우 해당 구조 내에서Arc<Mutex<T>>를 명시적으로 관리하면 더 많은 제어를 얻을 수 있습니다. 예를 들어, 동일한 상태를 수정하는 백그라운드 작업자 스레드를 구축하는 경우 직접Arc<Mutex<T>>복사본을 전달합니다.web::Data<T>는 내부적으로Arc<T>이지만 주로actix-web핸들러를 위한 추출기로 설계되었습니다.
애플리케이션 시나리오
두 패턴 모두 일반적인 웹 서비스 작업에 필수적입니다.
- 데이터베이스 연결 풀: 모든 핸들러에 데이터베이스 액세스를 제공하기 위해
Arc<PgPool>또는Arc<SqlitePool>이 종종web::Data로 래핑됩니다. - 구성 설정: 전역 애플리케이션 구성은 한 번 로드하여
web::Data<AppConfig>를 통해 사용할 수 있습니다. - 캐싱: 요청 전반에 걸쳐 공유되는 인메모리 캐시는 일반적으로
web::Data를 통해 노출되는Arc<Mutex<HashMap<K, V>>>또는 이와 유사한 것으로 구성됩니다. - 속도 제한: 요청 수, 사용자/IP 주소별로 추적하는 전역 속도 제한기 상태는 확실히
Arc<Mutex<T>>를 포함합니다.
결론
다중 스레드 Rust 웹 애플리케이션에서 리소스를 공유하려면 스레드 안전성 및 소유권 고려 사항을 신중하게 검토해야 합니다. Arc<Mutex<T>> 패턴은 이를 위한 기본 요소를 제공하여 가변 데이터에 대한 안전하고 동시적인 액세스를 보장합니다. actix-web의 web::Data<T>는 이 기반 위에 구축되어 애플리케이션별 상태를 핸들러로 주입하는 편리하고 관용적인 방법을 제공합니다. 두 가지 모두 궁극적으로 Arc를 활용하여 유사한 결과를 달성하지만, web::Data<T>는 일반적인 웹 애플리케이션 상태 관리를 위한 개발자 경험을 단순화하여 actix-web 서비스에 공유 리소스를 원활하게 통합하는 데 선호되는 선택입니다.

