Actix Web Data와 State 익스트랙터: 애플리케이션 상태 관리를 위한 이중 접근 방식
Daniel Hayes
Full-Stack Engineer · Leapcell

Rust에서 견고하고 확장 가능한 웹 애플리케이션을 구축하려면 종종 애플리케이션 상태를 신중하게 관리해야 합니다. 이 상태는 데이터베이스 연결, 구성 설정부터 사용자 세션 및 캐싱 계층까지 다양할 수 있습니다. Actix Web 프레임워크에서는 핸들러 내에서 이러한 상태를 주입하고 액세스하는 두 가지 주요 메커니즘이 두드러집니다. 바로 Data 및 State 익스트랙터입니다. 둘 다 상태 관리를 위한 목적을 수행하지만, 서로 다른 사용 사례를 위해 설계되었으며 다른 이점을 제공합니다. 이러한 복잡성을 이해하는 것은 효율적이고 유지 관리 가능하며 관용적인 Actix Web 애플리케이션을 작성하는 데 중요합니다. 이 글에서는 Data와 State의 구체적인 내용을 살펴보고 원칙, 구현 및 이상적인 애플리케이션 시나리오를 비교합니다.
핵심 개념
익스트랙터 자체를 자세히 살펴보기 전에 Actix Web 상태 관리에서 자주 접하는 기본 개념에 대한 명확한 이해를 확립해 보겠습니다.
- 애플리케이션 상태: 웹 애플리케이션의 전체 수명 주기 동안 여러 요청에 걸쳐 공유하거나 전역적으로 사용할 수 있어야 하는 데이터 또는 리소스를 나타냅니다. 예로는 데이터베이스 풀, Redis 클라이언트, 구성 구조 또는 사용자 지정 서비스 인스턴스가 있습니다.
 - 요청 범위 상태: 단일 들어오는 HTTP 요청 및 해당 처리에만 관련된 데이터 또는 리소스입니다. 여기에는 헤더에서 구문 분석된 사용자 인증 정보, 요청별 추적 ID 또는 요청 처리 중에 생성된 임시 데이터가 포함될 수 있습니다.
 - 익스트랙터: Actix Web에서 익스트랙터는 들어오는 HTTP 요청에서 특정 데이터를 쉽게 검색할 수 있도록 하는 메커니즘입니다. 이 데이터는 URI 매개변수 및 쿼리 문자열부터 JSON 요청 본문, 그리고 우리가 보게 될 애플리케이션 상태까지 무엇이든 될 수 있습니다. 익스트랙터는 핸들러 서명을 단순화하고 유형 안전성을 보장합니다.
 - Arc (Atomic Reference Counted): Rust에서 여러 소유자가 값을 공유할 수 있도록 하는 스마트 포인터입니다. 값을 관리하는 마지막 
Arc가 해제되면 해당 값도 해제됩니다.Arc는 비동기 웹 서버에서 일반적인 여러 스레드 간에 데이터를 불변으로 공유하는 데 중요합니다. - Mutex (Mutual Exclusion): 여러 스레드의 동시 액세스로부터 공유 데이터를 보호하는 데 사용되는 동기화 기본 요소입니다. 이를 통해 한 번에 한 스레드만 보호된 데이터에 액세스할 수 있어 데이터 손상을 방지합니다. 
Arc<Mutex<T>>는 스레드 간에 변경 가능한 상태를 공유하는 일반적인 패턴입니다. 
Data 익스트랙터: 애플리케이션 전반에 걸친 공유 상태
Actix Web의 Data 익스트랙터는 핸들러에 애플리케이션 전반에 걸친 불변 또는 안전하게 변경 가능한 공유 상태를 주입하도록 설계되었습니다. App::app_data를 사용하여 상태를 등록하면 Actix Web이 이를 Arc로 래핑합니다. 그러면 이 Arc는 해당 애플리케이션 인스턴스 내의 모든 서비스(경로)에서 사용할 수 있습니다.
원칙
Data의 핵심 원칙은 애플리케이션이 필수적이고 오래 지속되는 리소스를 효율적으로 공유할 수 있는 방법을 제공하는 것입니다. Arc로 상태를 래핑함으로써 Actix Web은 스레드 안전성을 손상시키지 않고 여러 작업자 스레드에서 상태에 안전하게 액세스할 수 있도록 합니다. 상태가 변경 가능해야 하는 경우 일반적으로 Arc와 Mutex 또는 RwLock과 같은 내부 변경 가능 기본 요소를 결합합니다.
구현
애플리케이션 전체에서 데이터베이스 연결 풀을 공유하려는 예제를 통해 이를 설명해 보겠습니다.
use actix_web::{web, App, HttpResponse, HttpServer}; use sqlx::{PgPool, postgres::PgPoolOptions}; use std::sync::Arc; // 데이터베이스 연결 풀을 보유할 간단한 구조체 정의 #[derive(Clone)] struct AppConfig { connection_string: String, // 기타 구성 필드 } async fn index(pool: web::Data<PgPool>, config: web::Data<AppConfig>) -> HttpResponse { // 데이터베이스 풀에 액세스 let _result = sqlx::query!("SELECT 1").fetch_one(pool.as_ref()).await; // 구성 액세스 println!("DB connection string: {}", config.connection_string); HttpResponse::Ok().body("Hello from Actix Web!") } #[actix_web::main] async fn main() -> std::io::Result<()> { let database_url = "postgres://user:password@localhost/database"; let pool = PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Failed to create PgPool."); let app_config = AppConfig { connection_string: database_url.to_string(), }; HttpServer::new(move || { App::new() .app_data(web::Data::new(pool.clone())) // PgPool 등록 .app_data(web::Data::new(app_config.clone())) // AppConfig 등록 .route("/", web::get().to(index)) }) .bind(("127.0.0.1", 8080))? .run() .await }
이 예제에서는 다음과 같습니다.
PgPool및AppConfig를 한 번 초기화합니다.App::app_data(web::Data::new(my_data))를 사용하여 이러한 리소스를 애플리케이션에 등록합니다.PgPool과 같은 더 큰 유형의 경우.clone()이 필요합니다.app_data가 상태가Clone이어야 한다고 예상하기 때문입니다(다른 작업자 스레드로 이동되는 경우가 많으므로).web::Data는 이를Arc로 효과적으로 래핑합니다.index핸들러에서web::Data<PgPool>및web::Data<AppConfig>를 인수로 추가하기만 하면 Actix Web이 올바른 인스턴스를 자동으로 추출하여 주입합니다.
사용 사례
- 데이터베이스 연결 풀 (
PgPool,SqlitePool,RedisPool) - 구성 설정 (
AppConfig,EnvConfig) - 공유 서비스 클라이언트 (예: S3 클라이언트, 결제 게이트웨이 클라이언트)
 - 전역 캐시 (예: 
Arc<RwLock<HashMap<K, V>>>) 
State 익스트랙터: 요청 범위의 컨텍스트 상태
State 익스트랙터(이전 Actix Web 버전의 web::ReqData이며, 이제는 일반적으로 더 일반적이거나 사용자 지정 익스트랙터를 통해 참조됨)는 현재 요청에 특정한 데이터이며 미들웨어 또는 이전 요청 처리기에 의해 요청 수명 주기 동안 추가되거나 수정될 수 있는 데이터를 주입하도록 설계되었습니다. 전체 애플리케이션에 대해 한 번 구성되는 Data와 달리 State를 사용하면 요청별로 동적으로 데이터를 연결할 수 있습니다.
원칙
State의 원칙은 애플리케이션 파이프라인을 통과하는 요청의 컨텍스트를 풍부하게 하는 것입니다. 미들웨어 또는 사용자 지정 사전 처리 단계는 해당 특정 요청과 관련된 정보를 삽입할 수 있으며, 이는 후속 핸들러 또는 다른 미들웨어에서 편리하게 액세스할 수 있습니다. 이 데이터는 일반적으로 요청 처리가 완료되면 해제됩니다.
구현
Actix Web의 web::Data는 애플리케이션 전반의 상태를 명시적으로 처리하는 반면, 요청 범위 상태는 일반적으로 HttpRequest 객체의 확장 또는 HttpRequest에서 작동하는 사용자 지정 익스트랙터를 정의하여 관리됩니다. 요청 범위 상태를 추가하는 일반적인 패턴에는 미들웨어가 포함됩니다.
사용자 ID를 토큰에서 구문 분석하여 해당 UserId를 다운스트림 핸들러에서 사용할 수 있도록 하려는 인증 미들웨어가 있다고 가정해 보겠습니다.
use actix_web::{dev::{ServiceRequest, ServiceResponse, Transform}, web, App, Error, HttpRequest, HttpResponse, HttpServer}; use futures::future::{ok, Ready, LocalBoxFuture}; use std::{rc::Rc, future::{ready, Future}}; // 요청 확장 프로그램에 저장할 간단한 UserId 구조체 #[derive(Debug, Clone)] struct UserId(u32); // 사용자 지정 미들웨어 pub struct AuthMiddleware; impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware where S: actix_web::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, S::Future: 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type InitError = (); type Transform = AuthMiddlewareService<S>; type Future = Ready<Result<Self::Transform, Self::InitError>>; fn new_transform(&self, service: S) -> Self::Future { ok(AuthMiddlewareService { service: Rc::new(service) }) } } pub struct AuthMiddlewareService<S> { service: Rc<S>, } impl<S, B> actix_web::Service<ServiceRequest> for AuthMiddlewareService<S> where S: actix_web::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, S::Future: 'static, B: 'static, { type Response = ServiceResponse<B>; type Error = Error; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready(&self, cx: &mut core::task::Context<'_>) -> core::task::Poll<Result<(), Self::Error>> { self.service.poll_ready(cx) } fn call(&self, req: ServiceRequest) -> Self::Future { let svc = self.service.clone(); Box::pin(async move { // 인증 및 사용자 ID 추출 시뮬레이션 // 실제 앱에서는 헤더 구문 분석, 토큰 확인 등 필요 let user_id = UserId(123); // 데모용, 사용자 ID 하드코딩 // UserId를 요청 확장 프로그램에 저장 // 요청 범위 상태가 추가되는 곳 req.extensions_mut().insert(user_id.clone()); let res = svc.call(req).await?; Ok(res) }) } } // web::ReqData(암시적 또는 사용자 지정 익스트랙터 사용)를 사용하여 UserId를 추출하는 핸들러 async fn user_profile(user_id: web::ReqData<UserId>) -> HttpResponse { println!("Accessed by User ID: {:?}", user_id.0); HttpResponse::Ok().body(format!("User Profile for ID: {}", user_id.0)) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .wrap(AuthMiddleware) // 인증 미들웨어 적용 .route("/profile", web::get().to(user_profile)) }) .bind(("127.0.0.1", 8080))? .run() .await }
이 예제에서는 다음과 같습니다.
- 인증을 시뮬레이션하기 위해 
AuthMiddleware가 도입되었습니다. - 미들웨어의 
call메서드 내에서 가상의 인증 후에UserId인스턴스가 생성됩니다. - 중요하게도 
req.extensions_mut().insert(user_id.clone());를 사용하여 이UserId를 요청의 로컬 확장 프로그램에 저장합니다. 이렇게 하면 특정 요청의 수명 동안UserId를 사용할 수 있습니다. user_profile핸들러는web::ReqData<UserId>를 인수로 사용합니다. Actix Web은 자동으로 요청의 확장 프로그램에서UserId유형을 검색하여 추출합니다.
사용 사례
- 인증된 사용자 세부 정보 (
UserId,SessionToken) - 사용자 지정 헤더에서 구문 분석된 요청별 매개변수
 - 요청 추적 ID (
X-Request-ID) - 다운스트림 핸들러에 전달하기 위해 미들웨어에서 생성된 임시 데이터
 
Data vs. State: 비교
| 특징 | web::Data<T> (애플리케이션 전반의 상태) | web::ReqData<T> (요청 범위 상태/확장 프로그램) | 
|---|---|---|
| 범위 | 전체 애플리케이션 인스턴스 | 단일 HTTP 요청 | 
| 수명 | 애플리케이션 종료 시까지 | 요청 처리 완료 시까지 | 
| 생성 | App::new() 설정 중에 한 번 구성 | 각 요청마다 미들웨어/익스트랙터에 의해 동적으로 추가 | 
| 변경 가능성 | 기본적으로 불변; 안전한 변경은 Arc<Mutex<T>> | 요청 범위 내에서 변경 가능 (예: HttpRequest::extensions_mut()) | 
| 기저 유형 | Arc<T> (종종) | HttpRequest::extensions에 저장 (내부적으로 Box<dyn Any>) | 
| 액세스 패턴 | App::app_data를 통해 주입 | 미들웨어 또는 사용자 지정 익스트랙터에서 req.extensions_mut().insert()를 통해 주입 | 
| 일반적인 사용 | 데이터베이스 풀, 환경 구성, 공유 클라이언트 | 인증된 사용자, 요청 ID, 구문 분석된 토큰 | 
결론
web::Data와 요청 범위 State(종종 web::ReqData를 통해 액세스됨)의 개념은 Actix Web 애플리케이션 내에서 정보를 관리하는 데 필수적입니다. web::Data는 모든 요청 및 스레드에 걸쳐 일관되고 공유되는 리소스에 대한 이동식이며, 애플리케이션 전반의 컨텍스트를 효율적으로 배포하는 방법을 제공합니다. 반대로 요청 범위 State는 요청별로 생성되는 동적 정보에 이상적이며, 인증 또는 추적 데이터와 같은 세부 정보로 요청 컨텍스트를 풍부하게 하는 데 적합합니다. 이 두 가지 메커니즘 중에서 신중하게 선택함으로써 개발자는 전역 관심사를 요청별 컨텍스트와 효과적으로 분리하는 잘 구성되고 성능이 뛰어나며 유지 관리 가능한 Actix Web 서비스를 만들 수 있습니다.

