Rust의 Axum, Actix Web, Diesel을 사용하여 견고하고 성능이 뛰어난 RESTful API 구축
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
빠르게 발전하는 웹 개발 환경에서 고성능, 안정성, 유지보수성이 뛰어난 API에 대한 수요는 끊임없이 증가하고 있습니다. 현대 웹 애플리케이션, 모바일 서비스 또는 마이크로서비스 아키텍처를 위한 백엔드를 구축하든, 기술 스택의 선택은 프로젝트의 성공에 상당한 영향을 미칩니다. Rust는 성능, 메모리 안전성, 동시성에 대한 타협 없는 집중으로 백엔드 개발에 있어 매력적인 선택지로 부상했습니다. 이 글에서는 Rust에서 가장 인기 있고 자주 논의되는 두 가지 웹 프레임워크인 Axum과 Actix Web을 강력한 ORM(객체 관계형 매퍼)인 Diesel과 함께 활용하여, 놀랍도록 빠를 뿐만 아니라 탁월한 타입 안전성과 유지보수성을 자랑하는 RESTful API를 구축하는 방법을 자세히 알아봅니다.
이 조합의 실질적인 중요성은 매우 큽니다. 성능이 중요한 애플리케이션은 Rust의 거의 제로에 가까운 오버헤드와 컴파일 타임 보장을 통해 런타임 오류를 최소화하고 처리량을 최대화합니다. Diesel에 의해 강제되는 타입 안전성은 데이터 불일치가 프로덕션에서 폭발하는 대신 컴파일 타임에 포착되므로 데이터베이스 상호 작용과 관련된 버그를 줄여줍니다. 또한, 이러한 라이브러리를 둘러싼 강력한 생태계는 개발자에게 복잡하고 확장 가능한 시스템을 자신 있게 구축하는 데 필요한 도구를 제공합니다. 각 기술의 핵심 개념을 살펴보고, 각 기술의 강점을 이해한 다음, 이들이 어떻게 우아하게 상호 작용하여 응집력 있고 강력한 백엔드 솔루션을 형성하는지 보여줄 것입니다.
핵심 구성 요소 이해
구현에 들어가기 전에 사용할 주요 기술인 Axum, Actix Web, Diesel에 대한 명확한 이해를 확립해 보겠습니다.
Axum
Axum은 Tokio 비동기 런타임과 Hyper HTTP 라이브러리 위에 구축된 웹 애플리케이션 프레임워크입니다. Tokio 커뮤니티에서 개발했으며, 라우팅 및 미들웨어에 Rust의 강력한 타입 시스템을 옵션으로 사용하고 "매크로 없는" 디자인을 채택했습니다. Axum의 핵심 철학은 구성 가능성과 표준 Rust 트레잇 준수를 중심으로 하며, 이는 Rustaceans에게 매우 관용적으로 느껴지게 합니다. 주요 기능은 다음과 같습니다.
- 타입 안전한 라우팅: 라우트는 요청 데이터를 구문 분석하고 타입 안전한 방식으로 종속성을 주입하는 추출기(extractor)를 수락하는 함수로 정의됩니다. 이를 통해 잘못된 데이터 유형으로 인한 런타임 오류 가능성을 줄일 수 있습니다.
- 최소주의 및 구성 가능: Axum은 더 작고 구성 가능한 단위로 애플리케이션을 구축하도록 권장합니다. 예를 들어, 미들웨어는 개별 라우트 또는 라우트 그룹에 쉽게 적용할 수 있습니다.
- Tokio 생태계 통합: Tokio 생태계의 일부로서 Axum은 Tokio의 강력한 비동기 런타임, 뛰어난 동시성 기본 요소 및 풍부한 도구를 활용합니다.
Actix Web
Actix Web은 Rust를 위한 강력하고 실용적이며 매우 빠른 웹 프레임워크입니다. 높은 동시성 애플리케이션을 뛰어난 성능 특성으로 가능하게 하는 액터 기반 동시성 모델로 유명합니다. 액터 모델은 복잡하게 들릴 수 있지만, Actix Web은 사용을 단순화하는 고수준 API를 제공합니다. 그 특징은 다음과 같습니다.
- 극도의 성능: Actix Web은 다양한 벤치마크에서 가장 빠른 웹 프레임워크 중 하나로 꾸준히 순위에 오릅니다. 이는 효율적인 액터 모델과 최적화된 내부 아키텍처 덕분입니다.
- 포괄적인 기능: Actix Web은 라우팅, 미들웨어, 세션, 웹소켓 및 강력한 테스트 유틸리티를 포함한 포괄적인 기능을 기본적으로 제공합니다.
- 액터 기반 동시성(추상화): 액터 모델을 기반으로 구축되었지만, 개발자는 주로 HTTP 요청을 동시적으로 처리하는 데 중점을 두고 기본 복잡성을 대부분 추상화하는
web::Service
및web::Data
구성 요소와 상호 작용합니다.
Diesel
Diesel은 Rust를 위한 강력하고 타입 안전한 ORM 및 쿼리 빌더입니다. 관계형 데이터베이스에 안전하고 효율적인 방식으로 액세스하는 것을 목표로 합니다. Diesel의 주요 강점은 데이터베이스 스키마 및 쿼리 유효성에 대한 컴파일 타임 보장에서 비롯됩니다.
- 컴파일 타임 타입 안전성: Diesel은 데이터베이스 스키마에 직접 해당하는 Rust 타입을 생성합니다. 이는 존재하지 않는 열을 쿼리하거나 잘못된 유형의 값을 삽입하려고 하면 코드가 컴파일되지 않아 많은 일반적인 런타임 오류를 방지합니다.
- 강력한 쿼리 빌더: Diesel은 타입 안전하고 관용적인 Rust 방식으로 복잡한 SQL 쿼리를 구성하기 위한 표현력이 풍부한 도메인별 언어(DSL)를 제공합니다.
- 마이그레이션 시스템: Diesel은 시간이 지남에 따라 데이터베이스 스키마 진화를 관리하는 데 도움이 되는 강력한 마이그레이션 시스템을 포함합니다.
- 다중 데이터베이스 지원: Diesel은 PostgreSQL, MySQL 및 SQLite를 지원합니다.
이들을 결합하는 이유?
이 조합의 아름다움은 각 구성 요소의 강점을 활용하는 데 있습니다. Axum과 Actix Web 모두 HTTP 요청 및 라우팅 처리에 뛰어나며, Axum은 추출기를 통한 보다 명시적인 타입 안전성을 제공하고 Actix Web은 순수한 성능과 기능이 풍부한 생태계를 제공합니다. 반면 Diesel은 데이터베이스와의 타입 안전한 상호 작용의 중요한 계층을 제공하여 성능을 유지하면서도 일반 SQL을 추상화합니다. 이러한 관심사 분리는 웹 로직과 데이터 액세스 로직이 분리되고 둘 다 Rust의 강력한 타입 시스템으로 뒷받침되는 유지보수 가능한 코드로 이어집니다.
기본 RESTful API 구축: 사용자 및 게시물
사용자와 그들의 게시물을 관리하기 위한 간단한 RESTful API를 구축하는 방법을 예시로 들어보겠습니다. 사용자 및 게시물에 대한 기본 CRUD 작업을 구현합니다. 데이터베이스 스키마를 정의하고, Diesel ORM을 설정한 다음, Axum 및 Actix Web과 통합하여 시작하겠습니다.
Diesel을 사용한 데이터베이스 설정
먼저 데이터베이스(예: PostgreSQL)를 설정해야 합니다. 마이그레이션을 관리하기 위해 Diesel CLI를 사용합니다.
1. Diesel CLI 설치:
cargo install diesel_cli --no-default-features --features postgres
(다른 데이터베이스를 사용하는 경우 postgres
대신 mysql
또는 sqlite
로 바꾸세요).
2. Diesel 초기화:
.env
파일에 데이터베이스 URL을 설정합니다.
DATABASE_URL=postgres://user:password@localhost/your_database_name
그런 다음 프로젝트에서 Diesel을 초기화합니다.
diesel setup
이것은 migrations
디렉토리를 생성합니다.
3. 마이그레이션 생성:
users
및 posts
테이블을 정의합니다.
diesel migration generate create_users
생성된 up.sql
파일을 편집합니다.
-- migrations/timestamp_create_users/up.sql CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE );
diesel migration generate create_posts
생성된 up.sql
파일을 편집합니다.
-- migrations/timestamp_create_posts/up.sql CREATE TABLE posts ( id SERIAL PRIMARYKEY, user_id INTEGER NOT NULL REFERENCES users(id), title VARCHAR(255) NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT FALSE );
4. 마이그레이션 실행:
diesel migration run
5. Diesel 스키마 및 모델 생성:
Cargo.toml
에 diesel
및 dotenvy
를 추가합니다.
[dependencies] diesel = { version = "2.1.0", features = ["postgres", "r2d2", "chrono"] } # 나중에 Axum/Actix Web 및 serde, anyhow 등도 추가
diesel print-schema
를 실행하고 출력을 src/schema.rs
에 복사하거나 diesel infer-schema > src/schema.rs
를 사용합니다. 이것은 테이블의 Rust 표현을 생성합니다.
// src/schema.rs // 이 파일은 Diesel CLI에 의해 생성됩니다. diesel::table! { posts (id) { id -> Int4, user_id -> Int4, title -> Varchar, body -> Text, published -> Bool, } } diesel::table! { users (id) { id -> Int4, username -> Varchar, email -> Varchar, } } diesel::joinable!(posts -> users (user_id)); diesel::allow_tables_to_appear_in_same_query!( posts, users, );
다음으로 src/models.rs
에서 Rust 모델 및 DTO를 정의합니다.
// src/models.rs use diesel::prelude::*; use serde::{Deserialize, Serialize}; use crate::schema::{users, posts}; #[derive(Queryable, Selectable, Debug, Serialize)] #[diesel(table_name = users)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct User { pub id: i32, pub username: String, pub email: String, } #[derive(Insertable, Deserialize)] #[diesel(table_name = users)] pub struct NewUser { pub username: String, pub email: String, } #[derive(Queryable, Selectable, Debug, Serialize)] #[diesel(belongs_to(User))] #[diesel(table_name = posts)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Post { pub id: i32, pub user_id: i32, pub title: String, pub body: String, pub published: bool, } #[derive(Insertable, Deserialize)] #[diesel(table_name = posts)] pub struct NewPost { pub user_id: i32, pub title: String, pub body: String, pub published: Option<bool>, // Optional, defaults to false in DB } #[derive(Deserialize, Serialize)] pub struct UpdatePost { pub title: Option<String>, pub body: Option<String>, pub published: Option<bool>, }
데이터베이스 연결 풀링
성능을 위해 r2d2
를 사용하여 연결 풀링을 수행합니다.
// src/db_config.rs use diesel::pg::PgConnection; use diesel::r2d2::{ConnectionManager, Pool}; use std::env; pub type PgPool = Pool<ConnectionManager<PgConnection>>; pub fn establish_connection_pool() -> PgPool { dotenvy::dotenv().ok(); let database_url = env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); let manager = ConnectionManager::<PgConnection>::new(database_url); Pool::builder() .max_size(10) // Maximum number of connections in the pool .build(manager) .expect("Failed to create pool.") }
Axum을 사용한 RESTful API 구현
이제 Axum을 사용하여 API 엔드포인트를 구축해 보겠습니다.
1. Axum용 Cargo.toml
:
[dependencies] axum = { version = "0.7.5", features = ["macros"] } tokio = { version = "1.37.0", features = ["full"] } serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" diesel = { version = "2.1.0", features = ["postgres", "r2d2", "chrono"] } r2d2 = "0.8.10" anyhow = "1.0.83" dotenvy = "0.15.7"
2. src/handlers_axum.rs
의 Axum API 핸들러:
// src/handlers_axum.rs use axum::{ extract::{Path, State}, http::StatusCode, response::{IntoResponse, Json}, routing::{get, post, patch, delete}, Router, }; diesel::prelude::*; diesel::r2d2::{ConnectionManager, Pool}; use serde_json::json; use crate::models::{User, NewUser, Post, NewPost, UpdatePost}; use crate::schema::{users, posts}; use crate::db_config::PgPool; pub type DbConnection = diesel::r2d2::PooledConnection<ConnectionManager<PgConnection>>; // 풀에서 연결을 가져오는 헬퍼 함수 async fn get_conn(pool: &PgPool) -> Result<DbConnection, (StatusCode, String)> { pool.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to get DB connection: {}", e)) }) } // 사용자 핸들러 pub async fn create_user( State(pool): State<PgPool>, Json(new_user): Json<NewUser>, ) -> Result<Json<User>, (StatusCode, String)> { let mut conn = get_conn(&pool).await?; let user = diesel::insert_into(users::table) .values(&new_user) .get_result::<User>(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create user: {}", e)))?; Ok(Json(user)) } pub async fn get_all_users( State(pool): State<PgPool>, ) -> Result<Json<Vec<User>>, (StatusCode, String)> { let mut conn = get_conn(&pool).await?; let users_list = users::table .load::<User>(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch users: {}", e)))?; Ok(Json(users_list)) } pub async fn get_user_by_id( State(pool): State<PgPool>, Path(user_id): Path<i32>, ) -> Result<Json<User>, (StatusCode, String)> { let mut conn = get_conn(&pool).await?; let user = users::table .find(user_id) .first::<User>(&mut conn) .map_err(|e| match e { diesel::result::Error::NotFound => (StatusCode::NOT_FOUND, "User not found".to_string()), _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to get user: {}", e)), })?; Ok(Json(user)) } // 게시물 핸들러 pub async fn create_post( State(pool): State<PgPool>, Json(new_post): Json<NewPost>, ) -> Result<Json<Post>, (StatusCode, String)> { let mut conn = get_conn(&pool).await?; let post = diesel::insert_into(posts::table) .values(&new_post) .get_result::<Post>(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create post: {}", e)))?; Ok(Json(post)) } pub async fn get_posts_by_user( State(pool): State<PgPool>, Path(user_id): Path<i32>, ) -> Result<Json<Vec<Post>>, (StatusCode, String)> { let mut conn = get_conn(&pool).await?; let user_posts = Post::belonging_to(&users::table.find(user_id).first::<User>(&mut conn) .map_err(|e| match e { diesel::result::Error::NotFound => (StatusCode::NOT_FOUND, "User not found for posts".to_string()), _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch user for posts: {}", e)), })?) .load::<Post>(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch posts: {}", e)))?; Ok(Json(user_posts)) } pub async fn update_post( State(pool): State<PgPool>, Path(post_id): Path<i32>, Json(update_data): Json<UpdatePost>, ) -> Result<Json<Post>, (StatusCode, String)> { let mut conn = get_conn(&pool).await?; let updated_post: Post = diesel::update(posts::table.find(post_id)) .set(( update_data.title.map(|t| posts::title.eq(t)), update_data.body.map(|b| posts::body.eq(b)), update_data.published.map(|p| posts::published.eq(p)), )) .get_result::<Post>(&mut conn) .map_err(|e| match e { diesel::result::Error::NotFound => (StatusCode::NOT_FOUND, "Post not found".to_string()), _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to update post: {}", e)), })?; Ok(Json(updated_post)) } pub async fn delete_post( State(pool): State<PgPool>, Path(post_id): Path<i32>, ) -> Result<StatusCode, (StatusCode, String)> { let mut conn = get_conn(&pool).await?; let num_deleted = diesel::delete(posts::table.find(post_id)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to delete post: {}", e)))?; if num_deleted == 0 { Err((StatusCode::NOT_FOUND, "Post not found".to_string())) } else { Ok(StatusCode::NO_CONTENT) } }
3. src/main.rs
의 메인 함수 (Axum 버전):
// src/main.rs (Axum 버전) mod schema; mod models; mod db_config; mod handlers_axum; use axum::Router; use axum::routing::get; use crate::db_config::establish_connection_pool; use crate::handlers_axum::{ create_user, get_all_users, get_user_by_id, create_post, get_posts_by_user, update_post, delete_post, }; #[tokio::main] async fn main() { let pool = establish_connection_pool(); let app = Router::new() .route("/users", get(get_all_users).post(create_user)) .route("/users/:user_id", get(get_user_by_id)) .route("/users/:user_id/posts", get(get_posts_by_user)) .route("/posts", post(create_post)) .route("/posts/:post_id", patch(update_post).delete(delete_post)) .with_state(pool); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("Axum server listening on http://0.0.0.0:3000"); axum::serve(listener, app).await.unwrap(); }
Actix Web을 사용한 RESTful API 구현
이제 Actix Web을 사용하여 동일한 API 엔드포인트를 구축해 보겠습니다.
1. Actix Web용 Cargo.toml
:
[dependencies] actix-web = "4.9.0" actix-rt = "2.10.0" # `main` 매크로용 serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" diesel = { version = "2.1.0", features = ["postgres", "r2d2", "chrono"] } r2d2 = "0.8.10" anyhow = "1.0.83" dotenvy = "0.15.7"
2. src/handlers_actix.rs
의 Actix Web API 핸들러:
// src/handlers_actix.rs use actix_web::{web, HttpResponse, Responder}; diesel::prelude::*; diesel::r2d2::{ConnectionManager, Pool}; use serde_json::json; use crate::models::{User, NewUser, Post, NewPost, UpdatePost}; use crate::schema::{users, posts}; use crate::db_config::PgPool; pub type DbConnection = diesel::r2d2::PooledConnection<ConnectionManager<PgConnection>>; // 풀에서 연결을 가져오는 헬퍼 함수 async fn get_conn_actix(pool: web::Data<PgPool>) -> Result<DbConnection, HttpResponse> { web::block(move || pool.get()) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to get DB connection: {}", e)))? .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to get DB connection: {}", e))) } // 사용자 핸들러 pub async fn create_user_actix( pool: web::Data<PgPool>, new_user: web::Json<NewUser>, ) -> impl Responder { let mut conn = match get_conn_actix(pool).await { Ok(c) => c, Err(e) => return e, }; let user = web::block(move || { diesel::insert_into(users::table) .values(&new_user.into_inner()) .get_result::<User>(&mut conn) }) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to create user: {}", e)))? .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to create user: {}", e)))?; HttpResponse::Ok().json(user) } pub async fn get_all_users_actix( pool: web::Data<PgPool>, ) -> impl Responder { let mut conn = match get_conn_actix(pool).await { Ok(c) => c, Err(e) => return e, }; let users_list = web::block(move || { users::table .load::<User>(&mut conn) }) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to fetch users: {}", e)))? .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to fetch users: {}", e)))?; HttpResponse::Ok().json(users_list) } pub async fn get_user_by_id_actix( pool: web::Data<PgPool>, path: web::Path<i32>, ) -> impl Responder { let user_id = path.into_inner(); let mut conn = match get_conn_actix(pool).await { Ok(c) => c, Err(e) => return e, }; let user = web::block(move || { users::table .find(user_id) .first::<User>(&mut conn) }) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to get user: {}", e)))? .map_err(|e| match e { diesel::result::Error::NotFound => HttpResponse::NotFound().body("User not found"), _ => HttpResponse::InternalServerError().body(format!("Failed to get user: {}", e)), })?; HttpResponse::Ok().json(user) } // 게시물 핸들러 pub async fn create_post_actix( pool: web::Data<PgPool>, new_post: web::Json<NewPost>, ) -> impl Responder { let mut conn = match get_conn_actix(pool).await { Ok(c) => c, Err(e) => return e, }; let post = web::block(move || { diesel::insert_into(posts::table) .values(&new_post.into_inner()) .get_result::<Post>(&mut conn) }) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to create post: {}", e)))? .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to create post: {}", e)))?; HttpResponse::Ok().json(post) } pub async fn get_posts_by_user_actix( pool: web::Data<PgPool>, path: web::Path<i32>, ) -> impl Responder { let user_id = path.into_inner(); let mut conn = match get_conn_actix(pool).await { Ok(c) => c, Err(e) => return e, }; let user_posts = web::block(move || { let user_found = users::table.find(user_id).first::<User>(&mut conn)?; Post::belonging_to(&user_found) .load::<Post>(&mut conn) }) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to fetch posts: {}", e)))? .map_err(|e| match e { diesel::result::Error::NotFound => HttpResponse::NotFound().body("User not found for posts"), _ => HttpResponse::InternalServerError().body(format!("Failed to fetch posts: {}", e)), })?; HttpResponse::Ok().json(user_posts) } pub async fn update_post_actix( pool: web::Data<PgPool>, path: web::Path<i32>, update_data: web::Json<UpdatePost>, ) -> impl Responder { let post_id = path.into_inner(); let mut conn = match get_conn_actix(pool).await { Ok(c) => c, Err(e) => return e, }; let updated_post: Post = web::block(move || { diesel::update(posts::table.find(post_id)) .set(( update_data.title.map(|t| posts::title.eq(t)), update_data.body.map(|b| posts::body.eq(b)), update_data.published.map(|p| posts::published.eq(p)), )) .get_result::<Post>(&mut conn) }) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to update post: {}", e)))? .map_err(|e| match e { diesel::result::Error::NotFound => HttpResponse::NotFound().body("Post not found"), _ => HttpResponse::InternalServerError().body(format!("Failed to update post: {}", e)), })?; HttpResponse::Ok().json(updated_post) } pub async fn delete_post_actix( pool: web::Data<PgPool>, path: web::Path<i32>, ) -> impl Responder { let post_id = path.into_inner(); let mut conn = match get_conn_actix(pool).await { Ok(c) => c, Err(e) => return e, }; let num_deleted = web::block(move || { diesel::delete(posts::table.find(post_id)) .execute(&mut conn) }) .await .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to delete post: {}", e)))? .map_err(|e| HttpResponse::InternalServerError().body(format!("Failed to delete post: {}", e)))?; if num_deleted == 0 { HttpResponse::NotFound().body("Post not found") } else { HttpResponse::NoContent().finish() } }
3. src/main.rs
의 메인 함수 (Actix Web 버전):
// src/main.rs (Actix Web 버전) mod schema; mod models; mod db_config; mod handlers_actix; use actix_web::{web, App, HttpServer}; use crate::db_config::establish_connection_pool; use crate::handlers_actix::{ create_user_actix, get_all_users_actix, get_user_by_id_actix, create_post_actix, get_posts_by_user_actix, update_post_actix, delete_post_actix, }; #[actix_web::main] async fn main() -> std::io::Result<()> { let pool = establish_connection_pool(); let pool_data = web::Data::new(pool); // 풀을 web::Data로 래핑 println!("Actix Web server listening on http://0.0.0.0:3000"); HttpServer::new(move || { App::new() .app_data(pool_data.clone()) // 공유 데이터를 라우트에 전달 .service( web::resource("/users") .route(web::get().to(get_all_users_actix)) .route(web::post().to(create_user_actix)) ) .service(web::resource("/users/{user_id}").route(web::get().to(get_user_by_id_actix))) .service(web::resource("/users/{user_id}/posts").route(web::get().to(get_posts_by_user_actix))) .service(web::resource("/posts").route(web::post().to(create_post_actix))) .service( web::resource("/posts/{post_id}") .route(web::patch().to(update_post_actix)) .route(web::delete().to(delete_post_actix)) ) }) .bind(("0.0.0.0", 3000))? .run() .await }
참고: Axum과 Actix Web 간에 전환하려면 하나의 main
함수를 주석 처리하고 다른 하나를 주석 해제하고 Cargo.toml
종속성을 조정해야 합니다.
애플리케이션 시나리오 및 이점
이 예제는 블로깅 플랫폼 또는 콘텐츠 관리 시스템을 위한 일반적인 RESTful API 백엔드를 시연합니다. 관찰된 주요 이점은 다음과 같습니다.
- 성능: Axum과 Actix Web 모두 Tokio의 비동기 런타임을 기반으로 하여 동시 HTTP 요청을 처리하는 데 탁월한 순수 성능을 제공합니다.
r2d2
연결 풀링을 통한 데이터베이스 상호 작용은 병목 현상을 방지하면서 효율적으로 관리됩니다. - 타입 안전성: Diesel의 컴파일 타임 검사는 API 핸들러가 데이터베이스와 올바르게 상호 작용함을 보장합니다.
NewUser
,User
,NewPost
,Post
및UpdatePost
구조체는 데이터베이스 작업 및 JSON 페이로드에 직접 해당하며 런타임 유형 오류를 최소화합니다. 스키마 또는 쿼리 구조의 불일치는 컴파일 오류로 이어져 버그를 조기에 발견합니다. - 유지보수성: 웹 프레임워크 로직(라우팅, 요청 처리)과 데이터베이스 상호 작용 로직(Diesel 쿼리) 간의 명확한 분리는 코드베이스를 체계적이고 유지 관리하기 쉽게 만듭니다. 핸들러는 데이터 흐름을 조정하는 데 중점을 두고 Diesel은 SQL의 복잡성을 처리합니다.
- 확장성: Rust의 메모리 효율성과 Axum/Actix Web의 비동기 특성은 이러한 서비스를 최소한의 리소스 소비로 많은 수의 동시 연결을 처리할 수 있게 해주어 트래픽이 많은 애플리케이션에 이상적입니다.
- 견고성: Rust의 소유권 시스템과 빌림 검사기는 null 포인터 역참조 및 데이터 경합과 같은 일반적인 버그 클래스를 제거하여 보다 견고하고 안정적인 서비스를 제공합니다.
결론
Diesel과 함께 Axum 또는 Actix Web을 사용하여 Rust로 RESTful API를 구축하는 것은 성능, 타입 안전성 및 안정성을 추구하는 개발자에게 강력한 조합을 제공합니다. 이 스택은 단순히 빠른 것뿐만 아니라 복원력이 뛰어나고 유지 관리하기 즐거운 백엔드 서비스를 만들 수 있도록 지원합니다. 잠재적인 오류의 대부분을 컴파일 타임에 포착함으로써 Rust, Axum/Actix Web 및 Diesel은 백엔드 인프라에서 기대할 수 있는 수준을 전체적으로 향상시킵니다. 이러한 상승적인 접근 방식은 웹 개발에서 Rust의 잠재력을 진정으로 열어주며 속도와 정확성 모두에서 달성 가능한 것의 경계를 넓힙니다.