Rust의 sqlx 및 bb8/deadpool을 사용한 효율적인 데이터베이스 연결 관리
Lukas Schneider
DevOps Engineer · Leapcell

소개
현대 웹 서비스 및 백엔드 애플리케이션의 영역에서 효율적이고 안정적인 데이터베이스 상호 작용은 매우 중요합니다. 데이터베이스에 자주 연결하는 모든 애플리케이션은 모든 요청에 대해 새 연결을 설정하는 오버헤드를 염두에 두어야 합니다. TCP 핸드셰이크, 인증 및 리소스 할당을 포함하는 이 오버헤드는 중간 정도의 부하에서도 빠르게 성능 병목 현상이 될 수 있습니다. 또한, 관리되지 않는 연결 생성은 데이터베이스 서버 자체에서 리소스를 고갈시켜 불안정성과 서비스 저하를 유발할 수 있습니다.
이것이 바로 데이터베이스 연결 풀링이 빛을 발하는 지점입니다. 즉시 사용할 수 있는 연결 풀을 유지 관리함으로써 애플리케이션은 지연 시간과 리소스 소비를 크게 줄일 수 있습니다. 모든 작업에 대해 새 연결을 여는 대신, 풀에서 연결을 빌려와 사용한 다음 반환합니다. 이 관행은 처리량과 시스템 안정성을 크게 향상시킵니다. Rust 생태계에서 sqlx
는 컴파일 시 안전하고 강력한 데이터베이스 상호 작용을 제공하는 비동기 ORM으로 사랑받고 있습니다. sqlx
의 기능을 보완하기 위해 bb8
및 deadpool
과 같은 연결 풀 라이브러리에 의존하며, 이 라이브러리는 이러한 귀중한 데이터베이스 연결을 효율적으로 관리하도록 특별히 설계되었습니다. 이 글에서는 고성능 및 복원력 있는 Rust 애플리케이션을 구축하기 위해 sqlx
를 bb8
또는 deadpool
과 함께 효과적으로 활용하는 방법에 대해 자세히 알아봅니다.
핵심 개념 이해
구현 세부 정보로 들어가기 전에 논의의 핵심이 되는 몇 가지 주요 용어를 명확히 하겠습니다.
sqlx
: 컴파일 시 확인되는 쿼리를 제공하는 순수 Rust SQL 크레이트입니다. PostgreSQL, MySQL, SQLite, Microsoft SQL Server와 같은 다양한 데이터베이스를 지원합니다.sqlx
는 타입 안정성과 관용적인 Rust에 중점을 맞춰 런타임 전에 일반적인 SQL 주입 및 타입 불일치 오류를 방지합니다. 비동기 특성으로 인해 현대적이고 고동기화된 애플리케이션(high-concurrency applications
)에 완벽하게 적합합니다.- 연결 풀: 애플리케이션에서 유지 관리하는 데이터베이스 연결 캐시입니다. 모든 요청에 대해 새 연결을 만드는 대신, 애플리케이션은 풀에서 사용 가능한 연결을 요청합니다. 사용 후 연결은 풀로 반환되어 다음 요청을 위해 준비됩니다. 이 디자인 패턴은 연결 오버헤드를 크게 줄이고 응답 시간을 개선합니다. 연결 풀은 일반적으로 리소스 고갈을 방지하기 위해 연결 유효성 검사, 유휴 시간 초과 및 최대 연결 제한을 처리합니다.
bb8
: Rust용 일반 비동기 연결 풀입니다. 다양한 데이터베이스 드라이버(또는 풀링이 필요한 기타 리소스)와 통합할 수 있는 유연한 프레임워크를 제공합니다.bb8
은 강력한 오류 처리와 구성 가능한 풀 설정을 통해 연결 관리에 대한 세밀한 제어를 가능하게 하는 것으로 알려져 있습니다.deadpool
: Rust용 또 다른 강력한 비동기 연결 풀로, 종종sqlx
와 함께 사용됩니다.deadpool
은 사용하기 쉬운 API를 제공하여 단순성과 효율성을 목표로 합니다. 연결 생성, 재활용 및 종료를 자동으로 처리하여 많은 시나리오에서 "그냥 작동하는" 솔루션이 됩니다.deadpool
은 Rust에서 가장 인기 있는 비동기 런타임인tokio
와도 잘 통합됩니다.tokio
: Rust용 선도적인 비동기 런타임입니다. 고성능 비동기 애플리케이션 구축에 필요한 도구와 기본 요소(작업, I/O, 타이머 포함)를 제공합니다.sqlx
,bb8
,deadpool
모두tokio
를 기반으로 구축되거나tokio
와 원활하게 통합됩니다.
sqlx 및 bb8/deadpool을 사용한 강력한 연결 풀 구축
sqlx
와 연결 풀을 함께 사용하는 핵심 아이디어는 애플리케이션 시작 시 풀을 한 번 초기화한 다음 데이터베이스 상호 작용이 필요할 때마다 이 풀에서 연결을 가져오는(또는 "획득") 것입니다. bb8
및 deadpool
로 이를 달성하는 방법을 살펴보겠습니다.
bb8
과의 통합
bb8
은 sqlx
의 PgConnection
(또는 기타 연결 유형)과 직접 통합되는 bb8-postgres
크레이트(또는 기타 데이터베이스용)를 제공합니다.
먼저 Cargo.toml
에 필요한 종속성을 추가합니다.
[dependencies] sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "macros", "time"] } tokio = { version = "1", features = ["full"] } bb8 = "0.8" bb8-postgres = "0.8" dotenvy = "0.15" # 환경 변수 로드용
다음으로, 연결 풀을 설정하고 사용법을 시연해 보겠습니다.
use sqlx::{PgPool, postgres::PgPoolOptions}; use tokio::net::TcpStream; use bb8::{Pool, PooledConnection}; use bb8_postgres::PostgresConnectionManager; use dotenvy::dotenv; use std::time::Duration; use tokio::sync::OnceCell; // 전역 연결 풀 static DB_POOL: OnceCell<Pool<PostgresConnectionManager>> = OnceCell::const_new(); async fn initialize_db_pool() -> Result<(), Box<dyn std::error::Error>> { dotenv().ok(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set in .env file or environment variables."); let manager = PostgresConnectionManager::new( database_url.parse()?, tokio_postgres::NoTls, // 또는 TLS로 연결하려면 tokio_postgres::TlsStream ); let pool = Pool::builder() .max_size(10) // 풀의 최대 연결 수 .min_idle(Some(2)) // 최소 유휴 연결 수 .build(manager) .await?; DB_POOL.set(pool).map_err(|_| "Failed to set DB_POOL")?; println!("Database pool initialized successfully with bb8."); Ok(()) } async fn create_user(username: &str, email: &str) -> Result<(), sqlx::Error> { let pool = DB_POOL.get().expect("DB_POOL not initialized"); let conn = pool.get().await.map_err(|e| sqlx::Error::PoolTimedOut)?; // bb8 오류를 필요한 경우 sqlx 오류로 변환 sqlx::query!( r#"