Axum와 Actix Web에서의 의존성 주입 전략
Ethan Miller
Product Engineer · Leapcell

소개
Rust, 특히 Axum 및 Actix Web과 같은 프레임워크를 사용하여 견고하고 유지보수 가능한 웹 서비스를 구축하는 것은 종종 복잡한 애플리케이션 상태와 외부 종속성을 관리하는 것을 포함합니다. 애플리케이션이 성장함에 따라 필요한 모든 리소스를 여러 함수 계층을 통해 명시적으로 전달하는 것은 번거로울 수 있으며, 상투적인 코드와 긴밀하게 결합된 구성 요소로 이어질 수 있습니다. 여기서 의존성 주입(DI)이 빛을 발합니다. 의존성 주입은 느슨한 결합을 촉진하고 일반적인 DI 기술과 코드 예제를 통해 테스트 용이성, 유지보수 용이성 및 모듈성을 향상시킵니다. 웹 개발의 맥락에서 이는 데이터베이스 연결, API 클라이언트, 구성 설정 또는 핸들러가 의존하는 다른 서비스의 쉬운 관리를 의미합니다. 이 글에서는 Axum 및 Actix Web 내에서 의존성 주입을 구현하는 다양한 접근 방식을 살펴보고, 이러한 패턴을 활용하여 보다 유연하고 확장 가능한 Rust 웹 애플리케이션을 구축하는 방법을 설명하는 실용적인 예제를 제공합니다.
의존성 주입 핵심 개념
구현 세부 사항을 살펴보기 전에 의존성 주입 이해의 중심이 되는 몇 가지 기본 개념을 명확히 해 보겠습니다.
- 의존성(Dependency): 소프트웨어 개발에서 의존성은 다른 개체 또는 모듈이 올바르게 작동하기 위해 필요한 개체 또는 모듈을 의미합니다. 예를 들어, 사용자 서비스는 사용자 데이터를 저장하고 검색하기 위해 데이터베이스 연결 풀에 의존할 수 있습니다.
- 제어 역전(Inversion of Control, IoC): IoC는 개체 생성 및 수명 주기의 제어가 애플리케이션 코드에서 프레임워크 또는 컨테이너로 이전되는 설계 원칙입니다. 의존성 주입은 IoC의 특정 형태입니다. 개체가 의존성을 생성하는 대신, 의존성은 외부 엔터티에 의해 개체에 제공됩니다.
- 의존성 주입(Dependency Injection, DI): DI는 개체가 의존성을 직접 생성하는 대신, 이들 개체를 받는 기술입니다. 이는 종속 개체가 의존성이 어떻게 구성되는지 알 필요가 없으므로 느슨한 결합을 촉진합니다.
- 서비스 로케이터(Service Locator): 개체가 명시적으로 "로케이터"에 의존성을 요청하는 대안 패턴입니다. 직접적인 결합을 줄일 수 있지만, 로케이터 자체에 대한 "전역 의존성"을 도입하여 테스트를 더 어렵게 만들고 의존성을 숨길 수 있습니다. DI는 일반적으로 서비스 로케이터보다 선호됩니다.
- 컨테이너/주입기(Container/Injector): 개체의 인스턴스화, 구성 및 수명 주기를 관리하고 의존성을 주입하는 역할을 하는 구성 요소입니다. Rust의 강력한 유형 시스템과 소유권 모델로 인해 명시적인 DI 컨테이너가 덜 일반적이지만, "컨테이너"의 개념은 종종 공유 애플리케이션 상태 또는 사용자 지정 구조체로 나타납니다.
Axum 및 Actix Web에서 의존성 주입 구현
Rust 웹 프레임워크에서 의존성 주입의 주요 방법은 일반적으로 핸들러 간에 상태를 공유하거나 추출기 패턴을 활용하는 데 중점을 둡니다. 실용적인 코드 예제를 통해 이러한 전략을 탐색할 것입니다.
Axum: 상태 및 추출기
tower::Service
를 기반으로 하는 Axum은 공유 상태를 관리하는 우아한 방법을 제공하며, 이는 의존성 주입에 자연스럽게 적합합니다.
전략 1: 공유 애플리케이션 상태
Axum에서 가장 일반적인 접근 방식은 모든 애플리케이션의 공유 리소스를 단일 AppState
구조체로 래핑하고, 이를 Extension
을 통해 핸들러에서 액세스할 수 있도록 하는 것입니다.
데이터베이스와 상호 작용하는 UserService
와 이메일을 보내는 Mailer
서비스를 가지고 있다고 가정해 보겠습니다.
// src/main.rs use axum::* use std::{net::SocketAddr, sync::Arc}; use tokio::sync::Mutex; use uuid::Uuid; // --- 의존성 --- // 모의 데이터베이스 클라이언트 #[derive(Debug, Clone)] struct DbClient { // 실제 앱에서는 sqlx::PgPool과 같은 연결 풀이 될 것입니다. } impl DbClient { async fn new() -> Self { println!("DbClient 초기화 중..."); tokio::time::sleep(std::time::Duration::from_millis(50)).await; // 비동기 초기화 시뮬레이션 DbClient {} } async fn fetch_user(&self, id: Uuid) -> Option<User> { println!("DB에서 사용자 {} 가져오는 중...", id); tokio::time::sleep(std::time::Duration::from_millis(100)).await; if id == Uuid::parse_str("c9f0b1a0-3e3e-4d4d-8a8a-0a0a0a0a0a0a").unwrap() { Some(User { id, name: "Alice".to_string(), email: "alice@example.com".to_string(), }) } else { None } } async fn create_user(&self, user: User) { println!("DB에 사용자 생성 중: {:?}", user); tokio::time::sleep(std::time::Duration::from_millis(50)).await; } } // 모의 메일러 서비스 #[derive(Debug, Clone)] struct Mailer { // 실제 앱에서는 SendGrid, Mailchimp 등과 같은 클라이언트가 될 수 있습니다. } impl Mailer { async fn new() -> Self { println!("Mailer 초기화 중..."); Mailer {} } async fn send_welcome_email(&self, email: String, name: String) { println!("{} ({})에게 환영 이메일 보내는 중", name, email); tokio::time::sleep(std::time::Duration::from_millis(70)).await; // 이메일 보내는 로직 } } // --- 애플리케이션 서비스 (비즈니스 로직) --- #[derive(Debug, Clone)] struct UserService { db_client: DbClient, } impl UserService { fn new(db_client: DbClient) -> Self { UserService { db_client } } async fn get_user_by_id(&self, id: Uuid) -> Option<User> { self.db_client.fetch_user(id).await } async fn register_user(&self, new_user: NewUser) -> User { let user = User { id: Uuid::new_v4(), name: new_user.name, email: new_user.email, }; self.db_client.create_user(user.clone()).await; user } } // --- DTO 및 모델 --- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct User { id: Uuid, name: String, email: String, } #[derive(Debug, Clone, serde::Deserialize)] struct NewUser { name: String, email: String, } // --- 애플리케이션 상태 --- // 이 구조체는 공유 서비스/종속성을 보유합니다. #[derive(Clone)] struct AppState { user_service: UserService, mailer: Mailer, } // 복잡한 서비스가 작업 간에 공유되고 잠재적으로 변형될 수 있는 경우(여기서는 주로 `Clone`이지만) Arc를 사용한 더 강력한 방법입니다. // 서비스 자체가 내부적으로 다른 요청 또는 핸들러 간에 공유 및 수정되어야 하는 가변 상태를 보유하는 경우 Arc<Mutex<T>> 또는 Arc<RwLock<T>>로 래핑해야 합니다. // 여기 `UserService` 및 `Mailer`의 경우, 자체 내부 `DbClient` 및 `Mailer`가 `Clone`이므로 `AppState`를 복제하는 것으로 충분합니다. // 시연을 위해 DbClient 자체가 복잡하고 복제하기 어렵고 UserService가 Arc를 보유한다고 가정해 봅시다. #[derive(Clone)] struct AppStateArc { user_service: Arc<UserService>, // UserService를 복제하기 어렵거나 수정될 수 있는 경우 mailer: Arc<Mailer>, } // --- 핸들러 --- async fn get_user_handler( // State 추출기 사용 ( `AppState`를 복제해야 함) State(app_state): State<AppState>, axum::extract::Path(user_id): axum::extract::Path<Uuid>, ) -> Result<Json<User>, axum::http::StatusCode> { println!("ID {:?}에 대한 get_user_handler 처리 중", user_id); match app_state.user_service.get_user_by_id(user_id).await { Some(user) => Ok(Json(user)), None => Err(axum::http::StatusCode::NOT_FOUND), } } async fn register_user_handler( // State 추출기 사용 ( `AppState`를 복제해야 함) State(app_state): State<AppState>, Json(new_user): Json<NewUser>, ) -> Json<User> { println!("사용자 {:?}에 대한 register_user_handler 처리 중", new_user.name); let registered_user = app_state.user_service.register_user(new_user).await; let _ = app_state .mailer .send_welcome_email(registered_user.email.clone(), registered_user.name.clone()) .await; Json(registered_user) } async fn get_user_handler_arc( // Arc<AppStateArc>를 사용한 State 추출기 // AppState 자체에 복제할 항목이 많지 않은 경우 유용합니다. State(app_state): State<Arc<AppStateArc>>, axum::extract::Path(user_id): axum::extract::Path<Uuid>, ) -> Result<Json<User>, axum::http::StatusCode> { println!("ID {:?}에 대한 get_user_handler_arc 처리 중", user_id); match app_state.user_service.get_user_by_id(user_id).await { Some(user) => Ok(Json(user)), None => Err(axum::http::StatusCode::NOT_FOUND), } } #[tokio::main] async fn main() { // 의존성 초기화 let db_client = DbClient::new().await; let mailer = Mailer::new().await; // 애플리케이션 서비스 초기화 let user_service = UserService::new(db_client.clone()); // db_client는 Clone임 // 애플리케이션 상태 생성 let app_state = AppState { user_service: user_service.clone(), // user_service는 Clone임 mailer: mailer.clone(), // mailer는 Clone임 }; // Arc 예제: let app_state_arc = Arc::new(AppStateArc { user_service: Arc::new(user_service), mailer: Arc::new(mailer), }); // Axum 애플리케이션 빌드 let app = Router::new() .route("/users/:id", get(get_user_handler)) .route("/users", post(register_user_handler)) // Arc 예제 라우트 .route("/arc/users/:id", get(get_user_handler_arc)) .with_state(app_state) // AppState를 라우터에 첨부 .with_state(app_state_arc); // 필요한 경우 여러 상태를 첨부할 수 있습니다. let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("Axum 서버가 {}에서 수신 대기 중", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }
이 예제에서:
DbClient
와Mailer
를 낮은 수준의 의존성으로 정의합니다.UserService
는DbClient
에 의존하는 더 높은 수준의 서비스입니다.AppState
는UserService
와Mailer
를 수집하는 구조체입니다. 중요하게도AppState
(및 포함된 서비스)는 각 들어오는 요청에 대해 Axum이 상태를 복제하기 때문에Clone
을 구현해야 합니다. 서비스 자체가Clone
이 아닌 유형을 보유하거나 스레드 안전한 변경이 필요한 경우Arc<Mutex<T>>
또는Arc<RwLock<T>>
로 래핑해야 합니다.main
에서 모든 의존성 및 서비스를 인스턴스화합니다.- 그런 다음
.with_state(app_state)
를 사용하여AppState
를 Axum 라우터에 첨부합니다. - 핸들러는
State<AppState>
추출기를 사용하여 이러한 의존성에 액세스합니다. Axum은 자동으로 올바른AppState
인스턴스를 제공합니다.
이 패턴은 Axum에서 매우 관용적이며 명확한 수명 관리와 명시적인 의존성 선언을 제공합니다.
전략 2: 특정 의존성에 대한 사용자 지정 추출기
State
는 전체 애플리케이션 상태에 훌륭하지만, 때로는 특정 의존성 또는 의존성 조합을, 잠재적으로 추가 로직과 함께 주입하고 싶을 수 있습니다. 이를 위해 사용자 지정 추출기를 만들 수 있습니다.
// 이전 Axum 예제에서 계속, 사용자 서비스 사용자 지정 추출기 정의 use async_trait::async_trait; use axum::* use std::net::SocketAddr; use tokio::sync::Mutex; use uuid::Uuid; // ... (위와 동일한 DbClient, Mailer, User, NewUser, UserService, AppState, AppStateArc 정의) ... // 사용자 지정 UserService 추출기 struct InjectedUserService(UserService); #[async_trait] impl<S> FromRequestParts<S> for InjectedUserService where S: Send + Sync, AppState: FromRequestParts<S>, // AppState를 추출할 수 있는지 확인 { type Rejection = <AppState as FromRequestParts<S>>::Rejection; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { let app_state = AppState::from_request_parts(parts, state).await?; Ok(InjectedUserService(app_state.user_service.clone())) } } // 사용자 지정 추출기를 사용하는 핸들러 async fn get_user_with_custom_extractor( InjectedUserService(user_service): InjectedUserService, // 사용자 지정 서비스를 추출 axum::extract::Path(user_id): axum::extract::Path<Uuid>, ) -> Result<Json<User>, axum::http::StatusCode> { println!( "ID {:?}에 대한 get_user_with_custom_extractor 처리 중", user_id ); match user_service.get_user_by_id(user_id).await { Some(user) => Ok(Json(user)), None => Err(axum::http::StatusCode::NOT_FOUND), } } // main()에 이 라우트 추가 // .route("/custom_extracted_users/:id", get(get_user_with_custom_extractor))
이 사용자 지정 추출기는 AppState
를 페치하여 UserService
를 추출합니다. 다음과 같은 경우 유용할 수 있습니다.
- 종속성의 소스를 추상화하려는 경우(예: 상태에서 오거나 요청 헤더에서 올 수 있음).
- 핸들러가 종속성을 받기 전에 유효성 검사 또는 권한 부여 로직을 수행하려는 경우.
- 전체
AppState
가 아닌 특정 서비스의 보기 또는 구성 요소를 주입하려는 경우.
Actix Web: 애플리케이션 데이터 및 추출기 특성
Actix Web도 주로 애플리케이션 데이터(web::Data
) 및 사용자 지정 추출기를 통해 강력한 의존성 주입 메커니즘을 제공합니다.
전략 1: 공유 애플리케이션 데이터 (web::Data
)
Axum의 State
와 유사하게 Actix Web은 요청 간에 애플리케이션 수준 상태 및 서비스를 공유하기 위해 web::Data
를 사용합니다. 데이터는 내부적으로 Arc
로 래핑되어 스레드 안전한 액세스를 보장합니다.
이전 예제를 Actix Web에 적용해 보겠습니다.
// src/main.rs use actix_web::* use std::sync::Arc; use uuid::Uuid; // --- 의존성 --- // (DbClient, Mailer, User, NewUser 정의는 위와 동일 - 복사만 하면 됩니다) // 서비스 자체가 Arc로 래핑되지 않은 경우 또는 서비스가 Arc인 경우 DbClient 및 Mailer가 Clone이어야 합니다. // Actix Web의 Data의 경우 내부 유형은 Send + Sync + 'static이어야 합니다. #[derive(Debug, Clone)] struct DbClient {} impl DbClient { async fn new() -> Self { println!("DbClient 초기화 중..."); tokio::time::sleep(std::time::Duration::from_millis(50)).await; DbClient {} } async fn fetch_user(&self, id: Uuid) -> Option<User> { println!("DB에서 사용자 {} 가져오는 중...", id); tokio::time::sleep(std::time::Duration::from_millis(100)).await; if id == Uuid::parse_str("c9f0b1a0-3e3e-4d4d-8a8a-0a0a0a0a0a0a").unwrap() { Some(User { id, name: "Bob".to_string(), email: "bob@example.com".to_string(), }) } else { None } } async fn create_user(&self, user: User) { println!("DB에 사용자 생성 중: {:?}", user); tokio::time::sleep(std::time::Duration::from_millis(50)).await; } } #[derive(Debug, Clone)] struct Mailer {} impl Mailer { async fn new() -> Self { println!("Mailer 초기화 중..."); Mailer {} } async fn send_welcome_email(&self, email: String, name: String) { println!("{} ({})에게 환영 이메일 보내는 중", name, email); tokio::time::sleep(std::time::Duration::from_millis(70)).await; } } // --- 애플리케이션 서비스 (비즈니스 로직) --- #[derive(Debug)] // UserService에서 Clone을 제거하여 AppData에서 Arc 사용을 보여줍니다. struct UserService { db_client: DbClient, } impl UserService { fn new(db_client: DbClient) -> Self { UserService { db_client } } async fn get_user_by_id(&self, id: Uuid) -> Option<User> { self.db_client.fetch_user(id).await } async fn register_user(&self, new_user: NewUser) -> User { let user = User { id: Uuid::new_v4(), name: new_user.name, email: new_user.email, }; self.db_client.create_user(user.clone()).await; user } } // --- DTO 및 모델 --- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct User { id: Uuid, name: String, email: String, } #[derive(Debug, Clone, serde::Deserialize)] struct NewUser { name: String, email: String, } // --- 핸들러 --- #[get("/users/{user_id}")] async fn get_user_handler_actix( user_id: web::Path<Uuid>, // web::Data를 통해 UserService 주입, 내부적으로 Arc를 보유합니다. user_service: web::Data<UserService>, ) -> impl Responder { println!("ID {:?}에 대한 get_user_handler_actix 처리 중", user_id); match user_service.get_user_by_id(user_id.into_inner()).await { Some(user) => HttpResponse::Ok().json(user) , None => HttpResponse::NotFound().body("User Not Found"), } } #[post("/users")] async fn register_user_handler_actix( new_user_data: web::Json<NewUser>, // UserService 및 Mailer 모두 주입 user_service: web::Data<UserService>, mailer: web::Data<Mailer>, ) -> impl Responder { println!( "사용자 {:?}에 대한 register_user_handler_actix 처리 중", new_user_data.name ); let registered_user = user_service .register_user(new_user_data.into_inner()) .await; let _ = mailer .send_welcome_email(registered_user.email.clone(), registered_user.name.clone()) .await; HttpResponse::Created().json(registered_user) } #[actix_web::main] async fn main() -> std::io::Result<()> { // 의존성 초기화 let db_client = DbClient::new().await; let mailer = Mailer::new().await; // 애플리케이션 서비스 초기화 // UserService 자체는 Clone이 아니므로 web::Data를 위해 Arc로 래핑합니다. let user_service = Arc::new(UserService::new(db_client)); HttpServer::new(move || { App::new() // 애플리케이션 데이터로 의존성 첨부 // web::Data는 유형을 Arc<T>로 래핑합니다. .app_data(web::Data::new(user_service.clone())) // UserService는 Send + Sync + 'static이어야 함 .app_data(web::Data::new(mailer.clone())) // Mailer는 Send + Sync + 'static이어야 함 .service(get_user_handler_actix) .service(register_user_handler_actix) }) .bind(("127.0.0.1", 8080))? // "127.0.0.1" 및 8080을 "127.0.0.1" 및 8080으로 변경, "127.0.0.1" 및 8080을 "127.0.0.1" 및 8080으로 변경 .run() .await }
Actix Web 예제에서는:
- Axum과 유사하게
DbClient
,Mailer
,UserService
를 정의합니다. main
에서UserService
와Mailer
를 인스턴스화합니다.web::Data
는 내부적으로 해당 콘텐츠를Arc
로 래핑하므로UserService
및Mailer
자체는 한 번 래핑되어 있으면 엄격하게Clone
일 필요는 없습니다. 그러나Send + Sync + 'static
이어야 합니다..app_data(web::Data::new(...))
를 사용하여App
인스턴스에 이러한 서비스를 첨부합니다.HttpServer::new
가FnMut
클로저를 기대하고app_data
가 값을 소비하기 때문에user_service
에 대해.clone()
을 사용합니다.Arc
복제는 저렴합니다.- 핸들러는
web::Data<T>
유형의 인수로 단순히 선언하여 이러한 의존성을 수신합니다. 여기서T
는 의존성의 유형입니다. Actix Web의 추출기 시스템은 자동으로 올바른 데이터를 확인하고 주입합니다.
이 web::Data
접근 방식은 Actix Web에서 종속성을 관리하는 가장 일반적이고 강력한 방법입니다.
전략 2: 사용자 지정 추출기 구현
Actix Web은 FromRequest
특성을 사용하여 사용자 지정 추출기를 지원하며, Axum의 사용자 지정 추출기와 유사하게 의존성 주입에 대한 세분화된 제어를 제공합니다.
// 이전 Actix Web 예제에서 계속, 사용자 서비스 사용자 지정 추출기 정의 use actix_web::FromRequest; // 특성 가져오기 use futures::future::{ready, Ready}; use actix_web::dev::Payload; use actix_web::Error; // ... (DbClient, Mailer, User, NewUser, UserService 정의는 위와 동일) ... // 사용자 지정 UserService 추출기 pub struct MyInjectedUserService(pub Arc<UserService>); // 서비스에 대한 Arc 보유 impl FromRequest for MyInjectedUserService { type Error = Error; type Future = Ready<Result<Self, Self::Error>>; fn from_request(req: &actix_web::HttpRequest, _: &mut Payload) -> Self::Future { // 애플리케이션 데이터에 액세스할 수 있습니다. if let Some(user_service_arc) = req.app_data::<web::Data<UserService>>() { ready(Ok(MyInjectedUserService(user_service_arc.into_inner()))) } else { // 이 경우는 app_data가 올바르게 구성된 경우에 이상적으로 발생하지 않습니다. ready(Err(actix_web::error::ErrorInternalServerError( "UserService를 애플리케이션 데이터에서 찾을 수 없습니다.", ))) } } } // 사용자 지정 추출기를 사용하는 핸들러 #[get("/custom_extracted_users/{user_id}")] async fn get_user_with_custom_extractor_actix( user_id: web::Path<Uuid>, InjectedUserService(user_service): MyInjectedUserService, // 사용자 지정 추출기 사용 ) -> impl Responder { println!( "ID {:?}에 대한 get_user_with_custom_extractor_actix 처리 중", user_id ); match user_service.get_user_by_id(user_id.into_inner()).await { Some(user) => HttpResponse::Ok().json(user) , None => HttpResponse::NotFound().body("User Not Found"), } } // main()의 App 구성에 이 라우트 추가: // .service(get_user_with_custom_extractor_actix)
이 사용자 지정 추출기는 FromRequest
를 구현하는 방법을 보여주며, 요청의 애플리케이션 상태(또는 요청의 다른 부분)에서 특정 데이터를 가져와 핸들러에서 액세스할 수 있도록 합니다. 이는 다음과 같은 경우에 매우 유용합니다.
- 요청에 기반한 계산된 값 주입(예: 토큰에서 현재 사용자).
- 복잡한 의존성 해결 로직 캡슐화.
- 핸들러가 실행되기 전에 액세스 제어 또는 사전 조건 적용.
의존성 주입의 이점
- 테스트 용이성: 의존성을 주입함으로써 실제 구현을 테스트 객체로 쉽게 교체할 수 있어 단위 및 통합 테스트가 훨씬 간단하고 안정적입니다.
- 모듈성 및 느슨한 결합: 구성 요소는 의존성의 내부 구현 세부 정보에 덜 의존합니다. 그들은 필요한 것의 인터페이스(특성)만 "알고" 있어 관심사의 분리를 촉진합니다.
- 유지 보수 용이성: 종속성의 구현 변경은 인터페이스가 동일하게 유지되는 한, 이를 사용하는 모든 구성 요소의 변경이 필요하지 않습니다. 이렇게 하면 버그가 발생할 가능성이 줄어듭니다.
- 유연성 및 재사용성: 서비스는 다양한 환경(개발, 스테이징, 프로덕션)에 맞게 다르게 구성되거나 수정 없이 다른 컨텍스트에서 재사용될 수 있습니다.
- 명확성: 구성 요소가 생성자 또는 핸들러 인수를 보면 어떤 의존성이 필요한지 즉시 명확해집니다.
결론
의존성 주입은 Rust, 특히 Axum 및 Actix Web과 같은 웹 프레임워크에서 확장 가능하고 테스트 가능하며 유지 보수 가능한 애플리케이션을 구축하는 데 중요한 설계 패턴입니다. 공유 애플리케이션 상태(Axum의 State
, Actix Web의 web::Data
) 및 사용자 지정 추출기를 활용함으로써 개발자는 외부 리소스와 서비스 의존성을 우아하게 관리하여 더 깔끔한 코드베이스와 더 명시적인 제어 흐름을 만들 수 있습니다. 이러한 의존성 주입 전략을 마스터하면 견고하고 적응 가능한 Rust 웹 서비스를 제작하는 능력이 크게 향상될 것입니다.