논리적 복제
논리적 복제 (Logical Replication)
이번 페이지에서는 PostgreSQL의 논리적 복제(logical replication) — 29장 전체의 개요를 함께 살펴볼게요. 물리적 복제와 어떻게 다르고, publish/subscribe 모델이 어떻게 돌아가는지, 그리고 실무에서 언제 쓰면 좋은지까지 옆에서 차근차근 설명해 드릴게요.
논리적 복제란 무엇인가 (What logical replication is)
**논리적 복제(logical replication)**는 데이터 객체와 그 변경 사항을, **복제 식별자(replication identity, 보통 기본 키)**를 기반으로 복제하는 방법이에요. 여기서 "논리적(logical)"이라는 단어는, 정확한 블록 주소와 바이트 단위 복제(byte-by-byte)를 사용하는 **물리적 복제(physical replication)**와 대비되는 의미예요. PostgreSQL은 두 메커니즘을 동시에 지원해요.
논리적 복제는 두 가지 측면에서 세밀한 제어를 제공해요.
- 데이터 복제의 세밀한 제어
- 보안의 세밀한 제어
Publish/Subscribe 모델 (Publish and subscribe model)
논리적 복제는 게시-구독(publish and subscribe) 모델을 사용해요. 하나 이상의 구독자가 게시자(publisher) 노드의 하나 이상의 게시물(publication)을 구독하는 구조죠. 구독자는 자신이 구독하는 게시물에서 데이터를 당겨오고(pull), 이후 데이터를 다시 게시해서 **캐스케이드 복제(cascading replication)**나 더 복잡한 구성을 만들 수 있어요.
트랜잭션 복제 (Transactional replication)
테이블의 논리적 복제가 시작되면, PostgreSQL은 게시자 데이터베이스에서 테이블 데이터의 스냅샷을 찍어서 구독자에게 복사해요. 이것이 완료되면, 초기 복사 이후의 게시자 변경 사항이 지속적으로 구독자에게 전송돼요. 구독자는 게시자가 적용한 것과 같은 순서로 데이터를 적용하므로, 단일 구독 안의 게시물들에 대해서는 **트랜잭션 일관성(transactional consistency)**이 보장돼요. 그래서 이 복제 방식은 **트랜잭션 복제(transactional replication)**라고도 불려요.
일반적인 사용 사례 (Typical use-cases)
논리적 복제의 일반적인 사용 사례는 다음과 같아요.
- 단일 데이터베이스 또는 데이터베이스 일부의 증분 변경을 발생하는 대로 구독자에게 보내기
- 변경 사항이 구독자에 도착할 때 개별 변경에 대해 트리거를 실행하기
- 여러 데이터베이스를 하나로 통합하기 (예: 분석 목적)
- 서로 다른 PostgreSQL 메이저 버전 간 복제
- 다른 플랫폼의 PostgreSQL 인스턴스 간 복제 (예: Linux에서 Windows로)
- 복제된 데이터에 다른 사용자 그룹이 접근하도록 하기
- 데이터베이스 일부를 여러 데이터베이스 간에 공유하기
구독자 데이터베이스는 다른 PostgreSQL 인스턴스와 똑같이 동작하며, 자기만의 게시물을 정의해서 다른 데이터베이스의 게시자가 될 수 있어요. 구독자가 애플리케이션에 의해 읽기 전용으로 취급되면 단일 구독에서 충돌이 없어요. 반대로 애플리케이션이나 다른 구독자가 같은 테이블 집합에 다른 쓰기를 수행하면 **충돌(conflict)**이 발생할 수 있어요.
게시물 (29.1 Publication)
게시물은 물리적 복제 프라이머리 어디에서나 정의할 수 있어요. 게시물이 정의되는 노드를 **게시자(publisher)**라고 해요. 게시물은 테이블 또는 테이블 그룹에서 생성된 변경 사항의 집합이며, 변경 집합(change set) 또는 **복제 집합(replication set)**이라고도 불려요.
게시물은 스키마와 다르며 테이블 접근 방식에 영향을 주지 않아요. 필요하면 각 테이블을 여러 게시물에 추가할 수 있어요. 게시물은 현재 테이블과 스키마의 모든 테이블만 포함할 수 있고, 객체는 명시적으로 추가해야 해요. 게시물은 생성하는 변경을 INSERT, UPDATE, DELETE, TRUNCATE의 임의 조합으로 제한할 수 있어요. 트리거가 특정 이벤트 유형으로 발화되는 것과 비슷하죠. 기본적으로 모든 연산 유형이 복제돼요.
복제 식별자(Replica Identity) — 게시된 테이블의 UPDATE와 DELETE를 구독자에서 올바르게 식별하려면 복제 식별자가 필요해요. 기본적으로 기본 키가 사용돼요.
구독 (29.2 Subscription)
구독은 논리적 복제의 **하류 측(다운스트림)**이에요. 구독이 정의되는 노드를 **구독자(subscriber)**라고 해요. 구독은 다른 데이터베이스로의 연결과, 구독하려는 게시물(하나 이상) 집합을 정의해요. 구독자 노드는 원하면 여러 구독을 가질 수 있어요. 단일 게시자-구독자 쌍 사이에 여러 구독을 정의할 때는, 구독하는 게시물 객체가 겹치지 않도록 주의해야 해요.
구독에는 **복제 슬롯 관리(Replication Slot Management)**가 있고, 실제 설정 예시는 "Examples" 절에 자세히 나와 있어요.
논리적 복제 페일오버 (29.3 Logical Replication Failover)
게시자 노드가 다운되어도 구독자 노드가 게시자 노드로부터 계속 복제할 수 있으려면, 게시자 노드에 대응하는 물리적 스탠바이가 있어야 해요. 프라이머리 서버에서 구독자에 해당하는 논리적 슬롯은 스탠바이에도 복제돼야 해요.
슬롯 동기화는 비동기로 복사되므로, 페일오버가 일어나기 전에 복제 슬롯이 스탠바이 서버에 동기화됐는지 확인해야 해요. 성공적인 페일오버를 보장하려면 스탠바이 서버가 구독자 뒤처짐보다 앞서야 해요. 특정 구독자에 대해 스탠바이 서버가 실제로 페일오버 준비가 되었는지 확인하는 절차가 문서에 나와 있어요.
행 필터 (29.4 Row Filters)
기본적으로 게시된 모든 테이블의 모든 데이터가 적절한 구독자에게 복제돼요. **행 필터(row filter)**를 사용하면 복제되는 데이터를 줄일 수 있어요. 사용자는 행 필터를 동작상, 보안상, 성능상 이유로 선택할 수 있어요.
행 필터는 변경 사항을 게시하기 전에 적용돼요. 행 필터가 false나 NULL로 평가되면 그 행은 복제되지 않아요. WHERE 절 표현식은 복제 연결에 사용된 동일한 역할로 평가돼요. 제약도 있어요 — WHERE 절은 단순한 표현식만 허용돼요. 사용자 정의 함수, 연산자, 타입, 콜레이션, 시스템 컬럼 참조, non-immutable 내장 함수는 포함할 수 없어요.
또 UPDATE 변환, 파티션 테이블 처리, 초기 데이터 동기화, 여러 행 필터 결합 등의 세부 규칙이 이 절에 담겨 있어요.
컬럼 리스트 (29.5 Column Lists)
각 게시물은 각 테이블의 어느 컬럼을 구독자에게 복제할지 선택적으로 지정할 수 있어요. 구독자 쪽의 테이블은 게시되는 컬럼을 적어도 모두 가져야 해요. 컬럼 리스트가 지정되지 않으면 게시자의 모든 컬럼이 복제돼요.
컬럼 선택은 동작상 또는 성능상 이유로 할 수 있어요. 다만 이 기능을 보안에 의존하지 마세요. 악의적인 구독자는 게시되지 않은 컬럼의 데이터를 얻을 수 있어요. 보안이 고려 사항이라면 게시자 쪽에서 보호를 적용해야 해요. 또 컬럼 리스트가 없으면 나중에 추가된 컬럼이 자동으로 복제돼요. 즉 모든 컬럼을 나열한 컬럼 리스트가 있는 것과 아예 없는 것은 다르다는 뜻이에요.
생성 컬럼 복제 (29.6 Generated Column Replication)
일반적으로 구독자의 테이블은 게시자와 동일하게 정의되므로, 게시자 테이블에 GENERATED 컬럼이 있으면 구독자 테이블에도 일치하는 생성 컬럼이 있어요. 이 경우 항상 구독자 테이블의 생성 컬럼 값이 사용돼요. 한 가지 중요한 점은, 18.0 버전 이전에는 논리적 복제가 GENERATED 컬럼을 전혀 게시하지 않았어요. (확인 필요: 정확한 버전 동작은 문서의 예시를 참고하세요.)
충돌 (29.7 Conflicts)
논리적 복제는 구독자 노드에서 데이터를 로컬로 변경했어도 그 데이터가 갱신된다는 점에서 일반 DML 연산과 비슷하게 동작해요. 들어오는 데이터가 어떤 제약을 위반하면 복제가 중지돼요. 이를 **충돌(conflict)**이라고 해요. 적용 프로세스가 충돌을 감지하면 충돌을 해결할 때까지 실패한 트랜잭션의 적용을 건너뛰고 계속해요. 시행된 뒤 생성되는 오류 메시지가 문서에 예시로 나와 있어요.
예를 들어 insert_exists 충돌은 NOT DEFERRABLE 유니크 제약을 위반하는 행 삽입 시 발생해요. 충돌이 감지되면 로그와 통계(pg_stat_subscription_stats 뷰)가 기록돼요. 이때 충돌 키의 출처와 커밋 타임스탬프 세부 정보를 기록하려면 구독자에서 track_commit_timestamp를 활성화해야 해요.
제한 사항 (29.8 Restrictions)
논리적 복제에는 현재 몇 가지 제한 또는 누락된 기능이 있어요. 이들은 향후 릴리스에서 해결될 수도 있어요.
- 데이터베이스 스키마와 DDL 명령은 복제되지 않아요. 초기 스키마는
pg_dump --schema-only로 수동 복사할 수 있고, 이후 스키마 변경은 수동으로 동기화해야 해요. - 시퀀스 데이터는 복제되지 않아요. 시퀀스로 뒷받침되는 serial 또는 identity 컬럼의 데이터는 테이블의 일부로 복제되지만, 시퀀스 자체는 구독자에서 시작값을 계속 보여줘요. 구독자가 읽기 전용으로 사용되면 이는 보통 문제가 되지 않아요.
아키텍처 (29.9 Architecture)
논리적 복제는 물리적 스트리밍 복제와 비슷한 아키텍처로 구축돼요. walsender와 apply 프로세스로 구현돼요. walsender 프로세스는 WAL의 논리적 디코딩(47장에서 설명)을 시작하고, 게시된 테이블의 변경을 스트림으로 구독자에게 보내요.
구독자 데이터베이스의 apply 프로세스는 항상 session_replication_role을 replica로 설정하고 실행돼요. 이는 기본적으로 트리거와 규칙이 구독자에서 발화되지 않는다는 뜻이에요. 사용자는 테이블에서 트리거와 규칙을 선택적으로 활성화할 수 있어요. 현재 논리적 복제 적용 프로세스는 행 트리거(row trigger)만 발화하고, 문 트리거(statement trigger)는 발화하지 않아요. 다만 초기 테이블 동기화는 COPY 명령처럼 구현되므로 INSERT에 대해 행과 문 트리거를 둘 다 발화해요.
초기 스냅샷 (Initial Snapshot) — 테이블 복제가 시작될 때 게시자 테이블 데이터의 스냅샷이 찍혀 구독자에게 복사돼요.
모니터링 (29.10 Monitoring)
논리적 복제는 물리적 스트리밍 복제와 비슷한 아키텍처를 기반으로 하므로, 게시 노드의 모니터링은 물리적 복제 프라이머리의 모니터링과 비슷해요.
구독에 대한 모니터링 정보는 pg_stat_subscription 뷰에서 볼 수 있어요. 이 뷰는 구독 워커마다 행 하나를 갖고 있어요. 구독은 상태에 따라 0개 이상의 활성 구독 워커를 가질 수 있어요. 보통 활성 구독에는 apply 프로세스 하나가 실행돼요. 비활성 구독이나 충돌(크래시)한 구독은 이 뷰에 행이 0개예요. 테이블의 초기 데이터 동기화가 진행 중이면 추가 워커가 있어요.
보안 (29.11 Security)
복제 연결에 사용되는 역할은 REPLICATION 속성을 가져야 하거나 (또는 슈퍼유저여야 해요) 그래야 게시자의 행 보안 정책을 실행할 수 있어요. 복제 연결이 사용하는 출력 플러그인(output plugin)의 이름은 서버의 output_plugin_libraries에 포함돼야 해요. (구독의 경우 사용되는 플러그인 이름은 pgoutput이에요.) 초기 테이블 데이터를 복사하려면, 복제 연결에 사용되는 역할이 게시된 테이블에 대한 SELECT 권한을 가져야 하거나 (또는 슈퍼유저여야 해요) 해요.
구성 설정 (29.12 Configuration Settings)
논리적 복제에는 몇 가지 구성 옵션을 설정해야 해요. 이 옵션들은 복제의 한쪽에서만 관련돼요.
게시자 (Publishers): wal_level은 logical로 설정해야 해요. max_replication_slots는 연결될 예상 구독 수와 테이블 동기화를 위한 여유분을 합한 값 이상으로 설정해야 해요.
구독자 (Subscribers): 관련 설정이 문서에 자세히 나와 있어요.
업그레이드 (29.13 Upgrade)
논리적 복제 클러스터의 마이그레이션은 모든 구 클러스터 멤버가 17.0 이상 버전일 때만 가능해요. pg_upgrade는 논리적 슬롯의 마이그레이션을 시도하는데, 이는 새 게시자에 동일한 논리적 슬롯을 수동으로 정의할 필요를 없애줘요. 슬롯 마이그레이션은 구 클러스터가 17.0 이상일 때만 지원돼요.
게시자 클러스터 업그레이드를 시작하기 전에 구독을 일시적으로 비활성화(ALTER SUBSCRIPTION ... DISABLE)해야 하고, 업그레이드 후에 다시 활성화해야 해요.
빠른 설정 (29.14 Quick Setup)
postgresql.conf에서 구성 옵션(예: wal_level = logical)을 먼저 설정하고, pg_hba.conf에서 복제 연결을 허용하도록 조정해야 해요. 그 다음 게시물과 구독을 만들고, CREATE PUBLICATION / CREATE SUBSCRIPTION으로 복제를 시작해요. 자세한 단계별 예시가 문서에 나와 있어요.