Skip to main content
FastAPI는 async Python 웹 프레임워크이고, SQLAlchemy는 가장 널리 쓰이는 Python ORM입니다. 이 가이드에서는 타입이 지정된 SQLAlchemy 모델을 정의하고, Alembic 마이그레이션(migration)으로 테이블을 생성한 뒤, FastAPI로 생성(create), 조회(read), 수정(update), 삭제(delete) 엔드포인트를 제공합니다. 앱은 asyncpg 드라이버를 사용하며, 모든 연결에는 TLS와 함께 전체 인증서 검증(certificate verification)이 적용됩니다.

사전 요구 사항

  • uv 및 Python 3.11 이상. 이 가이드는 Python 3.13, FastAPI 0.142, SQLAlchemy 2.1, asyncpg 0.31, Alembic 1.20, Uvicorn 0.54 환경에서 테스트되었습니다.
  • ClickHouse Cloud 계정
  • 데이터베이스 생성에 사용할 psql. SQL 콘솔에서 CREATE DATABASE SQL 문을 실행해도 됩니다.

ClickHouse Managed Postgres 서비스 생성

ClickHouse Cloud 콘솔에서 New service를 클릭한 후 Postgres를 선택하십시오. 몇 분 후 인스턴스가 준비됩니다. 단계별 안내는 빠른 시작 가이드를 참조하십시오.

연결 정보 확인하기

서비스의 왼쪽 사이드바에서 Connect를 클릭하십시오. 모달에 사용자 이름, 비밀번호, 서버, 포트가 표시되며, Directly / via PgBouncer 토글로 연결 방식을 전환할 수 있습니다. 이 가이드에서는 두 가지 연결을 모두 사용합니다.
  • PgBouncer (포트 6432): 애플리케이션에서 사용합니다. SQLAlchemy는 각 Uvicorn worker마다 연결 풀을 유지하며, 기본 제공되는 PgBouncer를 사용하면 여러 worker와 레플리카가 적은 수의 Postgres 연결을 공유할 수 있습니다. PgBouncer는 트랜잭션 풀링(transaction pooling) 모드로 실행됩니다.
  • Direct (포트 5432): Alembic에서 사용합니다. 마이그레이션은 DDL을 실행하고, 실행 시간이 길어질 수 있으며, 세션 상태에 의존할 수도 있으므로 Postgres에 직접 연결해야 합니다.
Use SSL을 켜고 Download CA certificate를 클릭하십시오. Settings → CA Certificate에서도 다운로드할 수 있습니다. 이 인증서는 인스턴스마다 고유하므로, 드라이버는 이 인증서로 실제 사용자의 서버와 통신하고 있는지 검증할 수 있습니다. Use SSL을 켜면 모달의 URL이 sslmode=verify-full&sslrootcert=...로 끝납니다. asyncpg는 이 매개변수들을 읽지 않으므로 모달에서는 비밀번호와 호스트만 복사하십시오. CA 인증서는 애플리케이션 코드에서 직접 전달합니다(연결 구성 참조).

프로젝트 설정

프로젝트를 생성한 후 FastAPI, Uvicorn, asyncio 엑스트라를 포함한 SQLAlchemy, asyncpg, Alembic을 설치하십시오.
기존 앱에 추가하시나요?uv init 및 mkdir 명령어는 건너뛰십시오. 프로젝트에서 uv add "sqlalchemy[asyncio]" asyncpg alembic을 실행한 다음 연결 구성부터 진행하세요. 앱에서 async SQLAlchemy로 SQLite를 사용하고 있다면 다음과 같이 하세요.
  • create_async_engine 호출을 app/db.py의 호출과 동일하게 변경하세요. migrations/env.py에서는 기존 모듈에서 Base와 모델을 가져오세요.
  • 시작 코드에서 Base.metadata.create_all을 제거하세요. Alembic은 Postgres에 기존 테이블을 생성하지만, SQLite의 데이터까지 복사하지는 않습니다.

연결 구성

다운로드한 CA 인증서를 프로젝트 디렉터리로 옮긴 후 파일 이름을 ca-certificate.pem으로 변경하십시오. 앱에서 사용할 데이터베이스를 생성하십시오. <PASSWORD>와 호스트를 Connect 모달에 표시된 값으로 바꾸십시오.
프로젝트 루트에 .env 파일을 생성하거나 기존 .env 파일에 다음 변수를 추가하십시오. 두 URL은 포트만 다르며, 둘 다 postgresql+asyncpg 스킴을 사용하고 SSL 매개변수는 포함하지 않습니다.
.env
DATABASE_CA_CERT는 명령어를 실행하는 디렉터리를 기준으로 한 상대 경로로 해석됩니다. 비밀번호에 @, /, # 같은 특수 문자가 포함되어 있다면 URL 인코딩하십시오. .env 파일은 버전 관리 대상에서 제외하십시오. app/db.py 파일을 생성하십시오. 이 파일에서는 TLS 설정과 엔진, 그리고 앱 전체에서 공유하는 세션 팩토리를 구성합니다.
app/db.py
PROTOCOL_TLS_CLIENT로 생성한 ssl.SSLContext는 유효한 인증서를 요구하고 호스트명을 검사하므로 sslmode=verify-full과 동일하게 동작합니다. SQLAlchemy는 이 컨텍스트를 connect_args를 통해 asyncpg에 전달합니다.
CA 인증서는 connect_args로 전달하십시오
  • ssl 인수를 지정하지 않으면 asyncpg는 서버가 TLS를 지원할 때 TLS를 사용하지만, 인증서는 검증하지 않습니다.
  • Connect 모달에 표시된 sslmode나 sslrootcert를 URL에 복사하지 마십시오. SQLAlchemy는 URL 매개변수를 asyncpg에 키워드 인수로 전달하므로, TypeError: connect() got an unexpected keyword argument 'sslmode' 오류와 함께 연결이 실패합니다.
  • Python 3.13 이상에서는 ssl.create_default_context 함수가 엄격한 X.509 검사를 활성화하므로, 이 함수로 만든 컨텍스트를 사용하면 certificate verify failed: Missing Authority Key Identifier 오류가 발생합니다. 예시와 같이 ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)로 컨텍스트를 생성하십시오.
  • CA 인증서가 잘못되면 certificate verify failed: unable to get local issuer certificate 오류와 함께 연결이 실패합니다. IP 주소로 연결하는 경우처럼 호스트명이 일치하지 않으면 certificate verify failed: IP address mismatch 오류가 발생합니다.
TLS 설정을 URL에 그대로 두려는 경우나 앱에서 동기식 SQLAlchemy 세션을 사용하는 경우에는 asyncpg 대신 psycopg 드라이버를 사용하십시오. psycopg는 libpq를 기반으로 하며, create_engine과 create_async_engine 모두에서 Connect 모달의 매개변수를 그대로 사용할 수 있습니다. 예: postgresql+psycopg://...:5432/guide_fastapi?sslmode=verify-full&sslrootcert=ca-certificate.pem

모델 정의

타입이 지정된 SQLAlchemy 모델을 포함하는 app/models.py 파일을 생성하세요:
app/models.py

마이그레이션 실행

async 템플릿으로 Alembic 환경을 생성합니다:
생성된 migrations/env.py의 내용을 다음으로 교체하십시오. 새 버전은 autogenerate에 사용할 모델을 로드하고, 앱과 동일한 TLS 설정으로 Postgres에 직접 연결합니다:
migrations/env.py
env.py에서 엔진을 직접 생성하므로 Alembic은 alembic.ini의 플레이스홀더 값인 sqlalchemy.url을 무시합니다. 모델에서 마이그레이션을 생성한 다음 적용하세요:
migrations/versions에 생성된 파일을 검토한 후 코드와 함께 커밋하십시오. Alembic은 적용된 revision을 alembic_version 테이블에 기록합니다. 모델을 변경할 때마다 revision --autogenerate와 upgrade head를 다시 실행하십시오.

API 엔드포인트 추가

app/main.py를 생성하십시오. 각 요청은 FastAPI 의존성(dependency)을 통해 각자의 AsyncSession을 할당받으며, 애플리케이션이 종료되면 엔진의 풀이 닫힙니다.
app/main.py
SQLAlchemy는 각 세션의 SQL 문을 하나의 트랜잭션(transaction) 안에서 실행하며, 이 트랜잭션은 session.commit이 호출될 때 종료됩니다. 세션이 닫히면 커밋되지 않은 작업은 모두 롤백됩니다. INSERT 시 SQLAlchemy는 RETURNING으로 생성된 id와 created_at을 다시 읽어 오므로 create_todo에서 별도의 쿼리를 실행할 필요가 없습니다.

실행 및 확인

애플리케이션을 시작합니다:
두 번째 터미널에서 할 일(todo) 2개를 생성한 후, 첫 번째 항목은 완료로 표시하고 두 번째 항목은 삭제하세요:
DELETE 요청은 204 No Content를 반환합니다. 존재하지 않는 todo를 요청하면 404가 반환됩니다.
브라우저에서 http://localhost:3112/todos를 열어 남은 할 일 목록을 확인하거나, http://localhost:3112/docs를 열어 FastAPI의 대화형 API 문서에서 엔드포인트를 직접 호출해 보십시오. 직접 연결(direct connection)에 검증된 TLS가 사용되는지 확인하려면 앱과 동일한 TLS 설정으로 pg_stat_ssl을 쿼리하십시오.
콘솔에서 데이터를 확인하려면 서비스의 왼쪽 사이드바에서 SQL 콘솔을 열고 guide_fastapi와 public을 차례로 펼친 다음 todos 테이블을 클릭하십시오. 바로 옆에 있는 alembic_version 테이블에는 마이그레이션 이력이 저장되어 있습니다. todos 테이블에는 남은 행이 다음과 같이 들어 있습니다. Ctrl+C를 눌러 앱을 중지하십시오.

PgBouncer 사용하기

앱은 asyncpg와 SQLAlchemy의 기본 설정을 그대로 사용해도 PgBouncer를 통해 정상적으로 동작합니다.
  • asyncpg는 모든 쿼리를 이름이 지정된 프로토콜 수준의 prepared statement로 실행하며, SQLAlchemy는 연결당 최대 100개까지 이를 캐시합니다. 번들로 제공되는 PgBouncer는 트랜잭션 풀링 모드(transaction pooling mode)에서 이러한 SQL 문을 지원하며, asyncpg는 SQL DEALLOCATE 대신 프로토콜 수준의 Close 메시지로 이를 해제합니다. 따라서 statement_cache_size=0이나 사용자 정의 prepared_statement_name_func를 설정할 필요가 없습니다. 그럼에도 asyncpg의 SQL 문 캐시를 끄려는 경우에는 URL에 prepared_statement_cache_size=0도 함께 추가하여 SQLAlchemy의 캐시도 끄십시오.
  • search_path처럼 SET으로 지정한 세션 설정은 다음 트랜잭션으로 유지되지 않으며, 다른 클라이언트로 유출될 수 있습니다. 대신 트랜잭션 내부에서 SET LOCAL을 사용하십시오.
  • pg_stat_ssl은 PgBouncer와 Postgres 간의 연결 정보를 보고하므로, 포트 6432를 통해 접속하면 위의 TLS 확인 결과로 (False, None)이 반환됩니다. 그래도 앱과 PgBouncer 간의 연결은 여전히 검증되므로, 잘못된 CA 인증서를 사용하면 마찬가지로 실패합니다.
  • Alembic은 어드바이저리 락(advisory lock)을 사용하지 않고 각 upgrade를 하나의 트랜잭션에서 실행하므로, PgBouncer를 통해서도 동작합니다. 다만 시간이 오래 걸리는 마이그레이션에는 직접 연결을 사용하는 것이 좋습니다.
컬럼 유형 변경하기PgBouncer는 Postgres 연결에 prepared statement를 유지합니다. 마이그레이션으로 쿼리가 반환하는 컬럼의 유형이 바뀌면(예: integer에서 bigint로), PgBouncer가 Postgres 연결을 교체하기 전까지 해당 쿼리는 새 연결에서도 PgBouncer를 거칠 때 InvalidCachedStatementError: cached statement plan is invalid due to a database schema or configuration change 오류와 함께 실패합니다. 이를 복구하려면 앱과 동일한 사용자로 직접 연결을 통해 접속한 뒤, 해당 데이터베이스에 대한 PgBouncer의 Postgres 연결을 종료하십시오. PgBouncer는 Postgres에 로컬로 연결하므로 해당 연결에는 client_addr가 없으며, 따라서 이 쿼리는 직접 연결에는 영향을 주지 않습니다. 이후 PgBouncer가 새 연결을 열며, 풀에 있는 각 앱 연결은 SQLAlchemy가 캐시를 갱신하기 전까지 한 번 더 실패할 수 있습니다.
컬럼을 추가할 때는 이 문제가 발생하지 않습니다. SQLAlchemy가 조회할 컬럼을 명시적으로 나열하기 때문입니다.

다음 단계

  • 연결: 연결 문자열(connection string), PgBouncer, TLS
  • 설정: max_connections 등 Postgres 및 PgBouncer 매개변수
  • 읽기 레플리카: 두 번째 엔진을 사용해 읽기 전용 쿼리를 레플리카로 전송
  • 보안: IP 액세스 목록 및 프라이빗 네트워킹
마지막 수정일 2026년 10월 14일