Skip to main content
Django는 ORM과 마이그레이션(migration) 시스템을 내장한 Python 웹 프레임워크로, psycopg 3 드라이버를 사용해 PostgreSQL과 통신합니다. 이 가이드에서는 새 Django 프로젝트를 PgBouncer를 거쳐 ClickHouse Managed Postgres에 연결하고, 마이그레이션으로 todos 테이블을 생성한 뒤, 간단한 JSON 뷰로 생성, 조회, 수정, 삭제 요청을 처리합니다. 모든 연결에는 전체 인증서 검증을 수행하는 TLS가 적용됩니다.

사전 요구 사항

  • Python 3.12 이상(Django 6 요구 사항). 이 가이드는 Python 3.13, Django 6.1.1, psycopg 3.3.6 환경에서 테스트되었습니다.
  • ClickHouse Cloud 계정
  • 데이터베이스 생성에 사용할 psql. SQL 콘솔에서 CREATE DATABASE SQL 문을 실행해도 됩니다.

ClickHouse Managed Postgres 서비스 생성

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

연결 정보 확인하기

서비스의 왼쪽 사이드바에서 Connect를 클릭하세요. 모달에 사용자 이름, 비밀번호, 서버, 포트가 표시되며, Directly / via PgBouncer 토글도 있습니다. 이 가이드에서는 번들로 제공되는 PgBouncer를 통해 포트 6432로 Django를 연결합니다. 프로덕션 환경의 Django 앱은 보통 여러 머신에서 여러 개의 worker 프로세스를 실행하며, 각 프로세스가 개별적으로 연결을 엽니다. PgBouncer는 트랜잭션 풀링 모드(transaction pooling mode)로 동작하며, 이 모든 프로세스가 소수의 Postgres 연결을 공유하도록 해 줍니다. 트랜잭션 풀링을 사용하려면 Django에 몇 가지 설정이 필요하며, 이 가이드에는 해당 설정이 포함되어 있습니다. 자세한 설명은 Django용 PgBouncer 설정을 참고하세요. 풀링된 연결 정보를 확인하려면 via PgBouncer를 선택하세요. 포트가 6432로 바뀌고, 연결 문자열에 sslmode=verify-full이 포함됩니다. 모달에서 Download CA certificate를 클릭하세요. Settings → CA Certificate에서도 다운로드할 수 있습니다. 이 인증서는 인스턴스마다 고유하므로, 드라이버는 이를 사용해 실제 서버와 통신하고 있는지 검증할 수 있습니다.

프로젝트 설정

가상 환경이 포함된 프로젝트 디렉터리를 만들고 Django와 psycopg 3을 설치한 다음, todos 앱이 포함된 Django 프로젝트를 생성하세요:
psycopg[binary]에는 최신 libpq가 포함되어 있으므로 로컬에 PostgreSQL을 따로 설치할 필요가 없습니다.
기존 앱에 추가하는 경우위의 명령어는 건너뛰십시오. 프로젝트의 가상 환경에서 pip install "psycopg[binary]"를 실행한 다음, 연결 구성 단계로 진행하여 기존 SQLite DATABASES 설정을 대체하세요. SQLite 데이터를 옮기려면 다음과 같이 진행하세요.
  • DATABASES를 변경하기 전에 python manage.py dumpdata --natural-foreign --natural-primary -e contenttypes -e auth.permission -o data.json을 실행하세요.
  • python manage.py migrate를 실행한 후 python manage.py loaddata data.json을 실행하세요.

연결 구성하기

다운로드한 CA 인증서를 프로젝트 디렉터리의 manage.py와 같은 위치로 옮긴 다음, 파일 이름을 ca-certificate.pem으로 변경하십시오. 앱에서 사용할 데이터베이스를 생성하십시오. <PASSWORD>와 호스트는 Connect 모달에 표시된 값으로 바꾸십시오.
호스트와 비밀번호가 코드에 포함되지 않도록 환경 변수로 설정하십시오:
mysite/settings.py를 열고 from pathlib import Path 아래에 import os를 추가한 후, DATABASES 설정을 다음과 같이 변경하십시오:
mysite/settings.py
Django는 OPTIONS의 키를 psycopg에 전달하고, psycopg는 이를 다시 libpq에 넘깁니다. sslmode를 verify-full로 설정하면 libpq는 서버 인증서가 서비스의 CA로 서명되었는지, 그리고 호스트명과 일치하는지 검사합니다. BASE_DIR을 사용하면 인증서 경로가 절대 경로가 되므로, 어느 디렉터리에서 Django를 시작하든 설정이 정상적으로 동작합니다.
sslrootcert가 잘못된 CA를 가리키면 SSL error: certificate verify failed 오류가 발생하며 연결에 실패합니다. sslrootcert를 지정하지 않으면 libpq가 기본 CA 위치로 폴백하므로 root certificate file ".../.postgresql/root.crt" does not exist 오류가 발생하며 연결에 실패합니다. 호스트명 대신 IP 주소로 연결하면 server certificate for "..." does not match host name 오류가 발생하며 연결에 실패합니다.
앱에서 dj-database-url을 사용해 DATABASE_URL을 읽는다면, URL 쿼리 문자열에 포함된 sslmode와 sslrootcert가 OPTIONS로 전달됩니다. 동일한 설정을 적용하려면 dj_database_url.config(conn_max_age=600, conn_health_checks=True, disable_server_side_cursors=True)를 호출하세요. URL에 상대 경로로 지정한 sslrootcert는 Django를 시작한 디렉터리를 기준으로 해석됩니다.

모델 생성 및 마이그레이션 실행

mysite/settings.py의 INSTALLED_APPS에 todos 앱을 추가합니다:
mysite/settings.py
todos/models.py에 Todo 모델을 정의합니다:
todos/models.py
모델에 대한 마이그레이션을 생성하고 모든 마이그레이션을 적용합니다:
migrate는 todos_todo 외에도 auth_user, django_session 등 Django 기본 제공 앱의 테이블을 생성하고, 적용된 마이그레이션을 django_migrations에 기록합니다. 기존 앱에서는 todos 모델 단계를 건너뛰고 python manage.py migrate만 실행하십시오. 이 명령으로 앱의 모든 테이블이 새 데이터베이스에 생성됩니다. migrate는 세션 수준의 권고 잠금(advisory lock)을 사용하지 않으므로 PgBouncer를 거쳐도 정상적으로 동작합니다. 다만 동시 실행을 방지하지는 않으므로, 하나의 배포 단계처럼 한 곳에서만 마이그레이션을 실행하십시오.

JSON 엔드포인트 추가

todos/views.py의 내용을 Django ORM으로 할 일(todo)을 생성, 조회, 수정, 삭제하는 두 개의 뷰로 교체하십시오.
todos/views.py
/todos와 /todos/<id>를 뷰로 라우팅하도록 mysite/urls.py의 내용을 다음과 같이 바꾸십시오:
mysite/urls.py

실행 및 확인

개발 서버를 시작합니다:
두 번째 터미널에서 할 일 2개를 생성하고, 첫 번째 할 일은 완료로 표시한 다음 두 번째 할 일은 삭제하세요:
PATCH 요청은 업데이트된 행을 반환합니다.
브라우저에서 http://localhost:3111/todos를 열면 남은 할 일 목록을 확인할 수 있습니다. 응답은 다음과 같습니다:
콘솔에서 데이터를 확인하려면 서비스의 왼쪽 사이드바에서 SQL 콘솔을 열고 guide_django와 public을 차례로 펼친 다음 todos_todo 테이블을 클릭하십시오. 테이블에는 다음과 같이 남은 행이 표시됩니다.

Django를 위한 PgBouncer 설정

PgBouncer는 트랜잭션 풀링 모드(transaction pooling mode)로 동작합니다. 즉, 각 트랜잭션 또는 트랜잭션 외부의 개별 SQL 문은 서로 다른 Postgres 연결에서 실행될 수 있습니다. 이 동작이 위의 설정과 기타 Django 기능에 미치는 영향은 다음과 같습니다.
  • DISABLE_SERVER_SIDE_CURSORS: QuerySet.iterator는 서버 측 커서를 통해 행을 청크 단위로 읽습니다. 트랜잭션 외부에서는 Django가 커서를 WITH HOLD로 선언하고 청크를 하나씩 따로 가져오므로, PgBouncer를 거치면 가져오기 요청이 해당 커서가 없는 Postgres 연결로 전달되어 cursor "_django_curs_..." does not exist 오류와 함께 실패할 수 있습니다. 이 문제는 다른 클라이언트가 동시에 풀을 사용할 때만 발생하므로 개발 환경에서는 드러나지 않는 경우가 많습니다. 이 설정을 켜면 .iterator()는 일반 커서를 사용하며, psycopg가 전체 결과를 메모리에 로드합니다. 매우 큰 테이블을 스트리밍해야 한다면 이 설정을 끈 상태에서 포트 5432의 직접 연결로 해당 코드를 실행하십시오.
  • CONN_MAX_AGE: 기본값(0)에서는 Django가 요청마다 새 연결을 엽니다. TLS 연결은 PgBouncer를 거치든 직접 연결하든 연결을 여는 데 여러 번의 네트워크 왕복이 필요하므로, 연결을 재사용하면 요청 속도가 크게 빨라집니다. 노트북에서 4개의 Gunicorn worker로 테스트한 결과, CONN_MAX_AGE를 600으로 설정하자 요청 시간 중앙값이 약 670 ms에서 95 ms로 줄었습니다. PgBouncer를 거치면 열려 있는 Django 연결이 트랜잭션 사이에 Postgres 연결을 점유하지 않으므로, 영구 연결을 사용해도 max_connections가 소진되지 않습니다. CONN_HEALTH_CHECKS를 사용하면 Django가 각 요청을 시작할 때 재사용하는 연결을 확인하고, 연결이 끊어졌으면 다시 연결합니다.
  • 연결 풀(Connection pool): Django의 내장 풀(OPTIONS의 "pool": True, pip install "psycopg[binary,pool]" 필요)도 비슷한 속도 향상 효과가 있으며 PgBouncer를 거쳐도 동작합니다. 내장 풀을 사용하려면 CONN_MAX_AGE를 0으로 설정해야 하며, 프로세스당 최소 4개의 연결이 항상 열려 있습니다. PgBouncer를 사용한다면 대부분의 앱은 CONN_MAX_AGE만으로 충분하며, 스레드 기반 서버나 ASGI 서버에서는 풀 사용을 고려하십시오.
  • Prepared statement: 별도의 설정이 필요하지 않습니다. Django는 기본적으로 psycopg의 prepare_threshold를 None으로 설정하고 쿼리 매개변수를 클라이언트에서 바인딩하므로 서버 측 prepared statement를 생성하지 않습니다. OPTIONS에서 "server_side_binding": True를 켜고 "prepare_threshold"를 설정하는 경우에도 prepared statement는 PgBouncer를 거쳐 정상적으로 동작합니다. psycopg[binary]에는 libpq 18이 번들로 포함되어 있어 프로토콜 수준의 Close 메시지로 SQL 문을 해제하기 때문입니다. 반면 libpq 16 이하로 빌드된 psycopg에서는 트랜잭션 내부에서 SQL 문을 해제할 때 prepared statement "_pg3_0" does not exist 오류가 발생합니다. 버전을 확인하려면 python -c "import psycopg; print(psycopg.pq.version())"를 실행하십시오. 결과가 170000 이상이어야 합니다.
  • 세션 상태(Session state): SET search_path와 같은 SET 문은 해당 문을 실행한 Postgres 연결에만 적용됩니다. 따라서 이후 쿼리에는 반영되지 않을 수 있으며, 오히려 다른 클라이언트에 반영될 수도 있습니다. PgBouncer는 Django의 "options": "-c search_path=..." 설정을 unsupported startup parameter in options: search_path 오류와 함께 거부합니다. 대신 ALTER DATABASE guide_django SET search_path = myschema, public;처럼 데이터베이스 수준에서 기본값을 설정하십시오. SET ROLE을 실행하는 assume_role 옵션은 사용하지 마십시오. Django 자체의 TIME_ZONE 처리와 isolation_level 옵션은 PgBouncer를 거쳐도 정상적으로 동작합니다.
PgBouncer 대신 직접 연결하려면 PORT를 5432로 설정하고 DISABLE_SERVER_SIDE_CURSORS를 제거하십시오. 이 경우 각 Django worker가 자체 Postgres 연결을 유지하므로, 전체 worker 수가 max_connections를 넘지 않도록 하십시오.

다음 단계

마지막 수정일 2026년 10월 14일