PostgreSQL Logical Replication으로 서비스 중단 없이 메이저 버전을 올린다 — 슬롯 생성, 컷오버 순서, 롤백 기준까지
pg_upgrade를 써본 분이라면 아실 겁니다. 메이저 버전 업그레이드 당일은 어김없이 긴장의 연속입니다. 새벽 2시에 점검 공지를 띄우고, 데이터 크기에 따라 수십 분에서 몇 시간씩 DB를 멈춰두어야 하죠. 그리고 업그레이드 직후의 VACUUM, 통계 재수집, 예상치 못한 플래너 퇴행까지. 솔직히 저도 처음 몇 번은 이게 "표준 절차"라고 받아들였습니다.
그런데 Logical Replication(논리적 복제) 을 이용하면 이 그림이 완전히 달라집니다. 구버전과 신버전 DB를 동시에 띄워두고 변경 사항을 실시간으로 싱크한 다음, 연결 풀러 레벨에서 트래픽 방향만 바꿔주면 됩니다. Gadget.dev의 사례에서는 pgBouncer PAUSE/RESUME만으로 30초 이내에 컷오버를 마쳤다고 보고하고 있습니다. 블루-그린 구조로 구성해두면 구버전 DB를 페일백 경로로 그대로 유지할 수 있어 롤백 여지가 넉넉해집니다.
이 글에서는 논리적 복제가 메이저 버전 업그레이드에 왜 동작하는지 원리부터, 슬롯·Publication·Subscription 설정 순서, 컷오버 직전에 반드시 처리해야 할 시퀀스 문제, 그리고 롤백을 결정하는 기준까지 한 번에 정리합니다.
논리적 복제가 버전 업그레이드에 쓰이는 이유
스트리밍 복제(물리적 복제)는 바이너리 블록을 그대로 복사합니다. 때문에 소스와 타겟의 PostgreSQL 버전이 동일해야 하고, 아키텍처 차이가 있으면 아예 불가능합니다. 반면 논리적 복제는 WAL을 행(row) 단위 DML 메시지로 디코딩해 전달합니다. INSERT가 INSERT로, UPDATE가 UPDATE로 전달되기 때문에 타겟이 다른 메이저 버전이어도 문제없이 적용할 수 있습니다. PostgreSQL 10부터 기본 내장된 pgoutput 디코딩 플러그인 덕분에 외부 확장 없이도 동작합니다.
이 구조가 핵심입니다. 소스 DB는 계속 트래픽을 받으면서 변경 사항을 슬롯에 쌓고, 타겟 DB는 그 스트림을 받아 따라잡습니다. 둘 사이의 갭이 충분히 좁혀지면, pgBouncer 같은 연결 풀러를 잠깐 멈추고 타겟으로 연결을 재지정한 뒤 다시 열면 끝입니다.
세 가지 핵심 구성 요소
| 구성 요소 | 위치 | 역할 |
|---|---|---|
| Publication | 소스(구버전) DB | 복제할 테이블 집합 선언 |
| Replication Slot | 소스(구버전) DB | 타겟이 소비하기 전까지 WAL을 보존 |
| Subscription | 타겟(신버전) DB | Publication을 구독, 변경 사항 지속 수신 |
슬롯이 중요한 이유가 있습니다. 타겟이 일시적으로 다운되거나 느려져도 소스의 WAL이 사라지지 않습니다. 반대로 이 특성이 제일 큰 위험이기도 합니다 — 타겟이 오래 지연되면 소스 디스크가 WAL로 가득 찰 수 있으므로, PostgreSQL 13 이상이라면 max_slot_wal_keep_size를 반드시 설정해야 합니다. PostgreSQL 10~12 환경에서는 이 파라미터가 존재하지 않으므로, 슬롯 지연을 모니터링해 임계치를 넘으면 슬롯을 수동으로 드롭하는 감시 스크립트를 운영하거나, 아카이빙 대상 파티션의 디스크 여유를 별도로 확보해야 합니다.
시작 전 체크리스트 — 이것부터 확인
소스 DB가 PG10 이상이고 wal_level = logical인지 먼저 확인해봅니다.
-- 소스 DB에서
SHOW wal_level;
-- 결과: logical 이어야 함
-- logical이 아니라면 postgresql.conf 수정 후 재시작 필요
-- wal_level = logical다음은 PK가 없는 테이블 탐색입니다. UPDATE와 DELETE 복제에 PK 또는 REPLICA IDENTITY가 필수이기 때문에 이 단계를 건너뛰면 나중에 복제가 조용히 실패합니다. 저는 예전에 통계용 append-only 테이블 한 개를 놓쳤다가, 컷오버 후 관리자 화면에서 DELETE가 반영되지 않아 뒤늦게 발견한 적이 있습니다.
-- PK 없는 테이블 목록
SELECT t.tablename
FROM pg_tables t
WHERE t.schemaname = 'public'
AND t.tablename NOT IN (
SELECT tc.table_name
FROM information_schema.table_constraints tc
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = 'public'
);PK를 추가하기 어려운 테이블이라면 REPLICA IDENTITY FULL로 우회할 수 있습니다. 단, 모든 컬럼을 WAL에 기록하게 되어 부하가 늘어납니다.
ALTER TABLE legacy_log_table REPLICA IDENTITY FULL;그 외 사전 준비 사항:
- 타겟 DB에 동일한 DDL 사전 적용: 논리적 복제는 DDL을 복제하지 않습니다. CREATE TABLE, 인덱스, 외래키, 확장 설치를 신버전에 미리 만들어야 합니다.
pg_largeobject별도 처리: Large Object는 논리적 복제 범위 밖입니다. 사용 중이라면 별도 마이그레이션 계획이 필요합니다.- 디스크 여유 공간 확보: 구버전·신버전이 동시에 운영되므로 현재 데이터 크기의 약 2배 공간이 필요합니다.
단계별 설정 — Publication 생성부터 초기 동기화까지
1단계: 소스 DB — Publication 생성
-- 소스(구버전) DB에서
-- 모든 테이블을 복제 대상으로
CREATE PUBLICATION upgrade_pub FOR ALL TABLES;
-- 특정 테이블만 선택적으로 복제하고 싶다면
CREATE PUBLICATION upgrade_pub FOR TABLE users, orders, products;복제 전용 계정에 적절한 권한을 부여합니다. GRANT ... ON ALL TABLES는 현재 존재하는 테이블에만 적용되므로, 복제 기간 중 새 테이블이 만들어질 여지가 있다면 ALTER DEFAULT PRIVILEGES도 함께 걸어두는 편이 안전합니다.
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'strong_password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO replicator;
-- 이후 새로 생성될 테이블에도 자동으로 SELECT 부여
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO replicator;pg_hba.conf에 타겟 DB 호스트에서의 복제 접속도 허용해야 합니다.
# pg_hba.conf (소스 DB)
host replication replicator <타겟_IP>/32 md52단계: 타겟 DB — Subscription 생성
타겟 DB에 접속해 Subscription을 만들면 초기 스냅샷(전체 데이터 복사)이 자동으로 시작됩니다.
-- 타겟(신버전) DB에서
CREATE SUBSCRIPTION upgrade_sub
CONNECTION 'host=source-db.internal port=5432 dbname=mydb user=replicator password=strong_password'
PUBLICATION upgrade_pub;초기 복사가 얼마나 걸리는지는 데이터 크기에 따라 다릅니다. 수 TB 규모의 대용량 데이터베이스의 경우 스냅샷 단계만 수 시간에서 하루 이상 걸릴 수 있습니다. 이때 소스 DB는 정상적으로 트래픽을 받고 있고, 그 사이 변경 사항은 슬롯에 쌓입니다.
3단계: 복제 진행 상황 모니터링
초기 복사가 끝나면 CDC(Change Data Capture) 단계로 전환됩니다. 복제 지연을 꾸준히 확인합니다.
-- 소스 DB에서: 슬롯 지연 확인
SELECT slot_name,
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) AS lag_bytes
FROM pg_replication_slots;
-- 타겟 DB에서: 구독 상태 확인
SELECT subname, pid, received_lsn, latest_end_lsn
FROM pg_stat_subscription;lag_bytes가 지속적으로 줄어들고 있다면 정상입니다. 컷오버 직전에는 이 값이 수백 KB 이하로 안정되는 것을 확인한 다음 진행합니다.
컷오버 — 30초 안에 끝내는 순서
이 단계가 실제 "다운타임"에 해당하는 구간입니다. 연결 풀러(pgBouncer)를 활용하면 서비스 입장에서는 잠깐의 대기처럼 보입니다. 순서를 잘못 밟으면 데이터 불일치가 생길 수 있으므로 아래 흐름을 그대로 따르는 것을 권장합니다.
시퀀스 동기화는 빠뜨리기 쉬운 함정입니다. 논리적 복제는 NEXTVAL 호출을 복제하지 않기 때문에 타겟의 시퀀스가 소스보다 뒤처져 있습니다. 컷오버 직후 INSERT를 시도하면 이미 존재하는 ID와 충돌할 수 있습니다.
저는 이 부분을 아래처럼 "소스 조회 → 안전 여유 계산 → 타겟에 setval" 흐름을 하나의 스크립트로 묶어 처리합니다. 여유 값(아래 예시의 10000)은 컷오버 윈도우 동안 예상되는 INSERT 수를 기준으로 잡습니다. 초당 수백 건이 들어오는 서비스라면 넉넉히 잡아도 손해가 없습니다.
#!/usr/bin/env bash
# 개념적 예시 — 시퀀스별로 소스 last_value + 안전 여유를 계산해 타겟에 반영
SAFETY=10000
SEQS=$(psql -Atqc "SELECT schemaname||'.'||sequencename FROM pg_sequences WHERE schemaname='public'" -h source-db)
for seq in $SEQS; do
LAST=$(psql -Atqc "SELECT last_value FROM $seq" -h source-db)
NEW=$((LAST + SAFETY))
psql -h target-db -c "SELECT setval('$seq', $NEW);"
done수동으로 개별 시퀀스를 만지는 경우에도 흐름을 명시적으로 남겨두면 좋습니다.
-- (소스 DB) 현재 시퀀스 값 조회
SELECT sequencename, last_value
FROM pg_sequences
WHERE schemaname = 'public';
-- 예: users_id_seq.last_value = 1000000, orders_id_seq.last_value = 5000000
-- (타겟 DB) 위 결과 + 안전 여유(10000)를 더해 setval
SELECT setval('public.users_id_seq', 1000000 + 10000); -- => 1010000
SELECT setval('public.orders_id_seq', 5000000 + 10000); -- => 5010000pgBouncer PAUSE/RESUME은 관리 콘솔에서 실행합니다.
# pgBouncer 관리 콘솔 접속
psql -p 6432 -U pgbouncer pgbouncer -c "PAUSE;"
# --- 이 사이에 lag_bytes 확인, 시퀀스 동기화, 연결 설정 변경 ---
psql -p 6432 -U pgbouncer pgbouncer -c "RESUME;"롤백 — 언제, 어떻게 돌아갈 것인가
롤백 경로가 있기 때문에 논리적 복제 방식이 강력합니다. 구버전 DB를 살아있는 상태로 유지하는 한, 신버전에서 문제가 생기면 pgBouncer를 구버전으로 재지정하는 것만으로 즉시 복구할 수 있습니다.
롤백 트리거 기준으로 고려할 상황:
- 신버전에서 특정 쿼리의 실행 계획이 바뀌어 응답 시간이 크게 늘어남
- 확장(Extension) 버전 불일치로 기능 오류 발생
- 데이터 정합성 검증에서 불일치 발견
단, 롤백에는 중요한 제약이 있습니다. 신버전에 쌓인 변경 사항은 소실됩니다. 컷오버 이후 신버전에서 발생한 쓰기 트랜잭션은 구버전으로 역복제되지 않기 때문입니다. 그래서 롤백을 결정하기 전에 "얼마나 소실되는가"를 파악하고 팀 전체에 공유해야 합니다. 저는 다음 두 가지를 습관처럼 함께 봅니다.
-- (신버전 DB) 컷오버 이후 커밋된 트랜잭션 수 추정
-- 컷오버 직후 값을 스냅샷으로 저장해두고, 롤백 직전 값과의 차이를 확인
SELECT datname,
xact_commit,
xact_rollback
FROM pg_stat_database
WHERE datname = 'mydb';
-- (신버전 DB) 도메인 테이블 단위 신규 유입 건수
-- created_at, updated_at 컬럼이 있다면 컷오버 시각 기준으로 직접 카운트
SELECT count(*) AS inserted_after_cutover
FROM orders
WHERE created_at >= '<컷오버 시각>';이 수치가 크다면 단순 롤백이 아니라, 신버전에 쌓인 데이터를 CSV로 추출해 구버전에 되돌려 적재하는 보정 절차까지 함께 준비해야 합니다. 롤백 결정은 빠를수록 좋고, 그만큼 소실 범위 파악은 필수입니다.
컷오버 후 최소 24~48시간, 주말 트래픽 패턴이 있는 서비스라면 1주일은 구버전을 살려두고 모니터링합니다. 아무 문제가 없다고 확인한 뒤에야 슬롯을 정리하고 구버전을 내립니다.
슬롯 정리 순서에 주의가 필요합니다. CREATE SUBSCRIPTION으로 만든 구독은 기본적으로 소스에 같은 이름의 슬롯을 자동 생성하며, DROP SUBSCRIPTION은 그 슬롯을 함께 삭제합니다. 소스에서 먼저 pg_drop_replication_slot을 실행하면, 이후 DROP SUBSCRIPTION이 사라진 슬롯을 찾다가 실패할 수 있습니다. 두 가지 안전한 방법 중 하나를 택합니다.
-- 방법 A: 타겟에서 DROP SUBSCRIPTION만 실행 (슬롯이 자동 삭제됨)
-- 타겟 DB
DROP SUBSCRIPTION upgrade_sub;-- 방법 B: 소스에서 먼저 슬롯을 정리하고, 타겟은 slot_name=NONE으로 분리
-- 타겟 DB (연결이 끊긴 상태에서)
ALTER SUBSCRIPTION upgrade_sub DISABLE;
ALTER SUBSCRIPTION upgrade_sub SET (slot_name = NONE);
DROP SUBSCRIPTION upgrade_sub;
-- 소스 DB
SELECT pg_drop_replication_slot('upgrade_sub');롤백 스크립트는 반드시 스테이징에서 먼저 검증합니다. 프로덕션 당일에 처음 실행해보는 상황은 최악입니다.
알아두면 좋은 함정과 트레이드오프
주요 제약 사항 한눈에
| 제약 | 상세 | 우회 방법 |
|---|---|---|
| DDL 비복제 | 스키마 변경은 복제 안 됨 | 타겟에 수동 사전 적용 |
| 시퀀스 미동기화 | NEXTVAL 호출 미복제 | 컷오버 전 setval로 앞당기기 |
| PK 필수 | UPDATE/DELETE 복제에 필요 | REPLICA IDENTITY FULL |
| Large Object 미지원 | pg_largeobject 범위 밖 | 별도 마이그레이션 |
| WAL 누적 위험 | 타겟 지연 시 소스 디스크 고갈 | max_slot_wal_keep_size 설정(PG13+) |
| 디스크 2배 필요 | 양쪽 DB 동시 운영 | 충분한 사전 용량 확보 |
WAL 누적 방지를 위한 설정은 소스 DB의 postgresql.conf에서(PG13 이상):
# 슬롯이 보존하는 WAL 최대 크기 제한 (단위: MB)
max_slot_wal_keep_size = 10240 # 예: 10GB이 값을 초과하면 슬롯이 무효화될 수 있으므로, 타겟 DB의 복제 지연을 꾸준히 모니터링하는 것이 중요합니다. PG12 이하라면 이 안전장치가 없으므로, 슬롯 지연 지표에 대한 알람을 반드시 걸어두어야 합니다.
PG17 이후의 개선 사항
PostgreSQL 17(2024년 9월 출시)부터는 pg_createsubscriber라는 CLI 도구가 추가되었습니다. 물리적 스탠바이를 논리적 구독자로 한 번에 전환해주는 도구로, 기존에 여러 단계로 수동으로 해야 했던 작업을 크게 줄여줍니다.
PostgreSQL 18과 관련해 자주 언급되는 개선 항목으로는 pg_createsubscriber의 다중 DB 처리 확장, 구독의 병렬 적용 성능 개선, 유휴 슬롯 자동 정리 등이 커뮤니티에서 논의·개발되어 왔습니다. 다만 각 기능의 정확한 도입 여부와 기본값은 버전에 따라 다를 수 있으므로, 실제 도입 시에는 해당 릴리스의 PostgreSQL 공식 릴리스 노트를 직접 확인한 뒤 적용 여부를 판단하는 것을 권장합니다(2026년 기준).
서드파티 도구가 도움 되는 경우
직접 SQL로 설정하는 것이 번거롭거나, 초기 복사 속도를 높이고 싶다면:
| 도구 | 언제 유용한가 |
|---|---|
| pgcopydb | 병렬 COPY + CDC 결합, 재개(resume) 지원, 소스에 확장 설치 불필요 |
| pg_easy_replicate | 논리적 복제 업그레이드 자동화 CLI, 소규모 팀에 적합 |
| pglogical | 멀티 마스터, DDL 복제 등 고급 기능, AWS RDS에 내장 |
| AWS DMS | 관리형 서비스 환경, RDS/Aurora 간 메이저 버전 점프 |
AWS RDS 환경이라면 pglogical을 통해 이전 메이저 버전에서 상위 버전으로의 점프도 가능합니다. Azure Database for PostgreSQL도 동일한 논리적 복제 접근법을 공식 문서로 지원합니다.
마무리 — 실무에서 가장 자주 어긋나는 순서
솔직히 처음 논리적 복제로 업그레이드를 시도했을 때, 가장 많이 막혔던 지점이 시퀀스 문제와 PK 없는 테이블이었습니다. 복제 자체는 잘 동작하는데 컷오버 직후 ID 충돌 에러가 터지면 순간 멘붕이 오거든요. 이후 몇 번 반복하면서 정리해본 결과, 사고는 대체로 아래 순서 어딘가에서 새어 나옵니다. 체크리스트처럼 훑어보시길 권합니다.
- 사전 준비 단계에서 놓치는 것들 — 타겟에 DDL·확장·인덱스가 100% 사전 적용됐는지, PK가 없는 테이블에
REPLICA IDENTITY FULL이 설정됐는지, 복제 계정에ALTER DEFAULT PRIVILEGES까지 걸려 있는지. - 모니터링 지표의 임계치 —
lag_bytes가 컷오버 기준(예: 수백 KB 이하)까지 안정되는지, 소스 디스크 여유가 슬롯 누적 최악 시나리오를 견디는지, PG13 미만이라면 슬롯 지연 알람이 별도로 걸려 있는지. - 컷오버 순서 — pgBouncer PAUSE → lag 재확인 → 시퀀스 setval → 연결 재지정 → RESUME. 이 순서를 스크립트로 만들고 스테이징에서 반드시 먼저 돌려봅니다.
- 롤백 조건과 소실 범위 — 롤백 트리거(쿼리 성능 저하, 정합성 이상)를 문서로 명시하고, 롤백 직전 신버전의 커밋 수와 도메인 테이블 신규 건수를 스냅샷으로 남길 절차까지 준비합니다.
- 정리 순서 —
DROP SUBSCRIPTION이 슬롯을 함께 지운다는 점을 잊지 말고, 소스의pg_drop_replication_slot을 먼저 부르지 않도록 유의합니다.
PG17 이후라면 pg_createsubscriber가 수동 설정 부담을 상당 부분 덜어줍니다. 새 환경을 구성한다면 최신 릴리스 노트를 확인하며 병렬 apply나 슬롯 자동 관리 같은 개선 사항을 함께 검토해보시길 권합니다.
참고 자료
- PostgreSQL 공식 문서 — Upgrading a PostgreSQL Cluster
- PostgreSQL 공식 문서 — Logical Replication Restrictions
- PostgreSQL 릴리스 노트
- Crunchy Data — Online Upgrades in Postgres
- Cybertec — PostgreSQL Major Version Upgrade via Logical Replication
- Gadget.dev — Zero downtime Postgres upgrades using logical replication
- myDBA.dev — Zero-Downtime Postgres Major Upgrades with Logical Replication
- Percona — PITR, pg_upgrade, and Logical Replication Together
- pgcopydb 공식 문서
- PostgreSQL Fastware — Seamless Subscriber Upgrades
- PostgreSQL Fastware — How to upgrade replication clusters without downtime
- pganalyze — Zero downtime upgrades and logically replicate very large tables
- Microsoft — Upgrade Azure Database for PostgreSQL with Minimal Downtime
- AWS — Using logical replication for Aurora PostgreSQL major version upgrade
- Nerd Level Tech — Postgres 18 Zero-Downtime Upgrade: pg_createsubscriber
- DBI Services — PostgreSQL 17-18 Blue-Green Migration