~/infra-dev.md
INFRA_DEV

PostgreSQL 18 논리 복제는 왜 “복사 파이프”가 아니라 WAL 보존 계약일까

10분 읽기·2026.07.26·출처 5·00
오늘의 질문

논리 복제를 켜면 데이터가 자동으로 따라가는 것처럼 보이지만, 실제 운영에서 먼저 관리해야 할 것은 테이블인가, WAL을 붙잡는 replication slot인가?

infra-dev.md
Postgres WAL contract

PostgreSQL 18 논리 복제는 왜 “복사 파이프”가 아니라 WAL 보존 계약일까

1. 왜 지금 봐야 하나

PostgreSQL 논리 복제(logical replication)는 작은 팀에게도 점점 현실적인 운영 도구가 되고 있다. 무중단에 가까운 마이그레이션, 읽기 모델 분리, 검색 인덱스나 분석 DB로의 변경 데이터 전달, RDS에서 Aurora로 옮기는 전환 작업까지 모두 “원본 DB의 변경을 다른 곳에 계속 흘려보내는” 문제로 볼 수 있기 때문이다.

그런데 논리 복제를 단순히 “테이블을 구독하면 복사된다”로 이해하면 운영 사고를 놓치기 쉽다. 논리 복제의 핵심 위험은 대부분 네트워크 연결 자체가 아니라 publisher가 subscriber를 위해 WAL을 얼마나 오래 붙잡아야 하는가에서 시작한다. subscriber가 멈추거나, slot을 만들고 쓰지 않거나, migration 테스트 후 slot을 지우지 않으면 publisher는 “언젠가 소비자가 다시 올 수 있다”고 보고 필요한 WAL을 보존한다. 이 보존이 길어지면 디스크 사용량, 백업/복구 시간, 장애 대응 시간이 같이 커진다.

PostgreSQL 18은 이 지점을 꽤 직접적으로 다룬다. 공식 릴리스 발표와 문서는 다음 변화를 설명한다.

  • idle replication slot을 idle_replication_slot_timeout으로 자동 invalidation할 수 있다.
  • pg_replication_slotsinactive_since, invalidation_reason 같은 상태를 보고 운영 판단을 할 수 있다.
  • stored generated column을 opt-in으로 논리 복제 스트림에 포함할 수 있다.
  • logical replication conflict를 로그와 pg_stat_subscription_stats의 새 counter로 더 세분해 볼 수 있다.
  • 새 subscription의 streaming 기본값이 parallel로 바뀌었다.
  • ALTER SUBSCRIPTION으로 two-phase commit 설정을 바꿀 수 있다.

AWS Database Blog도 RDS for PostgreSQL 18.3 예제로 같은 변화를 소개하면서, RDS/Aurora/self-managed PostgreSQL에서 SQL 문법은 같다고 설명한다. 이 글은 기능 목록을 외우는 대신, 개발자가 운영에서 가져가야 할 한 문장에 집중한다.

논리 복제는 복사 파이프가 아니라 WAL 보존 계약이다.

2. 핵심 개념

논리 복제에는 세 개의 주체가 있다.

  1. publication: publisher에서 어떤 table/column/change를 내보낼지 정하는 계약
  2. subscription: subscriber에서 어떤 publication을 받아 적용할지 정하는 계약
  3. replication slot: publisher가 subscriber를 위해 어느 WAL 위치부터 보존해야 하는지 기억하는 상태

처음 두 개는 “무엇을 복제할까”에 가깝고, 세 번째는 “얼마나 오래 책임질까”에 가깝다. 운영 장애는 보통 세 번째에서 난다.

예를 들어 subscriber가 죽었다고 하자. 애플리케이션 요청은 정상일 수도 있다. publisher DB도 쿼리를 계속 처리한다. 겉보기에는 당장 큰 문제가 없다. 하지만 replication slot이 살아 있다면 publisher는 subscriber가 아직 받지 못한 WAL을 계속 보존해야 한다. 시간이 지나면 slot의 restart_lsn 뒤쪽 WAL이 쌓이고, 디스크 여유 공간이 줄어든다. 이때 운영자가 확인해야 하는 질문은 “복제가 켜져 있나?”가 아니라 다음이다.

  • 이 slot은 지금 active인가?
  • inactive라면 언제부터 inactive였나?
  • 이 slot이 붙잡는 WAL은 얼마나 위험한가?
  • 이 slot은 실제 서비스 복구에 필요한가, 아니면 테스트/마이그레이션 잔재인가?
  • 자동으로 invalidation해도 되는 slot인가, 수동 확인이 필요한 slot인가?

PostgreSQL 18의 idle_replication_slot_timeout은 이 질문에 대한 기본 방어선이다. 문서에 따르면 이 값은 inactive 상태가 설정 duration을 넘긴 replication slot을 invalidation한다. 기본값은 0, 즉 비활성화다. 단위 없이 쓰면 초로 해석된다. 중요한 운영 디테일은 두 가지다.

  • invalidation은 timeout을 넘는 순간 즉시 일어나는 것이 아니라 checkpoint 시점에 일어난다.
  • inactivity 판단은 pg_replication_slots.inactive_since를 기준으로 한다.

즉, idle_replication_slot_timeout = 1h라고 해서 정확히 1시간 뒤 slot이 사라진다고 생각하면 안 된다. 다음 checkpoint까지 지연이 있을 수 있다. 급하게 정리해야 한다면 checkpoint를 강제로 유도할 수 있지만, 그 자체도 운영 행동이므로 runbook에 남겨야 한다.

또 하나 중요한 표현은 “drop”이 아니라 “invalidate”다. PostgreSQL 18 문서의 pg_replication_slots view에는 invalidation_reason이 있으며, 가능한 이유 중 하나가 idle_timeout이다. invalidated slot은 상태가 남아 있어 왜 쓸 수 없게 되었는지 확인할 수 있다. 운영자 입장에서는 자동 청소가 블랙박스로 사라지는 것이 아니라, 사후 점검 가능한 흔적을 남기는 셈이다.

3. 최신 이슈와 연결

AWS의 PostgreSQL 18 논리 복제 글은 실제 RDS 18.3 환경에서 개선점을 단계적으로 보여준다. 이 글에서 눈여겨볼 부분은 기능이 “더 편해졌다”가 아니라, 기존 운영 실패 모드를 각각 작게 줄인다는 점이다.

첫째, stored generated column 복제다. PostgreSQL 18 전에는 generated column이 논리 복제에서 publish되지 않았다. PostgreSQL 18 문서는 stored generated column을 publish_generated_columns = stored로 publication에 포함할 수 있다고 설명한다. 다만 기본값은 none이다. 호환성과 기존 동작을 깨지 않기 위해서다.

여기서 실무 포인트는 subscriber schema다. 문서의 표에 따르면 generated column 값을 publish하는 경우, subscriber 쪽 generated column으로 그대로 넣는 것은 지원되지 않고 error가 날 수 있다. publisher의 generated column 값을 subscriber의 regular column로 받는 식의 설계가 필요하다. 특히 비-PostgreSQL target이나 CDC pipeline으로 보낼 때 유용할 수 있지만, “양쪽에 같은 generated column을 만들면 원본 계산값이 그대로 들어가겠지”라고 가정하면 틀릴 수 있다.

둘째, conflict 관측성이다. PostgreSQL 18 릴리스 노트와 AWS 글은 logical replication conflict가 로그뿐 아니라 pg_stat_subscription_stats의 새 counter들로 더 자세히 보인다고 설명한다. 기존에는 “복제가 실패했다” 정도의 신호만 보고 subscriber 로그를 뒤지는 경우가 많았다면, 이제는 conflict 유형별 counter를 모니터링 지표로 올릴 여지가 생긴다. 이것은 incident response에서 매우 중요하다. 복제 지연이 네트워크 때문인지, 대량 transaction 때문인지, row conflict 때문인지에 따라 대응이 완전히 달라지기 때문이다.

셋째, parallel streaming 기본값이다. PostgreSQL 18에서 새 subscription의 streaming 기본값은 parallel이다. 큰 transaction이 끝날 때까지 기다렸다가 한 번에 적용하는 대신, streaming과 parallel apply worker를 통해 lag와 memory pressure를 줄이는 방향이다. 물론 기본값이 좋아졌다고 해서 모니터링이 필요 없어지는 것은 아니다. apply worker, lock, downstream constraint, subscriber I/O가 병목이면 lag는 여전히 생긴다.

넷째, idle slot 자동 invalidation이다. 이 기능은 특히 클라우드 DB에서 비용과 가용성 모두에 영향을 준다. RDS/Aurora에서는 WAL 보존이 storage 비용과 운영 안정성에 직접 연결된다. AWS 글은 idle_replication_slot_timeout을 0이 아닌 값으로 설정하면 다음 checkpoint 이후 idle slot을 invalidation하고, invalidated slot은 pg_replication_slotsinvalidation_reason과 함께 남는다고 설명한다.

4. 개발자 관점 해석

개발자는 DB 운영을 DBA의 일로만 분리하기 쉽다. 하지만 논리 복제는 애플리케이션 개발자의 스키마 설계, 배포 순서, 마이그레이션 코드와 직접 연결된다.

4-1. 마이그레이션은 “데이터 복사”가 아니라 “변경 계약 전환”이다

RDS에서 Aurora로 옮기거나, 메인 DB에서 검색/분석용 DB로 CDC를 보낸다고 하자. 초기 snapshot이나 dump는 과거의 한 시점을 복사한다. 그 뒤의 변경분은 WAL과 replication slot이 책임진다. 따라서 cutover runbook에는 최소한 다음이 있어야 한다.

  • publication 생성 시점
  • subscription 생성 시점
  • initial sync 완료 기준
  • replication lag 허용치
  • slot 이름과 목적
  • rollback 시 slot 유지/삭제 기준
  • cutover 후 남은 slot 정리 기준

이 항목이 없으면 “마이그레이션은 성공했는데 몇 주 뒤 WAL이 쌓였다” 같은 늦은 장애가 생긴다.

4-2. generated column은 target 계약까지 봐야 한다

stored generated column을 publish할 수 있게 된 것은 편리하지만, 무조건 켜면 되는 옵션이 아니다. subscriber가 PostgreSQL 18인지, 이전 버전인지, 비-PostgreSQL sink인지, 같은 계산식을 가지고 있는지에 따라 결과가 달라진다.

특히 문서는 PostgreSQL 18 이전 subscriber에서는 initial table synchronization이 generated column을 복사하지 않는다고 설명한다. 그러면 운영자는 “초기 데이터는 비어 있거나 기본값인데 이후 변경분부터는 들어오는” 어색한 상태를 만날 수 있다. 따라서 generated column을 복제할 때는 다음 중 하나를 명확히 정해야 한다.

  • subscriber가 직접 계산한다.
  • publisher의 stored 값을 regular column으로 받는다.
  • generated column은 CDC 대상에서 제외한다.
  • target 버전을 맞춘 뒤에 enable한다.

4-3. 자동 invalidation은 안전장치이지 복구 전략이 아니다

idle_replication_slot_timeout을 켜면 abandoned slot이 WAL을 무한히 붙잡는 위험은 줄어든다. 하지만 이것은 “복제가 오래 멈춰도 알아서 복구된다”는 뜻이 아니다. slot이 invalidated되면 해당 slot은 더 이상 정상 continuation을 보장하지 않는다. subscriber는 재동기화가 필요할 수 있다.

따라서 timeout 값은 서비스별 RPO/RTO와 연결해야 한다. 예를 들어 분석용 sink는 6시간 뒤 재동기화해도 괜찮을 수 있지만, migration cutover 직전의 hot standby는 1시간 idle로 invalidation되면 안 된다. 같은 PostgreSQL 기능이라도 목적에 따라 정책이 달라진다.

4-4. conflict counter는 알림 문장으로 번역해야 한다

pg_stat_subscription_stats에 더 많은 counter가 생겨도, 운영자가 보지 않으면 의미가 없다. 개발팀이 해야 할 일은 SQL view 이름을 외우는 것이 아니라 알림 문장으로 바꾸는 것이다.

  • “subscriber apply conflict가 5분 동안 증가했다”
  • “replication lag가 증가하면서 slot safe WAL 여유가 줄고 있다”
  • “idle slot이 timeout으로 invalidated되었다”
  • “generated column publication 변경 이후 subscription error가 발생했다”

이렇게 알림이 행동으로 연결되어야 한다.

5. 내 프로젝트에 적용할 체크포인트

아래 체크리스트는 PostgreSQL 18을 당장 쓰지 않더라도 유효하다.

replication slot 인벤토리

zsh — 생존확인.sh
SELECT slot_name,
       slot_type,
       active,
       inactive_since,
       restart_lsn,
       confirmed_flush_lsn,
       wal_status,
       safe_wal_size,
       invalidation_reason
FROM pg_replication_slots
ORDER BY active, inactive_since NULLS FIRST;

확인할 것:

  • 이름만 보고 목적을 알 수 없는 slot이 있는가?
  • inactive slot이 얼마나 오래되었는가?
  • safe_wal_size가 null인 이유가 lost 상태인지, max_slot_wal_keep_size = -1 때문인지 구분했는가?
  • invalidated slot을 drop하는 운영 절차가 있는가?

timeout 정책

  • idle_replication_slot_timeout 기본값 0을 그대로 둘 이유가 있는가?
  • 운영용 CDC, migration용 임시 복제, 실험용 slot의 timeout을 다르게 둘 수 있는가?
  • timeout 발생 후 subscriber 재동기화 절차가 문서화되어 있는가?
  • checkpoint 지연 때문에 invalidation이 늦어질 수 있음을 알림 설명에 반영했는가?

publication/schema 계약

  • generated column이 있는 table을 publication에 포함하는가?
  • publish_generated_columns = stored를 켜야 하는 이유가 명확한가?
  • subscriber column은 generated인가 regular인가?
  • subscriber가 PostgreSQL 18 이전이면 initial sync 동작을 테스트했는가?

관측성과 알림

  • replication lag만 보지 않고 slot WAL retention도 보는가?
  • pg_stat_subscription_stats conflict counter를 대시보드에 올렸는가?
  • subscriber apply error가 발생했을 때 어떤 query로 원인을 볼지 runbook에 있는가?
  • slot invalidation이 발생하면 Slack/Telegram/Jira 중 어디로 알릴지 정했는가?

비용과 정리

  • migration 테스트 후 slot, publication, subscription, 임시 DB를 지우는 cleanup checklist가 있는가?
  • 클라우드 DB storage 증가 알림이 WAL 증가와 연결되어 있는가?
  • 임시 replication user와 security group을 cutover 후 회수하는가?

6. 오늘 10분 액션

오늘 할 일은 PostgreSQL 18 업그레이드가 아니다. “우리 팀이 논리 복제를 WAL 보존 계약으로 보고 있는지” 확인하는 것이다.

  1. 운영 또는 staging DB에서 pg_replication_slots 조회 권한이 있는지 확인한다.
  2. slot이 있다면 slot_name, active, inactive_since, restart_lsn, wal_status를 표로 적는다.
  3. 각 slot 옆에 “소유 팀 / 목적 / 삭제해도 되는 조건 / 최대 idle 허용 시간”을 한 줄로 쓴다.
  4. generated column이 있는 테이블을 하나 골라 CDC나 migration 대상인지 확인한다.
  5. PostgreSQL 18 도입 후보라면 idle_replication_slot_timeout의 후보 값을 “분석용 / 마이그레이션용 / 실험용”으로 나눠 적는다.

10분 안에 끝나지 않더라도 괜찮다. 중요한 것은 slot을 “DB 내부 객체”가 아니라 “운영 책임이 붙은 계약”으로 이름 붙이는 것이다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 infra_dev 글을 확인했다. 최근 글은 Docker Content Trust/Notary v1 종료에 따른 컨테이너 서명 정책 마이그레이션, Cloud Run multi-region service health, GKE rollout sequencing, GitHub Linked Artifacts, Kubernetes In-place Pod Resize, Terraform state secret, GitHub Actions step-level parallelism, OpenTelemetry semantic convention, RDS delayed replica를 다뤘다. 이번 글은 RDS/PostgreSQL을 다시 언급하지만 delayed replica 기반 재해 복구나 백업 지연 복구가 아니라, PostgreSQL 18 논리 복제에서 replication slot, WAL retention, idle slot invalidation, generated column publication, conflict observability를 운영 계약으로 관리하는 관점에 집중해 중복을 피했다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)