Sandcorp샌드코프 · 웹·AI 제품 개발

새 프로젝트 받는 중 · 갱신 2026.11sandcorp.co.krGitHub

← 팁

테스트성능

시험 DB는 마이그레이션해 둔 템플릿에서 복제한다

워커마다 migrate를 돌리지 않는다. 마이그레이션 해시로 이름 지은 템플릿 DB를 한 번 만들고 CREATE DATABASE … TEMPLATE으로 복제하면 7~9초가 1초 미만이 된다.

pytest-django를 xdist로 병렬로 돌리면 워커마다 제 DB를 만들고 마이그레이션을 처음부터 전부 돌린다. 한 번에 약 7.5초이고, 그 시간이 각 워커의 첫 시험 준비에 붙는다. CI 워커가 12개면 합쳐서 90초다.

마이그레이션은 한 번만 돌려 템플릿 DB를 만들고, 워커 DB는 Postgres의 CREATE DATABASE … TEMPLATE으로 복제한다. 복제는 파일 복사라 7~9초 걸리던 준비가 1초 아래로 내려간다.

import hashlib
import pathlib
import psycopg

def template_name() -> str:
    files = sorted(pathlib.Path(".").glob("*/migrations/*.py"))
    digest = hashlib.sha256(b"".join(p.read_bytes() for p in files)).hexdigest()[:12]
    return f"test_tpl_{digest}"

def clone_test_db(admin_dsn: str, target: str) -> None:
    tpl = template_name()
    with psycopg.connect(admin_dsn, autocommit=True) as conn:
        with conn.transaction():
            conn.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (tpl,))
            found = conn.execute(
                "SELECT 1 FROM pg_database WHERE datname = %s", (tpl,)
            ).fetchone()
            if not found:
                build_template(tpl)  # migrate한 뒤 템플릿에 걸린 연결을 모두 닫는다
        conn.execute(f'CREATE DATABASE "{target}" TEMPLATE "{tpl}"')

요령 몇 가지:

  • 이름에 마이그레이션 해시를 넣는다. 스키마가 바뀌면 이름이 바뀌어 저절로 다시 만든다.
  • 누가 만들지는 DB 잠금으로 가른다. 파일 잠금은 호스트와 컨테이너가 서로 못 보지만, pg_advisory_xact_lock은 같은 DB를 보는 모두가 본다. 세션 잠금이 아니라 트랜잭션 잠금을 쓰는 이유는, pgbouncer 트랜잭션 모드 뒤에서는 세션 잠금이 문장마다 다른 연결에 떨어지기 때문이다.
  • 템플릿에 연결이 남아 있으면 복제가 거절된다. 만든 뒤 연결을 닫는다.
  • xdist 컨트롤러에서 미리 만든다(pytest_sessionstart). 워커가 각자 잠금 앞에서 기다리면 그 기다림이 또 첫 시험에 붙는다.
  • 지난 실행이 남긴 템플릿을 그대로 믿지 않는다. 템플릿 COMMENT에 실행 표식을 적어 두고 다르면 다시 만든다. 체크아웃 두 개가 같은 Postgres를 쓰면 이름에 경로 해시도 넣어 서로 지우지 않게 한다.

문의 →