본문 바로가기
서버구축 (WEB,DB)

SQLx와 Rust로 구현하는 안전하고 확장 가능한 데이터베이스 액세스

by 날으는물고기 2025. 9. 7.

SQLx와 Rust로 구현하는 안전하고 확장 가능한 데이터베이스 액세스

728x90

한줄요약 & 배경

  1. SQLx = ORM 아님. DSL 없이 그대로 SQL을 쓰되, 컴파일 타임에 실제 DB와 연결해 쿼리를 검증합니다.
  2. 비동기/순수 Rust(Postgres, MySQL/MariaDB 드라이버는 100% Rust; SQLite만 C 라이브러리 연동). 기본적으로 unsafe 금지로 안정성↑.
  3. 멀티 런타임 & TLS: tokio / async-std / actix + native-tls / rustls 호환.
  4. 멀티 DB: PostgreSQL / MySQL / MariaDB / SQLite. (MSSQL은 0.7 이후 제거, 재작성 예정)
  5. 운영 기능: 풀링(Pool), 준비문 캐시, 비동기 Row 스트리밍, 중첩 트랜잭션(세이브포인트), AnyPool(런타임 드라이버 교체), LISTEN/NOTIFY(Postgres).
  6. 라이선스: MIT/Apache-2.0 듀얼 — 오픈소스·상업 모두 OK.
  7. .env의 DATABASE_URL 지정만으로도 컴파일 타임 체크가 손쉬움.

결론: 대규모 서비스에서도 통할 만큼 기능·테스트·운영성이 강화되었고, ORM 없이 SQL을 직접 쓰고 싶은 팀에 강력 추천.

아키텍처·핵심 특징

구분 핵심
비동기 async/await 기반, 고동시성 I/O
타입/안전 query!, query_as!컴파일 타임 SQL 검증
DB 지원 Postgres / MySQL / MariaDB / SQLite
런타임/TLS tokio/async-std/actix + native-tls/rustls
운영 기능 Pool(풀링), 준비문 캐시, 스트리밍, 세이브포인트
알림 Postgres LISTEN/NOTIFY
드라이버 교체 Any/AnyPool (URL 스킴으로 런타임 선택)
안전성 기본 #![forbid(unsafe_code)] (SQLite 시 일부 예외)
라이선스 MIT / Apache-2.0 듀얼

설치 & 기능 플래그 선택 가이드

1. Cargo.toml (권장 예시: Tokio + rustls + Postgres)

[dependencies]
sqlx = { version = "0.8", features = [
  "runtime-tokio",
  "tls-rustls-aws-lc-rs",
  "postgres",
  "macros",
  "migrate",
  "uuid", "chrono", "json"
] }
  • 런타임: runtime-tokio / runtime-async-std
  • TLS: tls-native-tls 또는 tls-rustls-*
  • DB: postgres / mysql / sqlite (※ mssql은 드라이버 재작성 대기)
  • 확장: macros(컴파일 타임 검증), migrate(마이그), derive(FromRow), uuid/chrono/json

2. .env로 컴파일 타임 체크 활성화

DATABASE_URL=postgres://postgres:password@localhost/test

Tip: 개발·CI에선 샘플 스키마 DB를 띄워 두고 DATABASE_URL만 맞추면 매크로가 실제 DB에 붙어 쿼리를 검증합니다.

빠른 시작 (Pool 권장)

use sqlx::postgres::PgPoolOptions;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://postgres:password@localhost/test")
        .await?;

    // 런타임 준비문 예시
    let row: (i64,) = sqlx::query_as("SELECT $1")
        .bind(150_i64)
        .fetch_one(&pool).await?;

    assert_eq!(row.0, 150);
    Ok(())
}
  • Pool 중심 아키텍처: 연결 수 제어, 준비문 캐시, 재사용성 향상.
  • (MySQL/MariaDB는 파라미터 플레이스홀더 ?, Postgres는 $1 형식)

컴파일 타임 쿼리 검증 (핵심 가치)

1. query! (익명 레코드)

let rows = sqlx::query!(
    r#"
    SELECT country, COUNT(*) AS count
    FROM users
    WHERE organization = $1
    GROUP BY country
    "#,
    organization
)
.fetch_all(&pool) // Vec<{ country: String, count: i64 }>
.await?;

2. query_as! (명명된 타입)

struct Country { country: String, count: i64 }

let rows = sqlx::query_as!(
    Country,
    r#"
    SELECT country, COUNT(*) AS count
    FROM users
    WHERE organization = $1
    GROUP BY country
    "#,
    organization
)
.fetch_all(&pool).await?;
  • 장점: 바인딩 개수·타입·컬럼 호환성 컴파일 시 검증.
  • 필수: 빌드 시 DATABASE_URL실 DB 스키마에 붙어 확인.
  • 오프라인 모드: sqlx prepare로 메타를 캐시해 CI 속도·안정성 확보.
300x250

마이그레이션 & 오프라인 빌드

1. 마이그레이션 워크플로우

# 마이그 추가 (reversible 추천: -r)
sqlx migrate add -r add_users_table

# 적용
sqlx migrate run

# 컴파일타임 검증 메타 캐시 (오프라인 빌드)
sqlx prepare --merged
// 앱 시작 시 자동 적용(선택)
sqlx::migrate!("./migrations").run(&pool).await?;

2. 빌드 가속 (매크로 최적화)

[profile.dev.package.sqlx-macros]
opt-level = 3

운영 팁: DB 변경(PR)에는 DDL 영향·권한 변화·PII 영향·롤백 계획 기재를 필수화하세요.

운영·성능 베스트 프랙티스

  1. Pool 파라미터: max_connections, min_connections, acquire_timeout, idle_timeout을 트래픽·DB 한도에 맞춤.
  2. 준비문 캐시: query() 경로는 연결별 자동 캐싱. DDL 이후 캐시 무효화·재준비 고려.
  3. 대량삽입: Postgres는 COPY, 배치 Insert + 트랜잭션 묶기.
  4. 타임아웃 이중화: 앱(요청/쿼리) + DB(statement_timeout).
  5. 관측성: tracing 연동, 슬로우 쿼리 경보, 영향 행 수 로깅(민감정보 제외).
  6. 페이징: 대량 테이블은 키셋 페이징(OFFSET 회피) 권장.
  7. Runtimes: tokio 표준화(서버 프레임워크 대부분 호환)로 팀 러닝커브↓.

보안 가이드 & 점검표

1. 인젝션 방지 (가장 중요)

  • 항상 바인딩 사용. 문자열 결합/포매팅 금지.
    // ✅ 안전
    sqlx::query("SELECT * FROM users WHERE email = $1").bind(email);
    
    // ❌ 금지
    // sql = format!("SELECT * FROM users WHERE email = '{}'", email);
  • LIKE 검색 시 %/_ 이스케이프 정책 문서화(기본 유틸 제공).

2. 시크릿·자격증명

  • DATABASE_URL 등은 시크릿 매니저(Vault/Cloud Secret) 사용.
  • 로그/패닉 메시지에 절대 비밀번호/접속문자열/PII 노출 금지.
  • 개발용 .env는 로컬 한정, 리포지토리 커밋 금지.

3. TLS 강제

  • 연결 문자열에서 TLS 필수: Postgres sslmode=require, MySQL ssl-mode=REQUIRED.
  • 내부 CA 사용 시 루트 번들 배포·로테이션 체계화.

4. 최소 권한 원칙

  • 앱 계정은 DDL 금지, 필요한 DML/SELECT만.
  • 마이그 계정 분리(DDL 허용).
  • 스키마·테이블·뷰 단위 정밀 권한.

5. 감사·모니터링

  • 로그인 실패/권한 오류/DDL 시도를 SIEM(Elastic/Wazuh 등)으로 수집.
  • DML 감사가 필요하면 트리거+감사 테이블(변경 전/후·주체·트랜잭션ID).
  • 애플리케이션 레벨에 요청ID/주체ID를 함께 기록.

6. 스키마 방어

  • CHECK/ENUM으로 입력 범위 제한.
  • 멀티테넌트는 RLS(Row Level Security, Postgres) 검토.

7. 에러/로깅 정책

  • SQL 원문·파라미터 원값 로그 금지(특히 PII).
  • 마스킹 규칙: 이메일 a***@ex.com, 전화 010-****-****.

8. CI 보안 가드레일

  • query!/query_as! 사용률 지표화(메트릭/배지).
  • sqlx prepare 없는 PR은 CI 실패.
  • 마이그 PR 템플릿에 보안 영향/롤백 체크리스트.

9. MSSQL 이용 조직 메모

  • SQLx의 MSSQL 드라이버는 재작성 대기. 현재는 tiberius + 얇은 저장소 레이어로 운영 → SQLx MSSQL 안정화 후 마이그 경로 확보.
  • 보안 점검표(요약)
    1. 바인딩 100% 사용
    2. 컴파일 타임 검증 매크로 적용
    3. DB 계정 최소권한/분리
    4. TLS 강제 및 CA 관리
    5. 로그 마스킹/민감정보 차단
    6. DDL/DCL/실패 로그인 SIEM 연동
    7. 슬로우 쿼리/타임아웃 가드
    8. 마이그 PR 보안 항목/롤백 계획
    9. 백업·복구 DR 리허설(스키마 버전 포함)

SQLx 생태계 & 유사 도구

  • SeaORM / Welds: SQLx 위에서 동작하는 ORM. CRUD 생산성↑, 직접 SQL 컨트롤↓.
  • SeaQuery: 타입 세이프 쿼리 빌더. 복잡한 동적 쿼리에 유용.
  • SQLPage: SQL 중심 웹 프레임워크.
  • Diesel: 강력한 타입세이프 DSL(주로 동기), 러닝커브↑.
  • 결론: 명시적 SQL + 컴파일 검증을 원하면 SQLx가 가장 투명하고, 필요 시 ORM/빌더를 보완적으로 채택.

실전 코드 스니펫 모음

1. Pool & 타임아웃·TLS

use sqlx::postgres::PgPoolOptions;
use std::time::Duration;

let pool = PgPoolOptions::new()
    .max_connections(20)
    .min_connections(5)
    .acquire_timeout(Duration::from_secs(3))
    .idle_timeout(Duration::from_secs(300))
    .connect("postgres://app:***@db.internal/prod?sslmode=require")
    .await?;

2. 중첩 트랜잭션(세이브포인트)

let mut tx = pool.begin().await?;
sqlx::query!("UPDATE accounts SET balance = balance - $1 WHERE id = $2", amt, src)
    .execute(&mut *tx).await?;

let mut sp = tx.begin().await?; // savepoint
sqlx::query!("INSERT INTO ledger (src, dst, amt) VALUES ($1,$2,$3)", src, dst, amt)
    .execute(&mut *sp).await?;
sp.commit().await?;

tx.commit().await?;

3. 스트리밍 + 매핑

use futures_util::TryStreamExt;
use sqlx::{Row, postgres::PgRow};

let mut rows = sqlx::query("SELECT id, email, is_active FROM users WHERE email LIKE $1")
    .bind(format!("%{}%", keyword)) // ← LIKE는 바인딩 + 사전 이스케이프 유틸 권장
    .fetch(&pool);

while let Some(row) = rows.try_next().await? {
    let email: String = row.try_get("email")?;
    // ...
}

또는

#[derive(sqlx::FromRow)]
struct User { id: i64, email: String, is_active: bool }

let users = sqlx::query_as::<_, User>(
    "SELECT id, email, is_active FROM users WHERE is_active = $1"
)
.bind(true)
.fetch_all(&pool).await?;

4. 키셋 페이징(Postgres)

#[derive(sqlx::FromRow)]
struct User { id: i64, email: String }

let rows = sqlx::query_as!(
    User,
    r#"
    SELECT id, email
    FROM users
    WHERE ($1::bigint IS NULL OR id < $1)
    ORDER BY id DESC
    LIMIT $2
    "#,
    last_id, limit as i64
).fetch_all(&pool).await?;

5. 마이그·오프라인 메타

sqlx migrate add -r add_idx_users_email
sqlx migrate run
sqlx prepare --merged

적용 로드맵

  1. 런타임 표준화(tokio + rustls) & DB 표준(Postgres 권장) 확정
  2. 템플릿 리포지토리: sqlx + migrate! + tracing + CI에서 sqlx prepare 강제
  3. PR 체크리스트: 바인딩/컴파일 검증/권한/PII/타임아웃/슬로우쿼리/인덱스
  4. 시크릿 매니저 도입(DATABASE_URL·CA 번들)
  5. SIEM 연동: 로그인 실패/DDL/DCL/슬로우쿼리/타임아웃/권한오류
  6. 분기별 DR 리허설(스키마 버전 호환/롤백 확인)

최종 결론

  • SQLx는 “직접 SQL”과 “컴파일 타임 안전성”을 동시에 잡는 선택지입니다.
  • 운영·보안 필수 요소(풀링, TLS, 최소권한, 로그 마스킹, CI 가드레일, 감사)는 본 가이드대로 표준화하면 인젝션·시크릿 노출·성능 사고를 크게 줄일 수 있습니다.
  • 필요 시 SeaORM/SeaQuery 등 생태계 도구로 보완하되, 핵심 쿼리는 query! 기반으로 명시적·감사 가능한 구조를 유지하세요.
728x90
그리드형(광고전용)

댓글