Rust로 Sql 웹 서버를 만들어보자

Molly·2023년 6월 24일
2

Rust

목록 보기
1/1
post-thumbnail

Rust는 C++와 같은 기존의 시스템 프로그래밍 언어들이 갖고 있는 성능과 저수준 접근 가능성을 유지하면서, 메모리 안정성과 데이터 레이스 방지 등이 뛰어난 언어로, 최근의 조사에서 가장 사랑받는 프로그래밍 언어로 꼽힐 정도로 핫한 프로그래밍 언어이다.

기존에는 Node.js와 Next.js api로 서버를 만들었는데, Typescript와 비교했을때 Rust의 뛰어난 서버 성능으로 인해 큰 관심을 갖기 시작했다.
(참고: https://www.youtube.com/watch?v=Z0GX2mTUtfo&t=1s)

Rust는 인기에 비해 실제 사용률은 낮은 편이지만, 우수한 성능과 커뮤니티의 빠른 활성화를 보며, 나의 Backend 언어로 채택했고 계속 공부해보고자 한다.

이번 포스트에서는, Rust로 Mysql 데이터를 가져오는 간단한 웹 서버를 만들어보고자 한다.



Rust 프로젝트 시작하기


1. 프로젝트 구조

프로젝트의 디렉토리 구조는 다음과 같다.

backend에는 Rust서버가, frontend에는 Next.js 프로젝트가 담길 것이고, backend/src/api에 서버코드가 작성되어, main.rs에서 실행될 것이다.

Rust 프로젝트를 시작하기 위해 터미널에 다음과 같이 입력한다.

cargo new backend

자동으로 backend 디렉토리와 함께, src디렉토리의 main.rs, 그리고 cargo.lock, cargo.toml 파일이 생성될 것이다.


2. Cargo.toml

Cargo는 Rust의 공식 패키지 매니저이다. Cargo.toml 프로젝트의 패키지 dependency를 자동으로 다운로드하고 빌드하므로, 우리는 그저 dependency를 Cargo.toml에 작성하기만 하면 된다.

이번 프로젝트의 Cargo.toml이다.

/backend/Cargo.toml

actix-web

Actix-web은 Rust에서 웹 서버와 웹 애플리케이션을 만들기 위한 고성능 웹 프레임워크이다. Actix-web은 actor 기반 시스템인 Actix를 기반으로 하며, 효율적인 비동기 작업을 지원한다.

sqlx

Sqlx는 Rust에서 데이터베이스에 안전하게 액세스하기 위한 비동기 라이브러리이다. SQL 쿼리를 컴파일 시간에 검사하여 잘못된 쿼리를 미리 알 수 있게 해주는 기능이 특징이 있다. features에 기능을 추가함으로써, MySQL 데이터베이스와 함께 사용하기 위한 기능, Tokio를 사용한 비동기 런타임 지원, 그리고 SQL 쿼리를 컴파일 시간에 검사하는 매크로 기능이 활성화되어 있다.

dotenv

.env 파일에서 환경 변수를 로드하는 라이브러리이다.

serde, serde_json

Serde는 Rust에서 직렬화와 역직렬화를 처리하는 프레임워크이다. 여기서 직렬화는 복잡한 데이터 구조를 바이트 스트림이나 문자열로 변환하는 과정을, 역직렬화는 이를 다시 원래의 데이터 구조로 복원하는 과정을 의미한다.

Serde_json은 Serde 프레임워크를 사용하여 JSON 데이터를 처리하는 라이브러리이다. 이 라이브러리를 사용하면 Rust의 데이터 구조를 JSON으로 변환하거나, JSON 데이터를 Rust의 데이터 구조로 변환할 수 있다.



Http서버 생성하기,
데이터베이스 연결하기


/src/main.rs

mod api;

use api::search::{main_search,qr_search};
use api::detail::{profile_detail,pep_positions};
use actix_web::{web, App, HttpServer};
use actix_web::middleware::DefaultHeaders;
use sqlx::MySqlPool;
use dotenv::dotenv;
use std::env;


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    let pool = MySqlPool::connect(&database_url)
        .await
        .expect("Failed to create pool");

    let test_query = "SELECT 1";
    match sqlx::query(test_query).execute(&pool).await {
        Ok(_) => println!("Database connected successfully"),
        Err(e) => eprintln!("Failed to connect to database: {:?}", e),
    }

    HttpServer::new(move || {
        App::new()
    .wrap(
    DefaultHeaders::new()
        .add(("Access-Control-Allow-Origin", "http://localhost:3000"))
        .add(("Access-Control-Allow-Methods", "GET"))
        .add(("Access-Control-Allow-Headers", "authorization, accept"))
        .add(("Access-Control-Allow-Headers", "content-type"))
        .add(("Access-Control-Max-Age", "3600"))
)
            .app_data(web::Data::new(pool.clone()))
            .service(qr_search)
            .service(main_search)
            .service(profile_detail)
            .service(pep_positions)
        
    })
    .bind("127.0.0.1:7878")?
    .run()
    .await
}
  1. mod api와 use api를 통해, /src/api 디렉토리에 작성해놓은 서버를 main.rs로 가져온다.

  2. actix_web, sqlx, dotenv, env 등 필요한 라이브러리를 가져온다.

  3. #[actix_web::main] 으로 비동기 함수를 main 함수로 사용하게 해주는 역할을 한다.

  4. database_url을 정의하는데, expect 메서드를 통해, env 설정이 안돼있으면, "DATABASE_URL must be set"라는 메세지와 함께 프로그램이 panic 상태에 빠진다.

  5. Mysql pool에 연결하고, test query를 보내어 데이터베이스에 connect됐는지 확인하고, 실패시 "Failed to create pool" 메세지와 함께 panic 상태에 빠진다.

  6. HTTPSERVER::new... 를 통해, Actix의 HTTP 서버를 생선한다. move || {}로, pool 변수의 소유권을 이어나오는 block으로 옮기면서 클로저를 정의한다.

  7. defaultHeaders로 Access Control을 추가한다. 이번 프로젝트에는 get요청만 사용될 것이고, allow-origin에 localhost:3000을 추가하여, frontend에서 데이터를 fetching할 수 있도록 cors 설정을 해준다.

  8. .app_data(web::Data::new(pool.clone()))은 Actix 웹 애플리케이션에 데이터를 등록하는 코드이다. web::Data는 애플리케이션의 다른 부분에서 접근할 수 있는 공유 데이터를 저장하는 데 사용되는 Actix 웹의 타입이다. 이 경우, 데이터베이스 연결 풀이 애플리케이션 전반에서 사용될 수 있도록 등록한다.

  9. .service를 통해, actix web 어플리케이션에 작성한 http 서버를 등록한다.

  10. 127.0.0.1:7878에 actix web 서버를 바인드하고, run().await을 통해 서버를 시작한다. 서버는 클라이언트의 요청을 기다리며 실행된다.



검색 서버 작성하기


QR코드를 입력했을때, 해당하는 결과를 보여줄 간단한 서버를 작성하고자 한다.

/src/api/search.rs

use actix_web::{web, HttpResponse, get, Responder};
use sqlx::MySqlPool;
use serde_json::json;
use sqlx::FromRow;
use serde::Serialize;
use serde::Deserialize;
use std::collections::HashMap;


#[derive(FromRow, Serialize, Deserialize)]
pub struct Search {
    WE_CD:Option<i64>,
    BIRTHDATE: Option<String>,
    NATION:Option<String>,
    DATA_SET:Option<String>,
    WHOLE_NAME:Option<String>,
    PROFILE_IMAGE:Option<String>
}


#[get("/api/qrSearch")]
async fn qr_search(pool: web::Data<MySqlPool>, search: web::Query<HashMap<String, String>>) -> impl Responder {
    let search_string: String = search.get("search").unwrap_or(&String::from("")).clone();
    let page: i32 = search.get("page").unwrap_or(&String::from("1")).parse().unwrap_or(1);
    let view: i32 = search.get("view").unwrap_or(&String::from("20")).parse().unwrap_or(20);


    let condition = if !cleaned_search.is_empty() {
        format!("AND WE_CD = {}", cleaned_search)
    } else {
        String::from("AND null")
    };
    
    let count_query = format!(
        "SELECT COUNT(*) as total
        FROM Wide_Eyes.explore_name
        WHERE 1=1 {}",
        condition
    );
    
    let count_result: Result<i32, _> = sqlx::query_scalar(&count_query)
        .fetch_one(pool.as_ref())
        .await;

    let total_count = match count_result {
        Ok(count) => count,
        Err(e) => {
            eprintln!("Database error: {:?}", e);  
            return HttpResponse::InternalServerError().json(json!({ "message": "Can't find data" }));
        },
    };
    
    let limit = format!("LIMIT {}, {}", (page-1)*view, view);
    
    let query = format!(
        "SELECT WE_CD, BIRTHDATE, NATION, PROFILE_IMAGE, DATA_SET, WHOLE_NAME
        FROM Wide_Eyes.explore_name
        WHERE 1=1 
        {}
        {}",
        condition,
        limit
    );

    let rows: Result<Vec<Search>, _> = sqlx::query_as(&query)
        .fetch_all(pool.as_ref())
        .await;

    match rows {
        Ok(result) => HttpResponse::Ok().json(json!({"data": result, "total": total_count })),
        Err(e) => {
            eprintln!("Database error: {:?}", e); 
            HttpResponse::InternalServerError().json(json!({ "message": "Can't find data" }))
        },
    }
}
  1. #[derive(FromRow, Serialize, Deserialize)] Rust의 Derive 속성으로, FromRow, Serialize, Deserialize 트레이트를 자동으로 Search 구조체에 구현하도록 한다. FromRow는 데이터베이스의 한 행을 Rust 구조체로 변환하는데 사용한다.

  2. Seach라는 이름의 구조체를 정의하고, 각 필드는 값이 있거나, 없을 수 있기 때문에 Option 타입으로 설정했다.

  3. #[get("/api/qrSearch")] Actix 웹에서 제공하는 get 속성을 이용해 "/api/qrSearch" 경로에 GET 요청이 오면 이 함수를 실행하라는 라우팅을 설정하고 있다.

  4. qr_search라는 비동기 함수를 MySQL 연결 풀과 url 쿼리 파라미터를 인자로 받고, Responder 트레이트를 구현하는 타입을 반환한다. Responder은 HTTP 응답을 나타내는데 사용된다.

  5. search.get을 통해, url 쿼리파라미터에서 search 키의 값을 가져와 search_string이라는 변수에 저장한다. page와 view에도 마찬가지로 적용한다. (Hashmap 사용)

  6. 검색 문자열이 비어있는지 확인하는 변수인 condition을 정의하고, 비어있다면 AND null이라는 무의미한 조건을 사용한다.

  7. let count_query를 통해, 전체 결과 수를 찾기 위한 sql 쿼리 문자열을 작성한다. (pagination을 위해 활용), let count_result에서는 fetch_one을 사용하여 결과를 가져온다. (단일 필드를 가져오기 때문)

  8. let limit = format!("LIMIT {}, {}", (page-1)*view, view); 을 통해, 사용자가 화면에서 pagination 숫자 버튼을 클릭할때마다 자동으로 페이지와 뷰에 해당하는 데이터를 가져오게 설정한다.

  9. let query를 통해, 실제 결과 데이터를 가져올 sql 쿼리 문자열을 생성한다.

  10. let rows: Result<Vec, _> = sqlx::query_as(&query) ... .await; 를 통해, sql 쿼리를 실행하고, 각 행의 결과를 Search 구조체로 변환한 후, 벡터에 모아 반환한다. 가져올 필드가 다수 이므로, fetch_all을 사용한다.

  11. match rows를 통해 쿼리가 성공적으로 수행되었다면, result 벡터와 전체 아이템 수인 total_count를 JSON 형식으로 반환하고, 실패했다면 에러 메세지를 출력하고 500 상태 코드의 응답을 반환한다.



마무리


Rust로 Sql 웹 서버를 성공적으로 구현했다. 이제 사용자가 찾고 싶은 대상의 QR코드를 input에 입력하면 그에 해당하는 데이터가 작성한 Rust 서버를 거쳐, frontend에 데이터를 가져와 화면에 출력될 것이다.

Node.js와 Next.api에 비해서, 코드가 훨씬 robust하고 구체적임을 알 수 있다. 이미 Rust actix-web의 서버 성능의 우수함은 알려져있으므로, 더 파고들어 우수한 웹 서버를 만들면서 나의 주 backend 언어로 삼고자 한다.

profile
Next.js, Typescript, Rust

1개의 댓글

comment-user-thumbnail
2023년 7월 2일

https://melonplaymods.com/2023/06/11/minecraft-noobdanil_1288-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/john-wicknpc-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/mammott-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/nightmare-animatronics-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/aurma-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/dog-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/space-werewolf-kill-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/roblox-doors-pack-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/furniture-mod-for-melon-playground-2/
https://melonplaymods.com/2023/06/11/big-house-mod-for-melon-playground-2/
https://melonplaymods.com/2023/06/11/screamnpc-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/kids-mod-for-melon-playground-2/
https://melonplaymods.com/2023/06/11/invincible-meteor-hammer-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/evil-dream-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/fork-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/robotic-skeleton-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/statue-of-el-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/police-car-with-glow-mod-for-melon-playground/
https://melonplaymods.com/2023/06/10/by-bicycle-mod-for-melon-playground/
https://melonplaymods.com/2023/06/11/medications-and-stimulants-stalker-mod-for-melon-playground/

답글 달기