todos 테이블을 생성한 뒤, 간단한 JSON 뷰로 생성, 조회, 수정, 삭제 요청을 처리합니다. 모든 연결에는 전체 인증서 검증을 수행하는 TLS가 적용됩니다.
사전 요구 사항
- Python 3.12 이상(Django 6 요구 사항). 이 가이드는 Python 3.13, Django 6.1.1, psycopg 3.3.6 환경에서 테스트되었습니다.
- ClickHouse Cloud 계정
- 데이터베이스 생성에 사용할
psql. SQL 콘솔에서CREATE DATABASESQL 문을 실행해도 됩니다.
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을 따로 설치할 필요가 없습니다.
연결 구성하기
다운로드한 CA 인증서를 프로젝트 디렉터리의manage.py와 같은 위치로 옮긴 다음, 파일 이름을 ca-certificate.pem으로 변경하십시오.
앱에서 사용할 데이터베이스를 생성하십시오. <PASSWORD>와 호스트는 Connect 모달에 표시된 값으로 바꾸십시오.
mysite/settings.py를 열고 from pathlib import Path 아래에 import os를 추가한 후, DATABASES 설정을 다음과 같이 변경하십시오:
mysite/settings.py
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 오류가 발생하며 연결에 실패합니다.모델 생성 및 마이그레이션 실행
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
실행 및 확인
개발 서버를 시작합니다:PATCH 요청은 업데이트된 행을 반환합니다.
http://localhost:3111/todos를 열면 남은 할 일 목록을 확인할 수 있습니다. 응답은 다음과 같습니다:
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]에는libpq18이 번들로 포함되어 있어 프로토콜 수준의Close메시지로 SQL 문을 해제하기 때문입니다. 반면libpq16 이하로 빌드된 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를 거쳐도 정상적으로 동작합니다.
PORT를 5432로 설정하고 DISABLE_SERVER_SIDE_CURSORS를 제거하십시오. 이 경우 각 Django worker가 자체 Postgres 연결을 유지하므로, 전체 worker 수가 max_connections를 넘지 않도록 하십시오.