Diesel vs SeaORM: Rust에서 컴파일 시간 vs 동적 ORM 탐색
Min-jun Kim
Dev Intern · Leapcell

소개
Rust의 발전하는 생태계에서 강력하고 성능이 뛰어난 웹 서비스 또는 데이터 기반 애플리케이션을 구축하려면 안정적인 객체 관계 매핑(ORM) 솔루션이 필요한 경우가 많습니다. ORM은 직접적인 SQL 상호 작용의 복잡성을 추상화하여 개발자가 네이티브 Rust 구조체로 데이터베이스 엔터티를 작업할 수 있도록 합니다.
그러나 Rust 생태계는 ORM 디자인에 대해 몇 가지 뚜렷한 철학을 제공합니다. 바로 컴파일 시간 안전성을 우선시하는 것과 더 큰 동적 유연성을 제공하는 것입니다. 이 글에서는 두 가지 주요 Rust ORM인 Diesel과 SeaORM을 살펴보고 핵심 디자인 원칙을 탐구하며, Diesel의 컴파일 시간 접근 방식과 SeaORM의 동적 기능을 대조하고 궁극적으로 다음 Rust 프로젝트에 가장 적합한 ORM을 선택하도록 안내합니다.
이러한 차이점을 이해하는 것은 단순한 학문적 연습이 아닙니다. 실제 애플리케이션의 개발 속도, 유지 관리성 및 런타임 성능에 실질적인 영향을 미칩니다.
환경 이해
Diesel과 SeaORM의 특정 내용을 자세히 살펴보기 전에 Rust의 ORM과 관련된 몇 가지 핵심 용어를 간략하게 정의해 보겠습니다.
- ORM(객체-관계 매핑): 객체 지향 프로그래밍 언어를 사용하여 호환되지 않는 유형 시스템 간의 데이터를 변환하는 프로그래밍 기법입니다. Rust에서는 이를 데이터베이스 테이블 및 행을 Rust 구조체 및 인스턴스로 매핑하는 것을 의미합니다.
- 컴파일 시간 안전성: Rust 컴파일러가 애플리케이션이 실행되기 전, 컴파일 중에 데이터베이스 상호 작용과 관련된 오류(예: 잘못된 열 이름, 잘못된 데이터 유형)를 감지하는 기능입니다. 이는 종종 광범위한 매크로 사용을 포함합니다.
- 동적 쿼리 생성: 런타임에 데이터베이스 쿼리를 구성하는 기능으로, 특정 시나리오에서 더 유연하고 덜 장황한 코드를 허용하지만 일부 컴파일 시간 검사를 희생할 수 있습니다.
- Active Record 패턴: ORM에서 발견되는 아키텍처 패턴으로, 테이블이 클래스 또는 구조체로 래핑되어 데이터 작업(
save
,update
,delete
등)을 객체 자체에서 직접 수행할 수 있습니다. - Data Mapper 패턴: Mapper(또는 DAO - Data Access Object) 계층이 개체와 데이터베이스 간에 데이터를 전송하면서 둘 사이의 독립성을 유지하는 아키텍처 패턴입니다.
Diesel: 컴파일 시간 보호자
Diesel은 아마도 Rust 생태계에서 가장 성숙하고 널리 채택된 ORM일 것입니다.
그 핵심 강점은 컴파일 시간 안전성에 대한 약속에 있으며, Rust의 강력한 매크로 시스템을 활용하여 컴파일 중에 쿼리, 테이블 스키마 및 데이터 유형을 검증합니다. 이 접근 방식은 런타임 데이터베이스 오류의 가능성을 크게 줄여 애플리케이션을 더 견고하게 만듭니다.
원칙 및 구현:
Diesel은 코드가 컴파일되면 데이터베이스 상호 작용이 올바르게 수행될 가능성이 높다는 원칙에 따라 작동합니다.
이를 위해 데이터베이스 스키마를 나타내는 Rust 구조체를 생성합니다. 이러한 구조체는 유형 안전한 쿼 빌더 API를 사용하여 쿼리를 빌드하는 데 사용됩니다.
간단한 posts
테이블을 고려해 보겠습니다.
CREATE TABLE posts ( id SERIAL PRIMARY KEY, title VARCHAR NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT FALSE );
Diesel을 사용하면 schema.rs
파일(종종 diesel print-schema
로 생성됨)에 스키마를 정의합니다.
// src/schema.rs diesel::table! { posts (id) { id -> Int4, title -> Varchar, body -> Text, published -> Bool, } }
Rust 구조체:
// src/models.rs use diesel::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Queryable, Selectable, Debug, PartialEq, Serialize, Deserialize)] #[diesel(table_name = crate::schema::posts)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, } #[derive(Insertable, Debug, Serialize, Deserialize)] #[diesel(table_name = crate::schema::posts)] pub struct NewPost { pub title: String, pub body: String, }
이제 삽입 및 쿼리를 수행해 보겠습니다.
// src/main.rs (발췌) use diesel::prelude::*; use diesel::pg::PgConnection; use dotenvy::dotenv; use std::env; mod schema; mod models; use models::{Post, NewPost}; use schema::posts::dsl::*; fn establish_connection() -> PgConnection { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); PgConnection::establish(&database_url) .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) } fn main() { let mut connection = establish_connection(); // 새 게시물 삽입 let new_post = NewPost { title: "My First Post".to_string(), body: "This is the content of my first post.".to_string(), }; let inserted_post: Post = diesel::insert_into(posts) .values(&new_post) .get_result(&mut connection) .expect("Error saving new post"); println!("Saved post: {:?}", inserted_post); // 게시된 게시물 쿼리 let published_posts = posts .filter(published.eq(true)) .limit(5) .select(Post::as_select()) .load::<Post>(&mut connection) .expect("Error loading published posts"); println!("Published posts: {:?}", published_posts); }
posts
, published
, title
등이 모두 schema.rs
에서 파생된 유형 검사 요소임을 알 수 있습니다. 존재하지 않는 열을 쿼리하거나 잘못된 유형을 전달하려고 하면 Diesel이 컴파일 시간에 이를 감지합니다.
애플리케이션 시나리오:
- 고신뢰성 애플리케이션: 런타임 오류를 최소화해야 하는 경우.
- 안정적인 데이터베이스 스키마가 있는 애플리케이션: 스키마 변경 시
schema.rs
를 다시 생성하고 잠재적으로 Rust 코드를 업데이트해야 합니다. - 엄격한 유형 안전성을 우선시하는 팀: 데이터베이스 상호 작용에 Rust의 강력한 유형 시스템을 활용하는 이점이 있습니다.
SeaORM: 동적 유연주의자
Diesel과 대조적으로 SeaORM은 Active Record 및 Data Mapper 패턴에서 영감을 얻은 보다 동적인 접근 방식을 사용합니다.
비동기식, 스키마에 구애받지 않으며 매우 구성 가능한 ORM임을 자랑합니다. 쿼리 구조 자체에 대한 컴파일 시간 검증은 덜 제공하지만, 런타임에 쿼리를 구성하기 위한 강력하고 유창한 API와 데이터를 로드한 후에는 강력한 유형 안전성을 제공합니다.
원칙 및 구현:
SeaORM은 유연하고 간단한 복잡하고 동적인 쿼리 구축을 목표로 합니다.
schema.rs
에 크게 의존하여 쿼리를 구축하는 대신, 엔터티를 Rust 구조체로 정의하는 코드 우선 또는 엔터티 우선 접근 방식을 사용하며, 쿼리는 데이터베이스 작업에 매핑되는 유창한 API를 사용하여 구축됩니다.
데이터베이스 상호 작용을 위한 비차단 작업을 위해 async
/await
를 많이 활용합니다.
SeaORM으로 posts
테이블을 다시 살펴보겠습니다.
// src/entities/post.rs use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "posts")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub title: String, pub body: String, pub published: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {}
이제 삽입 및 쿼리를 수행해 보겠습니다.
// src/main.rs (발췌) use sea_orm::{ ActiveModelTrait, Database, EntityTrait, IntoActiveModel, Set, }; use dotenvy::dotenv; use std::env; mod entities; // 게시물 엔터티 포함 use entities::post; #[tokio::main] async fn main() -> Result<(), sea_orm::DbErr> { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let db = Database::connect(&database_url).await?; // 새 게시물 삽입 let new_post = post::ActiveModel { title: Set("My SeaORM Post".to_string()), body: Set("Content of my SeaORM post.".to_string()), published: Set(false), // 명시적으로 설정 ..Default::default() // 다른 필드는 기본값으로 채우기 }; let res = new_post.insert(&db).await?; println!("Saved post: {:?}", res); // 게시된 게시물 쿼리 let published_posts: Vec<post::Model> = post::Entity::find() .filter(post::Column::Published.eq(true)) .limit(5) .all(&db) .await?; println!("Published posts: {:?}", published_posts); Ok(()) }
SeaORM은 열을 참조하기 위해 post::Column::Published
를 사용합니다. 이 자체는 모델 내에서 열이 존재한다는 것을 아는 한 유형 안전하지만 Diesel만큼 엄격하게 쿼리 문자열 자체를 컴파일 시간에 검증하지는 않습니다.
그러나 유창한 API는 복잡한 조인 및 하위 쿼리를 매우 읽기 쉽고 구성 가능하게 만듭니다. 비동기 특성도 최신 웹 서비스에 중요한 이점입니다.
애플리케이션 시나리오:
- 비동기 웹 서비스: Rust의
async
/await
를 활용하여 비차단 I/O를 수행합니다. - 빠르게 발전하는 스키마가 있는 애플리케이션:
schema.rs
에 대한 결합력이 낮으면 일부 개발 워크플로에서 스키마 변경이 더 원활해질 수 있습니다. - 고도로 구성 가능하고 동적인 쿼리를 원하는 경우: 유창한 API는 복잡한 쿼리를 프로그래밍 방식으로 우아하게 구축하는 데 탁월합니다.
- 'Rust 네이티브' 느낌을 선호하는 경우: 쿼리 검증을 위해 외부 매크로에 크게 의존하지 않습니다.
ORM 선택
Diesel과 SeaORM 간의 선택은 주로 프로젝트 우선 순위와 팀의 선호도에 달려 있습니다.
- 최대 컴파일 시간 안전성과 강력하고 덜 동적인 쿼리 요구 사항의 경우: Diesel은 훌륭한 선택입니다. 성숙하고 잘 문서화되어 있으며 런타임 오류를 발생시키기 전에 많은 데이터베이스 관련 오류를 감지합니다. 데이터베이스 스키마가 상대적으로 안정적이고 컴파일 시간 보장이 가장 중요하다면 Diesel은 강력한 후보입니다.
- 비동기 작업, 동적 쿼리 빌딩 및 '코드 우선' 접근 방식의 경우: SeaORM이 빛을 발합니다. 비동기 특성은 고도로 병렬화된 서비스에 이상적이며, 유연한 API는 개발자가 런타임에 복잡한 쿼리를 우아하게 구성할 수 있도록 합니다.
더 동적인 쿼리 패턴을 예상하거나 비동기 네이티브 솔루션을 선호하는 경우 SeaORM이 더 나은 선택일 가능성이 높습니다.
두 ORM 모두 Rust 생태계에서 강력한 솔루션을 제공합니다. Diesel의 컴파일 시간 검증 강조는 탁월한 안전성을 제공하는 반면 SeaORM은 최신 비동기 기능과 동적 쿼리 구성을 제공합니다.
궁극적으로 Rust 프로젝트에 가장 적합한 ORM은 철권 같은 컴파일 시간 검사(Diesel) 또는 유연한 비동기 네이티브 쿼리 구성(SeaORM) 중 어느 것을 우선시하는지에 달려 있습니다.
결론
Diesel과 SeaORM은 Rust에서 데이터베이스 상호 작용을 위한 설득력 있는 솔루션을 모두 제공하며, 각각 디자인 철학을 기반으로 뚜렷한 틈새 시장을 개척합니다.
Diesel은 컴파일 시간 안전성을 옹호하며 런타임 오류를 줄이는 강력한 보장을 제공합니다.
반면 SeaORM은 비동기 작업과 동적 쿼리 유연성을 우선시하여 최신 웹 서비스 아키텍처를 충족합니다.
궁극적으로 Rust 프로젝트에 가장 적합한 ORM은 철저한 컴파일 시간 검사(Diesel)와 유연하고 비동기식 네이티브 쿼리 생성(SeaORM) 중 어느 것을 우선시하는지에 따라 달라집니다.