다섯 개의 에이전트와 하나의 개발 데이터베이스: Postgres 18을 이용해 워크트리별로 클론된 DB 구축하기
요약
본 글은 여러 에이전트와 워크트리가 공유하는 단일 데이터베이스 환경에서 발생하는 개발 문제를 다룹니다. Postgres 18의 기능을 활용하여 각 워크트리별로 클론된 독립적인 DB를 효율적으로 구축하고 관리하는 방법을 제시합니다.
핵심 포인트
- 워크트리는 파일을 격리하지만, 데이터베이스는 그렇지 않아 충돌이 발생한다.
- 공유 DB 환경에서 마이그레이션이나 시드 스크립트는 다른 에이전트에 영향을 준다.
- Postgres 18은 워크트리별 클론 DB 구축을 비용 효율적으로 가능하게 한다.
- 도서: Database Playbook
- 저자 소개: Thinking in Go (2권 시리즈) — Go 프로그래밍 완벽 가이드 + Go에서의 육각형 아키텍처
- 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구와 함께 배포하는 개발자를 위한 IDE
- 저자: xgabriel.com | GitHub
../app-auth에 있는 하나의 에이전트는 users.email을 users.login_email로 이름을 변경하고 있습니다. 또 다른 에이전트는 ../app-invoices에 있으며 테스트 스위트를 실행하려 합니다. 각 워크트리는 자체 브랜치, 자체 파일, 그리고 자체 node_modules를 가지고 있습니다. 두 .env 파일 모두 동일한 app_dev 데이터베이스를 가리킵니다. 마이그레이션(migration)을 적용하자 인보이스 테스트가 column users.email does not exist 오류로 실패하고, 두 번째 에이전트는 애초에 문제가 없었던 코드를 수정하기 시작합니다.
워크트리는 각 에이전트에게 코드의 사본을 제공했습니다. 하지만 데이터베이스는 그렇지 않았고, 일반적으로 공유되는 하나의 Postgres를 사용하는 개발 환경에서는 더욱 그렇습니다. 예전 방식은 브랜치당 하나의 데이터베이스였는데, 이는 매번 시드된(seeded) 데이터를 완전히 복사해야 함을 의미했습니다. Postgres 18은 해당 파일 시스템에서 이러한 복사를 거의 무료에 가깝게 만듭니다. 아래는 각 워크트리에 자체 클론 데이터베이스를 제공하고 브랜치 작업이 완료되면 정리하는 간단한 스크립트입니다.
워크트리는 파일을 격리할 뿐, 데이터베이스는 그렇지 않다
git worktree는 하나의 리포지토리가
마이그레이션(migration)은 공유 리소스에 대한 스키마 변경입니다. 테이블을 비우는 시드 스크립트(seed script)는 다른 에이전트의 픽스처(fixtures)를 지워버리고, 행 개수를 세는 테스트는 누군가 데이터를 삽입했기 때문에 불안정해집니다. 결과적으로 잘못된 워크트리에서 빨간색 테스트가 발생하고, 이 문제를 기꺼이 해결하려 드는 에이전트가 생깁니다.
각 워크트리별로 별도의 Postgres 서버를 자체 포트로 실행할 수 있습니다. 이것은 작동하지만, 에이전트당 서버 프로세스와 전체 데이터 복사본을 비용으로 지불해야 합니다. 대신, 하나의 서버에서 각 워크트리별로 데이터베이스를 두는 것이 더 가볍습니다. 단지 데이터베이스를 만드는 비용이 저렴하다면 말이죠.

별도의 체크아웃, 하나의 공유 데이터베이스: 한 워크트리의 마이그레이션은 다른 워크트리의 테스트를 망가뜨립니다.
Postgres 18에서 변경된 사항
CREATE DATABASE new TEMPLATE old는 항상 데이터베이스를 복사해 왔습니다. Postgres 15부터 기본 STRATEGY는 WAL_LOG이며, 이는 템플릿을 블록 단위로 복사하고 각 블록을 쓰기-앞(write-ahead) 로그에 기록합니다. CREATE DATABASE 문서에서는 이를
Postgres 18은 2025년 9월 25일에 출시되었으며, file_copy_method라는 서버 변수(Nazir Bilal Yavuz 기여)를 추가했습니다. 기본값은 copy입니다. 이 값을 clone으로 설정하면, 설정 문서에 따르면 FILE_COPY는 Linux 및 FreeBSD에서 copy_file_range()를, macOS에서는 copyfile을 사용하여 "커널이 디스크 블록을 공유할 기회를 얻게 합니다." Copy-on-write 파일 시스템의 경우 새로운 데이터베이스가 템플릿의 블록을 가리키며, 두 데이터베이스는 어느 한쪽에서 쓰기 작업이 발생할 때만 분기됩니다.
세 가지 요소를 모두 같은 세션 내에서 사용해야 합니다:
SET file_copy_method = clone;
CREATE DATABASE wt_agent_auth
TEMPLATE app_template
...
STRATEGY = FILE_COPY를 생략하면 WAL_LOG가 적용되어 file_copy_method를 완전히 무시합니다. 이 설정은 user 컨텍스트를 가지며 재시작이 필요 없으므로(postgresqlco.nf), 단순한 SET 명령만으로 충분합니다. postgresql.conf 파일을 수정할 필요가 없습니다.
실제 수치가 사용자가 신경 써야 하는 이유입니다. Radim Marek은 WAL_LOG를 사용할 경우 6,289 MB 데이터베이스 복제에 67,000ms가 걸린 반면, FILE_COPY와 clone을 함께 사용했을 때는 212ms 만에 완료했습니다. Ruohang Feng은 NVMe 환경에서 120 GB 데이터베이스를 약 200ms 만에 복제했다고 보고했으며, 797 GB의 경우 약 569ms가 걸렸습니다. 이 수치는 해당 장비 기준입니다. 스크립트 섹션에서는 사용자가 직접 자신의 성능을 측정하는 방법을 보여줍니다.

클론은 누군가 쓰기 작업을 할 때까지 템플릿의 블록을 공유합니다. 변경된 블록만 자체 복사본을 가집니다.
신뢰하기 전에 파일 시스템을 확인하세요
클론은 Copy-on-Write 파일 시스템(macOS의 APFS, reflinks를 사용한 XFS, Btrfs 또는 ZFS(블록 클로닝 활성화된 OpenZFS 2.2+ 기준, Feng 참조)에서만 블록을 공유합니다. Christophe Pettus는 ext4의 경우 명확하게 다음과 같이 언급했습니다: "ext4에는 reflink할 것이 없습니다."
ext4에서 발생하는 일은 출처마다 의견이 다릅니다. Pettus는 clone이 "조용히 복사로 폴백(fallback)하는 대신 실패한다"고 작성했고, Feng의 표는 ext4가 "전통적인 복사로 폴백한다"고 합니다. Postgres 코드는 자체적인 폴백 메커니즘이 없습니다: clone_file()은 시스템 호출에 실패하면 오류를 발생시키고, Linux 루프의 주석에는 커널이 "느린 복사로 폴백할 수 있다"는 내용이 있습니다. macOS에서는 COPYFILE_CLONE_FORCE 플래그를 전달하는데, 이는 Apple의 copyfile man page에 따르면 "클로닝 실패 시 오류를 반환한다"고 합니다. 어떤 방식으로 설정하든, reflinks가 없는 파일 시스템은 아무것도 절약해주지 못합니다. 빌드하기 전에 확인하세요.
중요한 파일 시스템은 레포지토리(repo) 아래의 것이 아니라 Postgres 데이터 디렉터리 아래에 있는 것입니다. 만약 Postgres가 컨테이너 내에서 실행된다면, 컨테이너 내부에서 검사를 수행해야 합니다.
dir=$(psql -d postgres -Atc 'SHOW data_directory')
# Linux: xfs, btrfs 또는 zfs가 좋습니다; ext4는 안 됩니다
...
SHOW data_directory는 superuser 권한이나 pg_read_all_settings를 가진 역할이 필요합니다. 만약 Postgres가 file_copy_method의 값으로 clone을 거부한다면, 빌드는 클론 지원이 없습니다: 해당 옵션을 추가한 커밋은 플랫폼이 copy_file_range 또는 클론 지원을 가진 copyfile을 가질 때만 해당 옵션을 컴파일합니다.
스크립트: 워크트리별 하나의 데이터베이스
wt-db는 세 가지 명령어를 가지고 있습니다. template은 마이그레이션(migrations)과 시드(seed)를 사용하여 app_template을 구축한 다음 잠급니다. clone <worktree>는 해당 worktree의 브랜치 이름으로 데이터베이스를 생성하고, 그 URL을 worktree의 .env 파일에 기록합니다. drop <worktree>는 이를 제거합니다.
#!/usr/bin/env bash
# wt-db: one cloned Postgres 18 database per git worktree.
# Uses PGHOST/PGUSER/PGPORT like psql does. MIGRATE is the
...
이 선택들 중 일부는 의도적인 것입니다.
템플릿은 매일 작업하는 app_dev가 아니라 자체 데이터베이스입니다. CREATE DATABASE는 "시작할 때 다른 연결이 존재하면 실패합니다" (docs), 그리고 사용자가 사용하는 데이터베이스에는 항상 무언가가 연결되어 있습니다. ALLOW_CONNECTIONS false는 새로운 세션을 차단하지만, 플래그를 false로 설정한다고 해서 기존 세션이 종료되는 것은 아닙니다 (template docs). 따라서 clone은 먼저 남아있는 모든 잔여 연결을 종료합니다. IS_TEMPLATE true는 소유자뿐만 아니라 CREATEDB 권한을 가진 모든 역할이 이를 클론할 수 있게 합니다 (ALTER DATABASE).
이름은 브랜치에서 가져옵니다: 소문자로 변환하고, 문자나 숫자가 아닌 모든 문자를 _로 변경하며, 전체 이름이 Postgres의 63바이트 식별자 제한 내에 유지되도록 잘립니다. 더 긴 이름은 NOTICE 이상의 알림 없이 잘리게 되는데, 이로 인해 두 개의 긴 브랜치 이름이 하나의 데이터베이스를 가리키게 되고, 한 worktree의 데이터베이스를 삭제하면 다른 쪽도 함께 삭제됩니다. 또한 이러한 정리 과정은 이름을 SQL에 삽입(interpolate)하는 것을 안전하게 만듭니다. 구두점만 다르고 fix/login과 fix-login인 두 브랜치는 동일한 이름으로 매핑되며, 이 경우 두 번째 clone은 첫 번째 데이터의 데이터를 재사용하기보다는 CREATE DATABASE에서 실패합니다.
DROP DATABASE ... WITH (FORCE)는 개발 서버처럼 열어둔 워크트리 세션을 종료시키고, 준비된 트랜잭션(prepared transaction)이 남아 있는 경우에만 실패합니다(DROP DATABASE). URL에 비밀번호가 필요하면 url()에 추가하세요.
다섯 개의 에이전트를 위해 연결하는 방법:
export MIGRATE='npm run db:migrate && npm run db:seed'
./wt-db template
...
cp가 있는 이유는 .env 파일은 추적되지 않기 때문에 git worktree add로는 가져와지지 않고, clone은 DATABASE_URL 라인만 재작성하기 때문입니다. 브랜치 작업이 끝나면 워크트리를 제거하기 전에 데이터베이스를 삭제하세요. 왜냐하면 drop 명령은 워크트리에서 브랜치 이름을 읽기 때문입니다:
./wt-db drop ../app-auth
git worktree remove ../app-auth
git worktree remove는 추적되지 않거나 수정된 파일이 있는 워크트리를 제거하는 것을 거부하므로, 강제 옵션(--force)을 사용하기 전에 무엇이 남아 있는지 확인하세요(docs).
자신의 머신에서 숫자를 얻으려면, psql에서 동일한 템플릿을 세 번 복제하고 iming이 출력하는 Time: 라인을 비교하세요:
\timing on
CREATE DATABASE t_wal TEMPLATE app_template;
...
또한, t_clone 전후로 데이터 볼륨에 대해 df -h를 실행하세요. pg_database_size('t_clone')은 논리적 크기(logical size)를 세고 공유 블록(shared blocks)을 세지 않기 때문에 여전히 템플릿의 전체 크기를 보고할 것입니다. 마렉이 자신의 write-up에서 보여주듯이 말이죠. 만약 시드 데이터가 몇 메가바이트에 불과하다면, WAL_LOG가 따라잡는 것을 보고 놀라지 마세요. 그것이 문서에서 언급된 소형 템플릿 케이스입니다. 클론을 사용하면 템플릿이 커짐에 따라 그 격차가 벌어집니다. 작업이 끝나면 세 개의 t_ 데이터베이스를 삭제하세요.
나중에 문제가 생기는 경우
이 모든 것 중 대부분은 에이전트들이 한동안 작성한 후에, 일주일 정도 지나서 나타납니다.
- 에이전트가 쓰기함에 따라 클론은 분기됩니다. 블록들은 한쪽에서 변경되기 전까지는 공유 상태를 유지합니다. Marek은 20개 행의
UPDATE가 변경된 영역을 분리하는 동안 나머지 테이블은 계속 공유 상태로 남아 있는 것을 관찰했으며,UPDATE와VACUUM이 페이지의 비공유화를 유지한다고 언급했습니다. 대형 테이블을 다시 쓰는 마이그레이션은 그 작업 트리에 전체 사본을 제공합니다. 에이전트가 재작성하는 테이블에 맞춰 디스크 용량을 책정하세요. 읽기는 비용이 들지 않습니다. - 모든
FILE_COPY는 두 개의 체크포인트를 강제합니다. 복사 전에 하나, 후에 하나입니다. 노트북에서 Postgres를 사용하는 것은 보통 저렴한 편입니다. 다른 바쁜 데이터베이스가 있는 공유 서버에서는 문서에서
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기