
많은 서버는 데이터베이스를 사용한다.
데이터베이스를 어떻게 사용할 수 있는지 알아보자.
여기서는 PostgreSQL을 사용할 것이다.
PostgreSQL은 객체-관계형(ORDBMS) 데이터베이스의 표준이다.
SONB 타입을 지원하여 NoSQL의 장점을 흡수했고,
PostGIS(지리정보), pgvector(AI 벡터 검색) 등 확장이 무궁무진하다.
데이터 무결성에 있어서도 엄격하며 안정적이기에
가장 복잡한 비즈니스 로직을 가장 안정적인 성능으로 처리할 수 있다.
~/workspace$ mkdir db-connection && cd db-connection ~/workspace/db-connection$ python3 -m venv venv ~/workspace/db-connection$ source venv/bin/activate ~/workspace/db-connection$ # 평소에 설치하던 것 외에 추가된 라이브러리를 놓치지 말자 ~/workspace/db-connection$ pip install maturin fastapi uvicorn orjson ~/workspace/db-connection$ maturin init ~/workspace/db-connection$ # 선택지 중 기본값인 PyO3 선택 ~/workspace/db-connection$ # Cargo.toml과 src/lib.rs가 자동 생성된다 ~/workspace/db-connection$ # Python 코드는 직접 생성해 주어야 한다 ~/workspace/db-connection$ mkdir app && touch app/main.py ~/workspace/db-connection$ # ProgreSQL Docker를 위한 파일도 생성한다 ~/workspace/db-connection$ touch docker-compose.yml .env ~/workspace/db-connection$ tree -a -I venv . ├── .env ├── .github │ └── workflows │ └── CI.yml ├── .gitignore ├── app │ └── main.py ├── Cargo.toml ├── docker-compose.yml ├── pyproject.toml └── src └── lib.rs
Cargo.toml 파일을 열어 라이브러리 이름을 수정해 주겠다.
데이터베이스 연결을 위해서는 sqlx 크레이트가 필요하다.
그리고 데이터베이스 입출력은 비동기로 수행하는 게 효율적이므로
비동기 작업을 위한 tokio 크레이트도 사용한다.
각 크레이트에는 적절한 features 도 설정해 주겠다.
데이터베이스 설정 파일과 계정 정보를 분리하기 위해
환경변수 사용을 위한 dotenvy 크레이트도 사용한다.
로그를 기록하는 것을 배웠으니 tracing 크레이트를 비롯한
로그 크레이트도 추가하여 로그도 남겨 보겠다.
Cargo.toml[package] name = "image-processor" version = "0.1.0" edition = "2024" [lib] name = "rust_engine" crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.0" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "macros"] } tokio = { version = "1.43", features = ["full"] } tracing-appender = "0.2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } dotenvy = "0.15"
docker compose 를 통해 데이터베이스를 관리할 것이다.
현 시점 가장 안정적인 PostgreSQL 버전을 사용하며,
성능 모니터링을 위해 유용한 도구들을 함께 띄운다.
계정 정보는 .env 환경변수 파일에 작성하여
하드웨어 자원 할당과 계정 정보를 분리하여 관리할 것이다.
.env# PostgreSQL POSTGRES_USER=peter POSTGRES_PASSWORD=ku201711424 POSTGRES_DB=rust_python_db POSTGRES_HOST=localhost POSTGRES_PORT=5432 # pgAdmin PGADMIN_EMAIL=admin@pjos.dev PGADMIN_PASSWORD=admin201711424 # SQLX DATABASE_URL=postgre://peter:ku201711424@localhost:5432/rust_python_db실무에서는
.env파일의 내용을 절대 어딘가에 업로드하거나 유출하지 말 것.
...이라는 기본 보안 수칙을 인지하지 못한 채 개발을 하는 바이브코더들이 종종 이슈가 되더라.
당장 Gemini에게 설정 파일 작성하라고 해도.env파일을 따로 빼지 않고
설정 파일에 보안 관련 정보까지 다 집어넣어 버리더라.
데이터베이스 설정에 io_method=worker 커맨드를 추가하여
PostgreSQL 18의 비동기 IO를 사용한다.
또한 io_workers=8 커맨드로 코어 활용을 최적화한다.
데이터의 변화를 시각적으로 확인하기 위해 pgAdmin을 사용한다.
이를 통해 PostgreSQL 18에서 도입된 UUID v7이
실제 시간 순서대로 정렬되어 인덱스 효율을 높이는지 직접 눈으로 확인할 수 있으며,
복잡한 메타데이터(JSON)를 계층 구조로 편하게 분석할 수 있다.
DB의 세션 상태와 I/O 부하를 실시간 그래프로 보며 성능 모니터링을 할 수 있다.
컨테이너 이름은 임의의 문자열을 사용하면 된다.
docker-compose.ymlservices: db: image: postgres:18-alpine container_name: rust_postgres_18 environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} ports: - "${POSTGRES_PORT}:5432" command: - "-c" - "io_method=worker" - "-c" - "io_workers=8" volumes: - ./postgres_data:/var/lib/postgresql restart: always pgadmin: image: dpage/pgadmin4 container_name: pgadmin_ui environment: PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} ports: - "8080:80" depends_on: - db restart: always
PostgreSQL에 연결된 데이터베이스 커넥션 풀을 미리 생성해 놓고
필요할 때 하나씩 꺼내 사용하는 방식으로 구현한다.
데이터베이스 커넥션 풀은 OnceLock 을 이용하여 전역 선언하여
FastAPI 서버가 가동되는 동안 살아있도록 한다.
src/lib.rsuse pyo3::prelude::*; use sqlx::postgres::PgPoolOptions; use sqlx::{Pool, Postgres}; use std::sync::OnceLock; use tokio::runtime::Runtime; use dotenvy::dotenv; use std::env; use tracing::{info, debug, warn, error, instrument}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; static DB_POOL: OnceLock<Pool<Postgres>> = OnceLock::new(); static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new(); fn init_tracing() { let filter_appender = tracing_appender::rolling::hourly("./logs", "server.log"); let (non_blocking, guard) = tracing_appender::non_blocking(filter_appender); let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("info")); let _ = LOG_GUARD.set(guard); tracing_subscriber::registry() .with(filter) .with(fmt::layer().with_thread_ids(true)) // 콘솔 출력 .with(fmt::layer().with_writer(non_blocking).json()) // 파일 저장 .init(); } #[pyfunction] #[instrument] fn init_database() -> PyResult<String> { dotenv().ok(); debug!("환경 변수 로드"); let database_url = env::var("DATABASE_URL") .map_err(|e| { error!("DATABASE_URL 환경 변수를 찾을 수 없음: {}", e); pyo3::exceptions::PyRuntimeError::new_err("DATABASE_URL not found in .env") })?; let rt = Runtime::new().unwrap(); rt.block_on(async { info!("PostgreSQL 연결 풀 생성 시작"); let pool = PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await .map_err(|e| { error!("데이터베이스 연결 풀 생성 실패: {}", e); pyo3::exceptions::PyRuntimeError::new_err(format!("DB 연결 실패: {}", e)) })?; if DB_POOL.set(pool).is_ok() { info!("데이터베이스 연결 풀 성공적으로 초기화"); } else { warn!("데이터베이스 연결 풀이 이미 초기화되어 있음"); } Ok("PostgreSQL 연결 성공 및 Pool 초기화 완료".to_string()) }) } #[pyfunction] #[instrument] fn close_database() -> PyResult<()> { if let Some(pool) = DB_POOL.get() { info!("데이터베이스 연결 종료"); let rt = Runtime::new().unwrap(); rt.block_on(async { pool.close().await; info!("PostgreSQL 연결 풀 안전하게 닫음"); }); } else { warn!("정리할 데이터베이스 연결 풀이 존재하지 않음"); } Ok(()) } #[pyfunction] fn check_connection() -> PyResult<bool> { debug!("데이터베이스 연결 상태 확인"); if let Some(pool) = DB_POOL.get() { let closed = pool.is_closed(); if closed { warn!("데이터베이스 풀이 닫혀 있음"); } else { debug!("데이터베이스 풀 활성화 상태"); } return Ok(!closed); } error!("데이터베이스 연결 풀이 존재하지 않음"); Ok(false) } #[pymodule] fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> { init_tracing(); tracing::info!("Rust Engine 가동 및 Tracing 시스템 초기화 완료"); m.add_function(wrap_pyfunction!(init_database, m)?)?; m.add_function(wrap_pyfunction!(close_database, m)?)?; m.add_function(wrap_pyfunction!(check_connection, m)?)?; Ok(()) }
서버 시작 시 데이터베이스 연결을 자동으로 수행하도록 설정한다.
서버 중단 시 데이터베이스 연결을 안전하게 종료하도록 설정한다.
이 작업은 lifespan 을 통해 작성할 수 있다.
app/main.pyfrom fastapi import FastAPI, HTTPException from fastapi.responses import ORJSONResponse from contextlib import asynccontextmanager import rust_engine import logging logger = logging.getLogger("uvicorn.error") class UTF8ORJSONResponse(ORJSONResponse): media_type = "application/json; charset=utf-8" @asynccontextmanager async def lifespan(app: FastAPI): logger.info("DB 연결 초기화 중...") try: msg = rust_engine.init_database() logger.info(msg) except Exception as e: logger.info(f"DB 연결 오류: {e}") yield # 앱 가동 # [SHUTDOWN] logger.info("서버 종료 감지") try: rust_engine.close_database() logger.info("모든 연결 안전하게 종료") except Exception as e: logger.error(f"종료 중 오류 발생: {e}") app = FastAPI(default_response_class=UTF8ORJSONResponse, lifespan=lifespan) @app.get("/") def read_root(): return { "status": "200", "info": "서버 가동 중입니다." } @app.get("/db-status") def get_db_status(): logger.info("DB 상태 체크 요청") is_alive = rust_engine.check_connection() if is_alive: logger.info("DB 연결 상태 양호") return { "status": "online", "message": "Rust 엔진이 PostgreSQL을 사용합니다." } else: logger.error("DB 연결 끊김 감지") raise HTTPException( status_code=500, detail="DB 연결이 끊겼거나 초기화되지 않았습니다." )
도커 서비스를 실행한다.
~/workspace/db-connection$ docker compose up -d [+] up 31/31 ✔ Image dpage/pgadmin4 Pulled 84.6s ✔ Image postgres:18-alpine Pulled 95.9s ✔ Network db-connection_default Created 0.0s ✔ Container rust_progres_18 Created 0.3s ✔ Container pgadmin_ui Created 0.0s
브라우저를 통해 다음 URL로 접속하여
환경변수에 작성한 pgAdmin 계정으로 로그인하면
데이터베이스 관리를 위한 웹 인터페이스에 접속할 수 있다.
http://localhost:8080
여기서 [Add New Server] 혹은 [새 서버 추가] 버튼을 눌러 데이터베이스 정보를 입력한다.

Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
병렬 처리가 포함된 코드는 성능 최적화를 위해 --release 를 붙여 컴파일한다.
컴파일 후 pip list 명령어를 사용해 보면 Cargo.toml 파일에 작성한 패키지 이름을 확인할 수 있다.
uvicorn 라이브러리를 통해 FastAPI를 실행한다.
~/workspace/db-connection$ maturin develop ~/workspace/db-connection$ uvicorn app.main:app --reload
curl 명령어 또는 브라우저를 통해 다음과 같은 테스트를 해볼 수 있다.
http://127.0.0.1:8000/db-status~$ curl -i http://127.0.0.1:8000 HTTP/1.1 200 OK date: Tue, 07 Apr 2026 01:09:36 GMT server: uvicorn content-length: 53 content-type: application/json; charset=utf-8 {"status":"200","info":"서버 가동 중입니다."}%~$ curl -i http://127.0.0.1:8000/db-status HTTP/1.1 200 OK date: Tue, 07 Apr 2026 01:09:39 GMT server: uvicorn content-length: 77 content-type: application/json; charset=utf-8 {"status":"online","message":"Rust 엔진이 PostgreSQL을 사용합니다."}%INFO: Started reloader process [85335] using StatReload 2026-04-07T01:09:29.646312Z INFO ThreadId(02) rust_engine: Rust Engine 가동 및 Tracing 시스템 초기화 완료 INFO: Started server process [85337] INFO: Waiting for application startup. INFO: DB 연결 초기화 중... 2026-04-07T01:09:29.648668Z INFO ThreadId(02) init_database: rust_engine: PostgreSQL 연결 풀 생성 시작 2026-04-07T01:09:29.688320Z INFO ThreadId(02) init_database: rust_engine: 데이터베이스 연결 풀 성공적으로 초기화 INFO: PostgreSQL 연결 성공 및 Pool 초기화 완료 INFO: Application startup complete. INFO: 127.0.0.1:55419 - "GET / HTTP/1.1" 200 OK INFO: DB 상태 체크 요청 INFO: DB 연결 상태 양호 INFO: 127.0.0.1:55420 - "GET /db-status HTTP/1.1" 200 OK ^CINFO: Shutting down INFO: Waiting for application shutdown. INFO: 서버 종료 감지 2026-04-07T01:09:46.667943Z INFO ThreadId(02) close_database: rust_engine: 데이터베이스 연결 종료 2026-04-07T01:09:46.669614Z INFO ThreadId(02) close_database: rust_engine: PostgreSQL 연결 풀 안전하게 닫음 INFO: 모든 연결 안전하게 종료 INFO: Application shutdown complete. INFO: Finished server process [85337] INFO: Stopping reloader process [85335]
RUST_LOG=debug일 때~$ curl -i http://127.0.0.1:8000 HTTP/1.1 200 OK date: Tue, 07 Apr 2026 01:11:16 GMT server: uvicorn content-length: 53 content-type: application/json; charset=utf-8 {"status":"200","info":"서버 가동 중입니다."}%~$ curl -i http://127.0.0.1:8000/db-status HTTP/1.1 200 OK date: Tue, 07 Apr 2026 01:11:18 GMT server: uvicorn content-length: 77 content-type: application/json; charset=utf-8 {"status":"online","message":"Rust 엔진이 PostgreSQL을 사용합니다."}%INFO: Started reloader process [85375] using StatReload 2026-04-07T01:11:10.113858Z INFO ThreadId(02) rust_engine: Rust Engine 가동 및 Tracing 시스템 초기화 완료 INFO: Started server process [85377] INFO: Waiting for application startup. INFO: DB 연결 초기화 중... 2026-04-07T01:11:10.115043Z DEBUG ThreadId(02) init_database: rust_engine: 환경 변수 로드 2026-04-07T01:11:10.115417Z INFO ThreadId(02) init_database: rust_engine: PostgreSQL 연결 풀 생성 시작 2026-04-07T01:11:10.154654Z INFO ThreadId(02) init_database: rust_engine: 데이터베이스 연결 풀 성공적으로 초기화 INFO: PostgreSQL 연결 성공 및 Pool 초기화 완료 INFO: Application startup complete. INFO: 127.0.0.1:55422 - "GET / HTTP/1.1" 200 OK INFO: DB 상태 체크 요청 2026-04-07T01:11:18.652556Z DEBUG ThreadId(18) rust_engine: 데이터베이스 연결 상태 확인 2026-04-07T01:11:18.652603Z DEBUG ThreadId(18) rust_engine: 데이터베이스 풀 활성화 상태 INFO: DB 연결 상태 양호 INFO: 127.0.0.1:55423 - "GET /db-status HTTP/1.1" 200 OK ^CINFO: Shutting down INFO: Waiting for application shutdown. INFO: 서버 종료 감지 2026-04-07T01:11:25.215181Z INFO ThreadId(02) close_database: rust_engine: 데이터베이스 연결 종료 2026-04-07T01:11:25.215891Z INFO ThreadId(02) close_database: rust_engine: PostgreSQL 연결 풀 안전하게 닫음 INFO: 모든 연결 안전하게 종료 INFO: Application shutdown complete. INFO: Finished server process [85377] INFO: Stopping reloader process [85375]
이어서 스키마 설계 및 CRUB 단계로 넘어갈 예정이다.