구독
구독 (Subscription) — 변경을 받아 적용하는 하류 쪽
논리 복제에서 **구독(subscription)**은 하류(downstream) 쪽이에요. 구독을 정의한 노드를 subscriber라고 부르고, 구독은 "어느 데이터베이스에 연결해, 어떤 출판(하나 이상)에 가입할지"를 정의합니다. 출판이 "무엇을 내보낼지"였다면, 구독은 "무엇을 받아서 적용할지"를 정하는 셈이죠.
출처: 공식문서
구독자도 다시 publisher가 될 수 있어요
구독자 데이터베이스는 다른 PostgreSQL 인스턴스와 똑같이 동작해요. 즉 자기만의 출판을 정의해서 다른 데이터베이스의 publisher가 될 수도 있습니다. 또 subscriber 노드 하나에 구독을 여러 개 달 수 있는데, 같은 publisher–subscriber 쌍 사이에 구독이 여러 개라면 가입한 출판 객체들이 겹치지 않도록 주의해야 해요.
복제 슬롯과의 관계
각 구독은 복제 슬롯 하나를 통해 변경을 받아요. 기존 테이블 데이터의 초기 동기화를 위해 추가 복제 슬롯이 필요할 수 있는데, 이 슬롯들은 데이터 동기화가 끝나면 자동으로 버려집니다.
논리 복제 구독은 동기 복제의 스탠바이가 될 수도 있어요. 스탠바이 이름은 기본적으로 구독 이름이고, 구독의 연결 정보에서 application_name으로 다른 이름을 지정할 수 있어요.
pg_dump와 구독
pg_dump는 현재 사용자가 슈퍼유저일 때만 구독을 덤프합니다. 그렇지 않으면 경고를 쓰고 구독을 건너뛰는데, 비슈퍼유저는 pg_subscription 카탈로그의 모든 구독 정보를 읽을 수 없기 때문이에요.
구독 수명주기
구독은 CREATE SUBSCRIPTION으로 추가하고, ALTER SUBSCRIPTION으로 언제든지 중지·재개하며, DROP SUBSCRIPTION으로 제거해요.
구독을 버리고 다시 만들면 동기화 정보가 사라집니다. 이후 데이터를 다시 동기화해야 한다는 뜻이에요.
스키마 정의는 복제되지 않습니다
중요한 제약인데, 스키마 정의는 복제되지 않아요. 출판된 테이블은 subscriber 쪽에 이미 존재해야 합니다. 또 복제 대상은 일반 테이블뿐이라서, 예를 들어 뷰로 복제하는 건 불가능해요.
테이블은 **정규화된 전체 이름(fully qualified table name)**으로 publisher와 subscriber 사이에서 매칭돼요. subscriber에서 이름이 다른 테이블로 복제하는 건 지원하지 않습니다.
열도 이름으로 매칭돼요. subscriber 테이블의 열 순서는 publisher와 같을 필요 없고, 데이터 타입도 데이터의 텍스트 표현이 목표 타입으로 변환될 수만 있다면 같을 필요가 없어요(예: integer 열에서 bigint 열로 복제 가능). 또 목표 테이블에 출판 테이블이 제공하지 않는 추가 열이 있어도 되는데, 그런 열은 목표 테이블 정의에 지정된 기본값으로 채워집니다. 다만 바이너리 형식의 논리 복제는 더 제약이 심해요. 자세한 내용은 CREATE SUBSCRIPTION의 binary 옵션을 참고하세요.
복제 슬롯 관리
앞서 말했듯 각 (활성) 구독은 원격(publishing) 쪽의 복제 슬롯 하나로부터 변경을 받아요. 초기 테이블 동기화용 추가 슬롯은 보통 일시적이라, 동기화가 끝나면 자동으로 버려지고 pg_%u_sync_%u_%llu(파라미터: 구독 oid, 테이블 relid, 시스템 식별자) 같은 생성된 이름을 가집니다.
보통 원격 복제 슬롯은 CREATE SUBSCRIPTION으로 구독을 만들 때 자동 생성되고, DROP SUBSCRIPTION으로 구독을 버릴 때 자동 제거돼요. 그런데 구독과 복제 슬롯을 따로 다뤄야 유용한 상황이 몇 가지 있어요.
- 구독을 만들 때 복제 슬롯이 이미 존재한다면,
create_slot = false옵션으로 준비된 슬롯에 연결할 수 있어요. - 구독을 만들 때 원격 호스트에 연결할 수 없거나 상태가 불분명하다면
connect = false옵션을 써서 원격 호스트에 전혀 접촉하지 않게 만들 수 있어요. 이건 pg_dump가 쓰는 방식이에요. 이 경우 구독을 활성화하기 전에 원격 복제 슬롯을 수동으로 만들어야 해요. - 구독을 버릴 때 복제 슬롯을 유지하고 싶다면(예: subscriber DB를 다른 호스트로 옮겨 그곳에서 활성화할 예정),
DROP SUBSCRIPTION전에ALTER SUBSCRIPTION으로 슬롯을 구독에서 분리해둬요. - 구독을 버릴 때 원격 호스트에 연결할 수 없다면 역시
ALTER SUBSCRIPTION으로 슬롯을 분리한 뒤 버려야 해요. 원격 DB 인스턴스 자체가 사라졌다면 더 할 일이 없고, 단지 연결만 불가능한 상황이라면 복제 슬롯(과 남은 테이블 동기화 슬롯)을 수동으로 제거해야 해요. 그렇지 않으면 WAL을 계속 예약해서 결국 디스크가 가득 찰 수 있으니, 이런 경우는 신중히 조사해보는 게 좋아요.
예시: 논리 복제 셋업 살펴보기
개념을 코드로 확인해볼게요. publisher와 subscriber 양쪽에 같은 형태의 테스트 테이블 t1, t2, t3(각각 주 키 포함)을 만들고, publisher 쪽에 데이터를 넣어둡니다.
이어서 테이블별로 출판을 만드는데, pub2, pub3a는 특정 publish 연산을 막고 pub3b는 행 필터(e > 5)를 답니다.
/* pub # */ CREATE PUBLICATION pub1 FOR TABLE t1;
/* pub # */ CREATE PUBLICATION pub2 FOR TABLE t2 WITH (publish = 'truncate');
/* pub # */ CREATE PUBLICATION pub3a FOR TABLE t3 WITH (publish = 'truncate');
/* pub # */ CREATE PUBLICATION pub3b FOR TABLE t3 WHERE (e > 5);
이제 subscriber 쪽에 구독을 만듭니다. sub3는 pub3a와 pub3b 양쪽에 가입해요. 모든 구독은 기본적으로 초기 데이터를 복사합니다.
/* sub # */ CREATE SUBSCRIPTION sub1 CONNECTION 'host=localhost dbname=test_pub application_name=sub1' PUBLICATION pub1;
/* sub # */ CREATE SUBSCRIPTION sub2 CONNECTION 'host=localhost dbname=test_pub application_name=sub2' PUBLICATION pub2;
/* sub # */ CREATE SUBSCRIPTION sub3 CONNECTION 'host=localhost dbname=test_pub application_name=sub3' PUBLICATION pub3a, pub3b;
여기서 핵심 관찰 포인트가 있어요. 초기 데이터 복사는 publish 연산 설정을 무시합니다. 그래서 t2, t3도 초기 상태는 모든 행이 복사돼요. 특히 pub3a에 행 필터가 없으므로 복사된 t3는 pub3b의 행 필터와 안 맞는 행까지 전부 담게 되죠.
이후 publisher에 데이터를 추가하면, 정상 복제 중에는 publish 연산이 적용됩니다. 즉 pub2, pub3a는 INSERT를 복제하지 않고, pub3b는 자신의 행 필터에 맞는 데이터만 복제해요. 결과적으로 subscriber 쪽 t1은 6행 전부, t2는 3행(최초 복사분), t3는 4행(1·2·3 + 행 필터에 맞는 6)이 되는 차이가 섹션 예시에서 자세히 확인됩니다.
예시: 슬롯 생성 미루기 (Deferred Replication Slot Creation)
원격 복제 슬롯이 자동으로 만들어지지 않는 상황에서는, 구독을 활성화하기 전에 사용자가 직접 슬롯을 만들어야 해요. 표준 논리 디코딩 출력 플러그인은 pgoutput이고, 내장 논리 복제가 바로 이걸 씁니다. 먼저 CREATE PUBLICATION pub1 FOR ALL TABLES;를 만든 뒤 진행해요.
- connect = false인 경우:
CONNECTION 'host=localhost dbname=test_pub' PUBLICATION pub1 WITH (connect=false);로 구독을 만들고(연결되지 않았다는 경고가 나와요), publisher에서 슬롯 이름이 구독 이름과 같게 직접 생성합니다.SELECT * FROM pg_create_logical_replication_slot('sub1', 'pgoutput');그다음 subscriber에서ALTER SUBSCRIPTION sub1 ENABLE;와ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;으로 활성화를 마치면pub1테이블들이 복제를 시작해요. - connect=false + slot_name 지정:
WITH (connect=false, slot_name='myslot')처럼 만들면 publisher에서 같은 이름myslot으로 슬롯을 만들고, 나머지 활성화 단계는 동일해요. - slot_name = NONE:
WITH (slot_name=NONE, enabled=false, create_slot=false)로 만들고, publisher에서 임의 이름으로 슬롯을 만든 뒤ALTER SUBSCRIPTION sub1 SET (slot_name='myslot');으로 구독에 연결하고 활성화합니다.
더 알아보기 (Learn more)
- 출판 (Publication) — 내보낼 변경의 기준점
- 빠른 셋업 (Quick Setup) — 세 단계로 시작하기
- 복제 슬롯 (Replication Slots) — WAL 예약과 슬롯 관리
- 논리 복제 전체 (Logical Replication) — 개념과 아키텍처