Soojeong Lee

2025. 7. 29.

Soojeong Lee

2025. 7. 29.

Soojeong Lee

2025. 7. 29.

Rust axum으로 백엔드?

Rust axum으로 백엔드?

Rust Axum 해보기

일단 결과물부터..?

https://github.com/MontyCoder0701/axum-starter


왜 Rust로 백엔드를 굳이 하는가

사실 당장 꼭 해야하는 이유는 없다. 개인적으로는 Rust가 좋은 언어이고, Node 환경보다 훨씬 빠르다. 미래에 언젠가 Rust 백엔드가 점령을 하지 않을까 싶지만 하도 Java Spring이 독점을 하다보니 쉽게 바뀔 것 같지는 않다.


그래서 뭘 한건가

Nest.js는 엄청!!! 친절하다. 무슨 소리냐면 cli 한줄이면 원하는거 뚝딱 다 만들어져있다. 그에 반해 Rust의 프레임워크는 아직 대중적인게 없고, 생태계가 그리 크지 않다. 그건 무슨 뜻이냐면 장인정신으로 하나 하나 원하는걸 만들어야 한단 뜻이다.


일단 메인 코드부터 공유하겠다.

use std::env;

use axum::{
    Router,
    http::{
        HeaderValue, Method, StatusCode,
        header::{AUTHORIZATION, CONTENT_TYPE},
    },
};
use deadpool_diesel::{
    Runtime,
    mysql::{Manager, Pool},
};
use dotenvy::dotenv;
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;

mod common;
mod hello;
mod user;

#[tokio::main]
async fn main() {
    dotenv().ok();

    const PORT: u16 = 3200;
    const HOST: &str = "localhost";

    let db_url = env::var("DATABASE_URL").unwrap();
    let client_url = env::var("CLIENT_URL").unwrap();

    let manager = Manager::new(db_url, Runtime::Tokio1);
    let pool = Pool::builder(manager).build().unwrap();

    let cors = CorsLayer::new()
        .allow_origin(client_url.parse::<HeaderValue>().unwrap())
        .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
        .allow_credentials(true)
        .allow_headers([AUTHORIZATION, CONTENT_TYPE]);

    let app = Router::new()
        .merge(hello::controller::routes())
        .merge(user::controller::routes())
        .layer(cors)
        .with_state(pool)
        .fallback((StatusCode::NOT_FOUND, "404 Not Found"));

    let listener = TcpListener::bind(format!("{HOST}:{PORT}")).await.unwrap();

    axum::serve(listener, app).await.unwrap();
}


DB 커넥션을 생성하고, CORS를 추가하고, 라우터를 등록했다. 아주 전형적인 흐름이다. 여기서 나름 신경 쓰려고 노력한 부분은 모듈과 각 계층의 분리다.

공식문서도 그렇고 딱히 모듈과 계층 분리를 명시적으로 보여주지는 않는다. 하지만 우리는 유지보수를 생각하면 해야하지 않을까?

그래서 나름 구조를 잡아보았다.
User 모듈부터 만들었다. 여기에 model, schema, service, controller가 들어간다. controller만 public으로 선언한 이유는 어차피 모듈 밖으로 노출되는건 controller의 router 뿐이기 때문이다.

pub mod controller;
mod model;
mod schema;
mod service;


그러고 나서 controller에 route를 등록한다.

use axum::{Router, routing::get};
use deadpool_diesel::mysql::Pool;

use super::service::get_users;

const PATH: &str = "/users";

pub fn routes() -> Router<Pool> {
    Router::new().route(PATH, get(get_users))
}


그러면 비즈니스 로직 및 DB와 상호작용하는 부분은 service에서 담당한다.

use axum::{Json, extract::State, http::StatusCode};
use deadpool_diesel::mysql::Pool;
use diesel::prelude::*;

use super::{model::User, schema::user};
use crate::common::utils::internal_error;

pub async fn get_users(State(pool): State<Pool>) -> Result<Json<Vec<User>>, (StatusCode, String)> {
    let conn = pool.get().await.map_err(internal_error)?;

    let users = conn
        .interact(|conn| user::table.select(User::as_select()).load(conn))
        .await
        .map_err(internal_error)?
        .map_err(internal_error)?;

    Ok(Json(users))
}


그리고 ORM은 Diesel이라고 제일 유명한 걸로 썼다.

Schema를 선언해주고,

diesel::table! {
    user (id) {
        id -> Integer,
        name -> Varchar,
    }
}

그에 해당되는 model을 만들어놓는다.

use diesel::{mysql::Mysql, prelude::*};

use super::schema::user;

#[derive(Queryable, Selectable)]
#[diesel(table_name = user)]
#[diesel(check_for_backend(Mysql))]
#[derive(serde::Serialize)]

pub struct User {
    pub id: i32,
    pub name: String,
}


아주 익숙한 패턴이지 않은가? 하지만 놀라운 사실은 이걸 손수 다 만들어야 한단 것이다. 뚝딱 이런거 없어요… 이제 다음 목표는 위를 적절한 struct로 만들어서 다른 모듈에서 implement 할 수 있도록 추상화를 하는 것이다.

아무튼 … 할만 한듯..?

Comments

Comments

Comments