Rust ORM: Diesel과 SQLx 심층 분석
Lukas Schneider
DevOps Engineer · Leapcell

소개
웹 개발 및 데이터 중심 애플리케이션의 세계에서 객체-관계형 매퍼(ORM)는 객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 격차를 해소하는 데 중요한 역할을 합니다. ORM은 원시 SQL 쿼리의 복잡성을 추상화하여 개발자가 친숙한 언어 구문을 사용하여 데이터베이스와 상호 작용할 수 있도록 합니다.
Rust는 안전성, 성능 및 동시성에 대한 강력한 강조를 통해 정교한 ORM 솔루션이 등장하는 것을 보았습니다. 이들 중 Diesel과 SQLx는 두드러진 선택지로, 데이터 무결성과 개발자 생산성을 보장하기 위해 고유한 접근 방식을 제공합니다. 이 글에서는 이 두 가지 강력한 Rust ORM을 심층적으로 살펴보고, 핵심 철학, 구현 세부 정보 및 실제적 의미를 검토하여 각각의 강점과 사용 사례에 대한 포괄적인 이해를 제공합니다.
핵심 개념
Diesel과 SQLx의 구체적인 내용을 살펴보기 전에, 작동 방식을 이해하는 데 중요한 몇 가지 기본 용어를 정의해 보겠습니다.
- ORM (객체-관계형 매퍼): 데이터베이스 스키마를 객체 지향 패러다임에 매핑하여 개발자가 프로그래밍 언어의 객체로 데이터베이스 레코드를 조작할 수 있도록 하는 프로그래밍 도구입니다.
- 쿼리 빌더: SQL 쿼리를 프로그래밍 방식으로 구성하는 데 도움이 되는 라이브러리 또는 구성 요소로, 종종 SQL 구조와 유사한 API를 제공합니다.
- 스키마 마이그레이션: 애플리케이션의 데이터 모델 변경을 수용하기 위해 시간이 지남에 따라 데이터베이스 스키마를 발전시키는 프로세스입니다.
- 컴파일 타임 확인: 컴파일 프로세스 중에 컴파일러가 수행하는 검증으로, 실행 전에 코드의 정확성과 안전성을 보장합니다. 이것은 Rust의 핵심 원칙입니다.
- 매크로: Rust에서 다른 코드를 작성하는 코드를 작성할 수 있는 코드 생성 메커니즘입니다. 프로시저(예:
proc-macros) 또는 선언적일 수 있으며, 메타 프로그래밍 작업에 자주 사용됩니다.
Diesel: 타입 시스템을 통한 컴파일 타임 보장
Diesel은 Rust의 강력한 타입 시스템을 활용하여 SQL 쿼리의 정확성에 대한 컴파일 타임 보장을 제공하는 강력하고 매우 독선적인 ORM입니다. 애플리케이션이 실행되기 전에 열 이름의 오타 또는 타입 불일치와 같은 일반적인 데이터베이스 오류를 방지하는 것을 목표로 합니다.
Diesel 작동 방식
Diesel은 주로 쿼리 빌더와 스키마 관리를 통해 컴파일 타임 검사를 수행합니다. 일반적으로 diesel print-schema 명령으로 생성된 schema.rs 파일을 통해 Rust로 데이터베이스 스키마를 정의합니다. 이 스키마 파일에는 데이터베이스 테이블 및 열을 미러링하는 Rust 타입이 포함됩니다. Diesel API를 사용하여 쿼리를 구성할 때 Rust 컴파일러는 작업이 정의된 스키마와 일치하는지 확인합니다.
예제: Diesel로 스키마 정의 및 쿼리
먼저 데이터베이스에 posts 테이블이 있다고 가정해 보겠습니다.
CREATE TABLE posts ( id SERIAL PRIMARY KEY, title VARCHAR NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT FALSE );
diesel print-schema를 사용하면 src/schema.rs에서 다음과 유사한 내용을 얻게 됩니다.
// @generated automatically by Diesel CLI. diesel::table! { posts (id) { id -> Int4, title -> Varchar, body -> Text, published -> Bool, } }
이제 이 테이블을 쿼리하기 위해 Diesel 코드를 작성해 보겠습니다.
use diesel::prelude::*; use diesel::PgConnection; // 또는 사용 중인 다른 데이터베이스 #[derive(Queryable, Selectable)] #[diesel(table_name = crate::schema::posts)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, } pub fn establish_connection() -> PgConnection { let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); PgConnection::establish(&database_url) .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) } pub fn get_posts() -> Vec<Post> { use crate::schema::posts::dsl::*; let mut connection = establish_connection(); posts .filter(published.eq(true)) .limit(5) .select(Post::as_select()) .load::<Post>(&mut connection) .expect("Error loading posts") } fn main() { let published_posts = get_posts(); for post in published_posts { println!("Title: {}", post.title); } }
이 예제에서:
#[derive(Queryable, Selectable)]매크로는 데이터베이스 행을 Rust 구조체에 매핑하는 데 도움이 됩니다.posts.filter(published.eq(true))는 컴파일 타임에 타입 검사를 받습니다.posts.filter(non_existent_column.eq(true))를 시도하면non_existent_column이schema.rs의posts테이블 정의에 포함되지 않기 때문에 컴파일러가 즉시 오류를 플래그 지정합니다.select(Post::as_select())는 선택된 열이Post구조체의 필드와 일치하는지 확인합니다.
Diesel 사용 사례
Diesel은 다음과 같은 애플리케이션에서 탁월합니다.
- 강력한 컴파일 타임 보장이 무엇보다 중요합니다. 개발 주기의 초기에 데이터베이스 관련 오류를 포착하는 것이 중요합니다.
- 복잡한 쿼리가 일반적입니다. 타입 안전 쿼리 빌더는 복잡한 SQL 로직을 관리하는 데 도움이 됩니다.
- 데이터베이스 스키마가 상대적으로 안정적입니다.
schema.rs를 다시 생성해야 하므로 빈번한 스키마 변경은 번거로울 수 있습니다. - 성능이 중요한 관심사입니다. Diesel은 종종 수동으로 작성한 쿼리와 비교할 수 있는 효율적인 SQL을 생성합니다.
SQLx: 원시 SQL을 위한 컴파일 타임 매크로
SQLx는 컴파일 타임 안전성을 위해 비슷하지만 강력한 다른 접근 방식을 취합니다. 생성된 스키마에 의존하는 대신 절차적 매크로를 사용하여 컴파일 중에 라이브 데이터베이스에 연결하고 원시 SQL 쿼리를 검증합니다. 즉, 일반 SQL을 작성하지만 SQLx가 정확성을 보장합니다.
SQLx 작동 방식
SQLx는 sql! 매크로를 통해 마법을 구현합니다. 이 매크로를 사용하면 SQLx가 컴파일 타임에 DATABASE_URL로 지정된 데이터베이스에 연결하고 SQL 쿼리를 "가상 실행" 방식으로 실행하며 입력 매개변수와 출력 타입을 추론합니다. SQL에 구문 오류가 있거나 예상 열이 일치하지 않거나 잘못된 매개변수 타입이 있으면 컴파일러가 보고합니다.
예제: SQLx로 쿼리
동일한 posts 테이블 예제를 사용해 보겠습니다.
use sqlx::{PgPool, FromRow, postgres::PgPoolOptions}; use dotenvy::dotenv; #[derive(Debug, FromRow)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, } pub async fn establish_connection() -> PgPool { dotenv().ok(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Failed to connect to Postgres.") } pub async fn get_posts_sqlx() -> Result<Vec<Post>, sqlx::Error> { let pool = establish_connection().await; // 여기서 마법이 일어납니다! let posts = sqlx::query_as!( Post, "SELECT id, title, body, published FROM posts WHERE published = $1 LIMIT $2", true, // $1 5_i64 // $2, 타입은 sqlx에 중요합니다 ) .fetch_all(&pool) .await?; Ok(posts) } #[tokio::main] async fn main() { match get_posts_sqlx().await { Ok(posts) => { for post in posts { println!("Title: {}", post.title); } } Err(e) => { eprintln!("Error fetching posts: {:?}", e); } } }
이 SQLx 예제에서:
sqlx::query_as!매크로는 첫 번째 인수로 원시 SQL을 사용합니다.- 컴파일 중에 SQLx는 데이터베이스에 연결하고
SELECT문을 검증하며 열 이름과 타입이Post구조체에 선언된 타입과 일치하는지 확인합니다. - 존재하지 않는 열을 포함하면 예를 들어
SELECT non_existent_column FROM posts와 같이 컴파일러가 "column "non_existent_column" does not exist"와 같은 오류를 생성합니다. - 매개변수 타입도 확인합니다.
$2에 대해i64가 예상되는 곳에String을 전달하면 컴파일러가 이를 감지합니다. - SQLx는 Rust 구조체의
Option<T>를 사용하여 선택적 Nullable 열을 암묵적으로 처리합니다.
SQLx 사용 사례
SQLx는 다음과 같은 시나리오에서 빛을 발합니다.
- 개발자가 원시 SQL 작성을 선호하는 경우: 쿼리 최적화 및 복잡한 SQL 기능(예: CTE, 윈도우 함수)에 대한 완전한 제어를 원합니다.
- 기존 SQL 쿼리를 통합해야 하는 경우: 기존 SQL 코드베이스를 이전하기가 더 쉽습니다.
- 비동기 작업이 최우선적으로 고려되는 경우: SQLx는
async/await를 염두에 두고 설계되어 동시 애플리케이션에 자연스럽게 적합합니다. - `스키마 변경이 빈번하거나 동적인 경우:** SQLx가 라이브 데이터베이스에 직접 검증하므로 스키마 파일을 다시 생성할 필요가 없습니다.
- 최소한의 ORM 추상화가 선호되는 경우: SQLx는 SQL을 숨기려고 하는 완전한 ORM보다는 컴파일 타임 검증과 함께 타입 안전 쿼리 빌더 역할을 더 많이 합니다.
결론
Diesel과 SQLx는 Rust에서 데이터베이스 상호 작용에 대한 매력적인 솔루션을 제공하며, 각각 약간 다른 선호도와 프로젝트 요구 사항에 맞춰집니다. 생성된 스키마를 통한 컴파일 타임 검사를 제공하는 Diesel은 스키마 안정성과 강력한 추상화를 중시하는 견고한 애플리케이션에 이상적인, 타입 안전성과 쿼리 구성을 위한 매우 관용적인 Rust API를 우선시합니다.
반면에 SQLx는 원시 SQL을 수용하면서 컴파일 타임 매크로를 활용하여 동등하게 강력한 안전망을 제공하며, 특히 비동기 애플리케이션 및 직접 SQL 애호가에게 적합한 탁월한 유연성과 쿼리 제어를 제공합니다.
이 둘 사이의 선택은 종종 원하는 ORM 추상화 수준과 쿼리 정확성을 보장하는 선호하는 방법에 대한 트레이드오프에 달려 있지만, 둘 다 의심할 여지 없이 Rust에서 데이터베이스 프로그래밍의 표준을 높입니다.

