데이터베이스 연결

Pt J·어제
post-thumbnail

데이터베이스 연결

많은 서버는 데이터베이스를 사용한다.
데이터베이스를 어떻게 사용할 수 있는지 알아보자.

여기서는 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 파일

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.yml

services:
  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

Rust 코드

PostgreSQL에 연결된 데이터베이스 커넥션 풀을 미리 생성해 놓고
필요할 때 하나씩 꺼내 사용하는 방식으로 구현한다.
데이터베이스 커넥션 풀은 OnceLock 을 이용하여 전역 선언하여
FastAPI 서버가 가동되는 동안 살아있도록 한다.

src/lib.rs

use 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(())
}

Python 코드

서버 시작 시 데이터베이스 연결을 자동으로 수행하도록 설정한다.
서버 중단 시 데이터베이스 연결을 안전하게 종료하도록 설정한다.
이 작업은 lifespan 을 통해 작성할 수 있다.

app/main.py

from 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 단계로 넘어갈 예정이다.

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글