4P by GN⁺ | ★ favorite | 댓글 1개
  • Postgres 변경을 다른 시스템으로 실시간 전달하려면 CDC(Change Data Capture) 가 필요하며, 단순 알림부터 WAL 기반 복제까지 선택지마다 안정성과 운영 부담이 크게 다름
  • Listen/Notify는 가장 가볍게 시작할 수 있지만 at-most-once 전달, 일시적 알림, 8000바이트 페이로드 제한 때문에 핵심 CDC보다는 보조 신호에 가까움
  • 테이블 폴링과 감사 테이블(outbox pattern) 은 표준 테이블·트리거만으로 구현 가능하지만, 삭제 감지·diff·커밋 순서·쓰기 증폭·백프레셔를 직접 해결해야 함
  • 논리 복제(logical replication) 는 WAL에서 insert/update/delete를 스트리밍하는 강력한 방식이지만, replication slot, ack, 재시작, 처리량 대응까지 애플리케이션이 관리해야 함
  • Sequin은 Postgres 논리 복제를 기반으로 SQS, Kafka, Elasticsearch, Redis, HTTP 엔드포인트 등으로 변경을 전달해 replication slot을 직접 다루는 부담을 줄임

Postgres CDC가 필요한 상황

  • Postgres는 저장된 데이터를 다루는 데 강하지만, 테이블 변경으로 워크플로를 트리거하거나 다른 데이터 저장소·시스템·서비스로 실시간 스트리밍하려면 데이터 이동을 별도로 설계해야 함
  • Change Data Capture(CDC) 는 데이터베이스 변경을 식별·캡처한 뒤 downstream 시스템으로 실시간 전달하는 방식임
  • Postgres에서 변경을 잡는 방법은 여러 가지이며, 구현 난이도·안정성·운영 부담이 서로 다름

Listen/Notify: 가장 단순한 pub-sub

  • Postgres의 Listen/Notify는 프로세스 간 통신 기능이며 publish-subscribe 패턴으로 동작함
  • 세션은 특정 채널을 listen하고, 데이터베이스 활동이나 다른 세션은 해당 채널에 notify를 보낼 수 있음
  • 변경 캡처에는 트리거를 붙여 사용할 수 있음
    • 예시 트리거는 after insert or update or delete 시점에 변경된 레코드의 table, id, action을 JSON으로 만들고 pg_notify('table_changes', payload::text)를 호출함
  • 한계가 뚜렷함
    • at-most-once 전달 의미론을 가지며, listener가 알림 발행 시점에 연결되어 있어야 함
    • listener는 구독한 뒤부터의 알림만 받으므로 네트워크 문제로 잠시 끊겨도 알림을 놓칠 수 있음
    • 페이로드 크기 제한은 8000바이트이며, 이를 넘으면 notify 명령이 실패함
    • 페이로드 크기에는 채널명도 포함되고, Postgres 식별자처럼 채널명은 최대 64바이트일 수 있음
  • 기본 변경 감지나 테이블 폴링 최적화에는 쓸 수 있지만, 복잡한 CDC 요구에는 잘 맞지 않을 수 있음

테이블 폴링: 단순하지만 삭제와 diff가 약함

  • 가장 단순한 견고한 변경 캡처 방식은 테이블을 직접 폴링하는 것임
  • 각 테이블에는 행이 업데이트될 때마다 갱신되는 updated_at 같은 컬럼이 필요하며, 필요하면 트리거로 만들 수 있음
  • updated_atid 조합을 커서로 쓰고, 애플리케이션 로직이 커서를 저장·관리함
  • Notify 구독을 함께 쓰면 레코드 삽입·수정 사실을 애플리케이션에 알려 폴링 빈도를 줄일 수 있음
    • Postgres 알림은 일시적이므로 폴링 위의 최적화로만 쓰는 편이 적합함
  • 주요 단점은 세 가지임
    • 삭제된 행은 테이블에 남지 않으므로 삭제 감지가 불가능함
    • 보완책으로 delete 트리거가 id와 필요한 컬럼을 deleted_contacts 같은 별도 테이블에 저장하게 하고, 애플리케이션이 그 테이블을 폴링할 수 있음
    • 레코드가 업데이트됐다는 사실은 알 수 있지만 무엇이 바뀌었는지는 알 수 없음
    • Postgres datetime과 sequence는 커밋 순서가 어긋날 수 있어 updated_at 기준 블록을 읽는 동안 아직 커밋 중인 행을 놓칠 수 있음
  • 삭제, diff, 간헐적 누락이 큰 문제가 아닌 단순 변경 추적에는 합리적인 선택지임

감사 테이블: outbox pattern으로 변경 로그 저장

  • 감사 테이블(audit table) 방식은 별도 changelog 테이블에 변경을 기록하며, outbox pattern이라고도 불림
  • changelog에는 변경과 관련된 컬럼을 둘 수 있음
    • action: insert, update, delete 여부
    • old: 변경 전 레코드의 jsonb, insert에서는 비어 있음
    • values: 변경된 필드의 jsonb, delete에서는 비어 있음
    • inserted_at: 변경 발생 시각
  • 구현하려면 변경이 발생할 때마다 changelog에 삽입하는 트리거 함수와, 감시할 테이블별 트리거가 필요함
  • changelog를 큐처럼 다루는 소비 방식도 가능함
    • 애플리케이션 worker가 테이블에서 변경을 가져옴
    • 대략 exactly-once 처리를 위해 Postgres의 for update skip locked를 사용할 수 있음
    • worker는 트랜잭션을 열고 order by timestamp limit 100 for update skip locked로 묶음을 잠근 뒤 처리하고, 처리된 레코드를 삭제한 후 커밋할 수 있음
  • 운영상 단점이 있음
    • 단일 테이블 쓰기가 감사 테이블에 여러 쓰기를 만드는 쓰기 증폭(write amplification) 이 발생함
    • 일반적으로 감사 테이블 초기 insert, 처리 중 update, 처리 후 delete까지 최소 세 번의 쓰기가 생김
    • worker로 fan-out하는 방식은 애플리케이션에 맞춰 직접 설계해야 함
    • 프로덕션 규모 배포 전에는 트리거 함수와 테이블 설계를 조정해야 할 가능성이 있음
    • worker가 변경을 체크아웃한 채 유지할 수 있는 시간 제한 같은 세부 정책도 고려할 수 있음
    • worker가 성공적으로 처리하지 못해도 감사 테이블은 계속 차므로 백프레셔 관리가 부족함

Foreign Data Wrapper: 특정 Postgres 간 동기화에 가까운 선택지

  • Foreign Data Wrapper(FDW) 는 Postgres 데이터베이스에서 외부 데이터 소스를 읽고 쓸 수 있게 하는 기능임
  • 가장 널리 지원되는 FDW 기반 확장은 postgres_fdw
    • 두 Postgres 데이터베이스를 연결하고, 한 데이터베이스에서 다른 데이터베이스의 테이블을 참조하는 view와 비슷한 구조를 만들 수 있음
    • 내부적으로 한 Postgres 데이터베이스가 클라이언트가 되고 다른 데이터베이스가 서버가 됨
    • foreign table에 질의하면 클라이언트 데이터베이스가 Postgres wire protocol로 서버 데이터베이스에 질의를 보냄
  • FDW는 변경 캡처 방식으로 일반적이지 않으며, 매우 특정한 상황 밖에서는 권장되기 어려움
  • 한 Postgres 데이터베이스의 변경을 다른 Postgres 데이터베이스에 쓰려는 경우에는 FDW가 맞을 수 있음
    • 예시는 회계용 데이터베이스와 애플리케이션용 데이터베이스를 따로 쓰는 상황임
    • 중간 변경 캡처 단계를 건너뛰고 postgres_fdw로 데이터베이스 간 직접 반영할 수 있음
  • 직접 FDW를 만들어 내부 API에 변경을 POST하는 방식도 가능함
    • 커밋 안에서 API에 쓰기 때문에 API가 변경을 거부하고 커밋을 롤백할 수 있음
  • FDW는 강력하지만 CDC 용도로 최선이 되는 경우는 드물고, 직접 FDW를 작성하는 일은 변경 캡처 방식 중 가장 큰 작업에 가까움
    • 직접 FDW 작성은 Supabase wrappers 같은 도구로 쉬워졌지만 여전히 큰 작업임

직접 논리 복제: WAL 기반의 강력한 CDC

  • Postgres는 데이터베이스 복제를 위한 프로토콜을 갖고 있으며, 그중 하나가 논리 복제(logical replication)
  • 논리 복제는 Postgres의 WAL(write-ahead log) 위에 구축됨
    • 데이터베이스의 모든 insert, update, delete가 추적됨
    • 변경은 subscriber로 스트리밍됨
  • 사용자는 primary에 replication slot을 먼저 생성함
    • pg_create_logical_replication_slot('<your_slot_name>', '<output_plugin>') 형태를 사용함
  • output_plugin은 WAL 변경을 디코딩할 플러그인을 지정함
    • pgoutput은 기본 플러그인이며, 클라이언트 서버가 기대하는 바이너리 형식으로 출력함
    • test_decoding은 WAL 변경을 사람이 읽을 수 있는 형태로 제공하는 단순 출력 플러그인임
    • Postgres 내장 플러그인은 아니지만 인기 있는 플러그인으로 wal2json이 있으며, JSON은 Postgres 바이너리 형식보다 애플리케이션 시작점으로 다루기 쉬움
  • replication slot을 만든 뒤 시작하고 소비할 수 있음
    • replication slot은 표준 질의와 다른 Postgres 프로토콜 영역을 사용함
    • 여러 클라이언트 라이브러리는 replication slot 작업을 돕는 함수를 제공함
    • psycopg2 예시는 cursor.start_replication(...)cursor.consume_stream(...)으로 WAL 메시지를 소비하고, cursor.send_feedback(flush_lsn=msg.wal_end)로 ack를 보냄
  • 클라이언트는 받은 WAL 메시지를 ack해야 하며, replication slot은 offset을 가진 Kafka와 비슷하게 동작함
  • 논리 복제는 CDC를 위해 만들어진 견고한 방식이지만 복잡함
    • replication slot과 replication protocol은 일반 테이블·질의보다 개발자에게 덜 익숙함
    • 재시작 중 메시지를 놓치지 않는 전략이 필요함
    • Postgres에서 나오는 대량 메시지를 처리할 수 있도록 설계해야 함

Sequin: 논리 복제를 감싼 CDC 도구

  • Sequin은 Postgres 변경과 행을 큐·스트림·검색 인덱스·캐시·HTTP 엔드포인트 등으로 전달하는 CDC 도구임
  • 대상에는 SQS, Kafka, Elasticsearch, Redis, HTTP endpoints 등이 포함됨
  • Sequin은 내부적으로 Postgres 논리 복제를 사용하지만, low-level 프로토콜의 복잡성을 추상화함
  • insert, update, delete를 모두 캡처할 수 있으며, update와 delete에서는 행의 new 값과 old 값을 모두 캡처함
  • Sequin을 고려할 조건은 다음과 같음
    • 실시간 CDC가 필요함
    • SQS나 webhook 같은 목적지로 중간 시스템 없이 직접 스트리밍하고 싶음
    • 과거 데이터 backfill과 SQL where 절 기반 변경 필터링 같은 기능이 필요함
    • replication slot을 직접 관리하는 방식보다 단순한 대안이 필요함
    • exactly-once 처리 보장이 필요함
  • 단점도 있음
    • Sequin은 Postgres 내부 확장이 아니라 데이터베이스 옆에서 실행되는 서드파티 도구
    • 확장이 아니기 때문에 어떤 Postgres 데이터베이스와도 폭넓은 호환성을 갖지만, Sequin Cloud를 쓰지 않는다면 추가 인프라를 직접 세워야 함

선택 기준

  • 시작 단계에서는 Listen/Notify와 테이블 폴링이 적합함
    • Listen/Notify는 중요하지 않은 이벤트 캡처, 프로토타이핑, 폴링 최적화에 좋음
    • 폴링은 단순한 사용 사례에 무난하고 직선적인 해법임
  • 조금 더 진지한 단계에서는 감사 테이블이 중간 선택지가 될 수 있음
    • 행의 newold payload를 캡처할 수 있음
    • 제대로 만들면 exactly-once 처리 시스템을 얻을 수 있음
    • 확장 시 쓰기 증폭과 백프레셔 부재가 문제가 되고, 수동 구성에서 실수하면 메시지를 떨어뜨릴 수 있음
  • 확장 단계에서는 논리 복제가 견고한 해법에 가장 가까움
    • 다만 slot에서 직접 읽기보다 Sequin 같은 도구를 쓰는 쪽이 권장됨
  • FDW는 흥미로운 기능이지만, 일반적인 CDC 요구를 해결할 가능성은 낮음

댓글과 토론

Hacker News 의견들
  • 트리거 + 히스토리 테이블(감사 테이블)이 98%의 경우 정답임. 이미 쓰고 있지 않다면 오늘부터 쓰면 됨. 30년 넘게 검증되어 온 기법임
    제네릭하게 구현하는 간단한 예시는 https://gist.github.com/slotrans/353952c4f383596e6fe8777db5d...에 있음. 공간 효율은 포기하고 “쉬운 구현”을 택한 방식임
    불변 데이터를 저장할 수 있다면 정말 좋지만, 데이터베이스에는 아마 가변 데이터가 엄청나게 많고 매일 많은 걸 잊고 있을 가능성이 큼. 잊지 말고 히스토리 테이블을 쓰면 됨
    참고: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...
    Papertrail 같은 애플리케이션 계층의 히스토리 추적 라이브러리나 기법은 쓰지 않는 게 좋음. 느리고 오류가 나기 쉬우며, 앱 스택을 우회하는 DB 변경을 잡지 못함. updated 타임스탬프를 앱에서 찍으려는 시도도 근본적으로 틀렸는데, 각 웹 서버마다 시계가 다르기 때문임. DB 시계를 써야 하고, 그게 유일하게 맞는 시계임

    • 일관성을 위해서는 클라이언트에서 시간을 만들지 말고 쿼리 안에 now() 같은 호출을 넣어 DB 시계를 쓰는 게 맞음
      하지만 이 타임스탬프만으로 동기화하는 건 충분하지 않음. 타임스탬프가 트랜잭션 커밋 시점이 아니라 트랜잭션 시작 시점에 생성되기 때문임
      테이블을 폴링하면서 최근 타임스탬프로 필터링하면, 커밋 순서가 뒤섞인 트랜잭션 일부를 놓칠 수 있음. 몇 분 더 과거까지 조회하고 중복을 제거하는 완충 구간을 둘 수는 있지만, PostgreSQL에서는 트랜잭션 시간이 무제한이고 너무 과거까지 조회하면 낭비가 큼. 정확성과 효율이 중요하면 이 방식은 맞지 않음
    • Estuary(https://estuary.dev, CTO임)는 운영 DB에 추가 설정 없이, 클라우드 스토리지에 데이터베이스 변경 전체의 실시간 데이터 레이크 변경 로그를 만들어 줌
      로그 순서 번호, DB 시간, REPLICA IDENTITY FULL을 쓰면 변경 전/후 상태까지 포함됨. 이후 Snowflake 같은 곳에 컬렉션을 구체화하면 소스 DB 업데이트를 따라가는 동기화 테이블을 기본으로 얻을 수 있음
      같은 기반 데이터 레이크에서 감사 목적의 전체 테이블 히스토리를 변환하거나 구체화할 수도 있어서, 소스 DB에 다시 캡처나 WAL 리더를 붙일 필요가 없음
    • 트리거에서 세션 변수를 참조하면 변경 이유에 대한 코멘트 같은 추가 정보를 히스토리에 넣을 수 있었음. 작은 개인 프로젝트에서만 해봤지만 지금까지는 잘 동작함
    • 예시를 SQLite로 포팅하고 동작을 시연해 봄: https://chat.openai.com/share/b5113cb1-10df-4a38-adde-5ec0e7...
      비슷한 패턴을 JSON이 아니라 컬럼 기반으로 구현한 SQLite 방식도 따로 설명해 둠: https://simonwillison.net/2023/Apr/15/sqlite-history/
    • 이 접근은 좋고, 실제로 앱의 활동 피드도 이렇게 만들고 있음. 다만 “변경을 밖으로 푸시”하는 문제 자체는 해결하지 않음. 물론 감사 테이블의 WAL 변경을 리스닝하면 두 장점을 모두 얻을 수 있음
  • 이 글은 Postgres 기본 기능으로 가능한 여러 접근을 간결하게 잘 정리함
    “감사 테이블에 변경 캡처” 부분에서는 이전 회사에서 Temporal Tables 패턴을 좋게 썼음. 다른 주요 관계형 DBMS와 달리 Postgres 자체에는 내장되어 있지 않지만, SQL 함수로 활용할 수 있는 단순한 패턴이 있음: https://github.com/nearform/temporal_tables
    특정 시점의 테이블 상태를 볼 수 있어 “8월 12일 이 사용자의 설정은 무엇이었나”, “어젯밤 11:55에 미처리 레코드는 몇 개였나”, “현재와 일주일 전의 기능 플래그 차이를 보여 달라” 같은 질문에 답할 수 있음

  • 예전에 아주 큰 모놀리식 SQL Server를 가진 회사에서 컨설팅한 적이 있음. Postgres는 아니었지만 Postgres였다고 가정해도 비슷함
    수십 년 동안 운영되면서 회사 안의 온갖 용도로 쓰였고, 사실상 회사 전체의 모든 애플리케이션과 업무 프로세스가 이 데이터베이스에 데이터를 저장하고 있었음
    문제는 이 DB를 조회하는 애플리케이션이 많고, 데이터를 삽입·수정하는 프로세스와 절차도 엄청나게 많아서, 상류 삽입·수정 프로세스가 바뀌거나 새로 추가되면서 애플리케이션 수준의 불변조건을 깨는 일이 생겼다는 점임. 정상 프로세스도 나쁜 데이터가 있으면 다르게 동작했음
    원인을 추적하기가 매우 어려웠는데, 들여다보는 것들이 대개 10년 전에 작성됐고 그 직원들은 이미 회사를 떠난 뒤였기 때문임
    Postgres 데이터베이스의 변경을 어떤 DAG 형태로 캡처해서, 어떤 프로세스가 데이터를 삽입·수정·삭제하는지와 역사적으로 어떻게 행동했는지, 여러 애플리케이션이 이 데이터를 어떻게 조회하는지와 쿼리 통계가 시간에 따라 어떻게 변하는지 알 수 있을까 궁금함
    이런 선행 사례가 있는지, 어떤 접근이면 이런 도구를 만들 수 있을지 잘 모르겠음. 예전에 비슷한 걸 만들 생각을 해봤지만, 좋은 선택을 하려면 Postgres 코어 엔지니어 수준의 이해가 필요한 영역 같음

    • Postgres의 논리 복제에는 다른 데이터베이스에서 같은 상태를 논리적으로 재생성하기 위한 변경 문장, 즉 삽입·수정·삭제 정보가 모두 들어 있음
      각 변경마다 클라이언트 수준의 출처 데이터까지 얻지는 못함
      그래도 우회는 가능함. 논리 복제 스트림은 pg_logical_emit_message 함수의 정보 메시지도 포함할 수 있으니, 클라이언트가 직접 메타데이터를 넣을 수 있음. 각 트랜잭션 시작 시 클라이언트 식별자를 내보내도록 설정할 수 있을지도 모름
    • 쿼리는 어떻게 처리할지 모르겠지만, 삽입·수정에는 이벤트 출처(last updated by)를 추적하는 컬럼을 둠. 안티패턴일 수도 있어서 더 견고한 해법이 있으면 좋겠음
    • 기술적으로 로그 복제에는 모든 주체가 한 모든 작업이 들어 있고, 트리거를 조심해서 쓰면 DDL/DML 캡처 테이블로도 전부 추적할 수 있음. DCL이 걱정되면 그것도 포함 가능함
      이런 접근은 WAL이나 트리거를 쓰는 거의 모든 SQL 계열 솔루션에서 동작함
      SQL Server에서 트리거 방식은 여러 번 써봤지만 모든 쿼리를 로깅하면 느려지는 경향이 있음. 운영을 막지 않는 삽입 메커니즘을 설계하는 건 완벽하지 않고, 샘플링이 필요할 수 있음
    • 애플리케이션마다 자체 DB 사용자를 갖게 하는 것만으로도 꽤 많은 정보를 얻을 수 있음
    • DB에 쿼리를 보내는 모든 스크립트와 프로그램을 훑어서, 쿼리마다 해당 스크립트·프로그램으로 연결되는 고유 ID 주석을 붙이는 아이디어가 있었음. 쿼리 로그에 그 주석과 ID가 남으면 출처를 추적할 수 있을 것 같음
  • “감사 테이블” 경로로 간다면 그냥 pgaudit을 쓰면 됨. 실전에서 검증된 확장이고, AWS를 쓰면 RDS에서도 사용 가능함
    https://github.com/pgaudit/pgaudit/blob/master/README.md
    https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appen...

  • 굳이 하지 않아도 됨. 이걸 원한다는 건 Postgres의 관계를 계약으로 바꾸는 것임. 어떤 서비스도 내부 상태를 영속화할 수 없게 됨
    도메인 주도 설계에 정말로 전념한다면 가능할 수 있지만, 가벼우면서도 실제적인 이벤트 기반 시스템을 쓰는 편이 나음

    • 데이터베이스의 관계는 좋든 싫든 이미 계약
      이벤트 기반 무언가는 1000배 더 복잡함
  • updated_at 컬럼을 폴링하는 방식은 가장 단순한 형태에서는 견고하지 않음. 트랜잭션이 그 순서대로 커밋된다는 보장이 없기 때문임

    • 글쓴이임. 좋은 지적임. 예를 들어 트랜잭션 A가 시작되어 before 트리거가 실행되고 Row 1의 updated_at2023-09-22 12:00:01로 설정됨
      잠시 뒤 트랜잭션 B가 시작되어 Row 2의 updated_at2023-09-22 12:00:02로 설정되고, B가 먼저 커밋됨
      폴링 쿼리가 실행되어 Row 2를 최신 변경으로 보고 커서를 2023-09-22 12:00:02로 갱신한 뒤, A가 나중에 커밋되면 Row 1을 놓치게 됨
      이 문제를 피하는 단순한 방법은 거의 실시간으로 폴링하지 않는 것임. 순서는 결국 일관적으로 맞춰짐
      더 견고한 제안은 시퀀스를 쓰는 것일 수 있음. 예를 들어 행이 변경될 때마다 증가하는 updated_at_idx 컬럼을 두는 방식임
    • 이건 처음 알았음. 컬럼을 갱신하는 데 트리거를 써도 그런가?
      now()를 넣는 before 트리거를 사용해도 두 행의 updated_at 타임스탬프가 트랜잭션 커밋 순서와 다를 수 있는지 궁금함. updated_at과 커밋 타임스탬프가 같을 필요는 없지만, updated_at은 밀리초·마이크로초 단위로 커밋 순서를 정확히 나타내야 함
    • 폴링에는 updated_at 대신 트리거가 현재 트랜잭션 ID로 설정하는 _txid 컬럼을 씀. 이후 폴링할 때 txid_current()로 어떤 트랜잭션이 커밋됐고 어떤 것이 아직인지 확인함
      약간 아슬아슬하고 경계값 오류를 내기 매우 쉽지만, 몇 년째 운영에서 잘 돌아가고 있음
  • 글이 훌륭함
    Elixir와 Postgres를 쓴다면, 비슷한 접근으로 WAL 변경을 리스닝하는 작은 라이브러리를 만들어 둠: https://github.com/cpursley/walex

  • 이 방식들은 다 좀 별로고, 개인적으로는 폴링이 가장 실용적이라고 봄
    Postgres가 이 영역에서 혁신하면 좋겠음

    • SQL 표준에 여러 종류의 시간성을 일급 기능으로 넣으려는 시도들이 있었음
      SQL 표준에 들어가기 전까지는 관계형 DBMS의 커널 공간에서 추진력이 생기기 어렵다고 봄. 선택지는 많고 복잡하며, 사용자 공간의 성공적인 해법들도 성능 면에서 과도한 부담이 있는 편은 아님
      참고로 이 분야를 연구하는 사람들은 대체로 감사 테이블 접근으로 기움. 데이터베이스 안에서 일관된 ACID 성질을 유지하고, 프록시나 폴링 작업을 추가하는 대신 Postgres를 단일 장애 지점으로 유지하기 때문임
    • 1초 폴링 간격이 실용적인가?
  • 데이터 세계에는 큰 빈틈이 있음. 데이터 저장소에 결과를 물어보는 대신, 쿼리 결과가 점진적으로 푸시되면 좋겠음
    실시간·스트리밍 분석을 많이 하는데, 스트림 처리도 할 수 있고 데이터 저장소 안에서 구체화 뷰로 일부 처리할 수도 있음. 하지만 데이터가 DB나 데이터 레이크에 들어간 뒤에는 하류에서 변경을 보려면 사실상 다시 폴링으로 돌아감
    데이터 안에서 어떤 상황이 발생했을 때 반응하거나, 페이지 새로고침 없이 화면을 갱신하려면 깔끔한 해법이 별로 없음. 이 글의 해법들도 일급 기능이라기보다 우회에 가까워 보임
    페이지 새로고침 없이 실시간으로 갱신되는 리포트를 만들고 싶다면, 보통 DB에서 데이터를 로드한 뒤 Kafka와 WebSocket으로 GUI에 변경을 흘리는 방식이 됨. 그러면 일부 분석은 코드로, 일부는 DB로 처리하는 이상한 람다 아키텍처를 운영하게 됨
    이 영역의 혁신은 있음. KSQL과 Kafka Streams는 변경을 내보낼 수 있고, Materialize에는 구독이 있으며, ClickHouse에는 라이브 뷰가 있음. 다만 많은 기능이 새롭거나 미리보기 단계이고 딱 맞지는 않음. 다 써봤지만 개발자에게 너무 많은 작업을 떠넘긴다고 느낌
    [select * from orders with suscribe] 같은 옵션으로 바로 변경 피드를 받을 수 있는 라이브러리가 있으면 좋겠음. 충분히 중요한 영역인데 관심을 덜 받아왔음

  • 글에서 다루지 않은 복제의 큰 함정이 있고, 그래서 나는 복제를 쓰지 않음
    Postgres는 복제 슬롯의 소비자가 데이터를 놓치지 않도록 매우 강하게 보장하려 함. 그래서 소비자가 슬롯에서 데이터를 소비하지 않으면 Postgres는 놓친 데이터를 친절하게 계속 보관하고, 결국 디스크가 가득 차서 DB가 쓰러질 때까지 감. 프로토타이핑 중 서로 다른 SaaS DB 두 곳에서 겪었고, 복구하려면 지원 티켓을 넣는 수밖에 없었음
    복제 슬롯 소비자가 읽기를 멈추면 반드시 알림이 울려야 함
    또 다른 이유는 테이블의 초기 스냅샷을 가져오는 코드 경로와 변경을 읽는 코드 경로가 완전히 다르다는 점임. 변경을 하나도 놓치지 않도록 복제 슬롯 읽기를 초기화하는 건 사소하지 않음
    아쉽게도 변경 캡처 관점에서는 복제가 가장 덜 해키한 해법임
    나는 폴링을 쓰지만, updated_at 대신 txid를 저장함

    • 슬롯이 공간을 계속 붙잡는 대신 일정 크기를 넘으면 무효로 표시되도록 크기 제한을 설정할 수 있음: https://www.postgresql.org/docs/current/runtime-config-repli...
      어떤 동작을 더 원하는지 궁금함
      큰 데이터 볼륨을 다룬다면 초기 스냅샷과 변경 읽기는 다르게 처리하고 싶어짐. 병렬 초기화나 물리 백업 기반 초기화 같은 작업이 가능해야 하기 때문임. 다만 슬롯 생성 뒤 기존 데이터를 선택적으로 스트리밍해 주는 기능이 유용할 수 있다는 점은 이해됨
      변경을 놓치지 않도록 복제 슬롯 읽기를 초기화하는 부분은 어려워서는 안 될 것 같은데, 어디에서 막혔는지 궁금함
    • 첫 번째 문제를 다루는 요령 중 하나는 논리 디코딩 메시지를 자기 자신에게 보내는 것임. 그러면 보관되는 WAL을 낮게 유지할 수 있음
      모든 변경이 필요하지 않을 때는 연결이 끊기면 스스로 정리되는 임시 복제 슬롯도 유용함. 서버를 죽이지 않도록 보관 WAL의 최대치를 설정하는 구성도 있음
    • 이 함정에 걸린 적이 있음. 정말 미묘함. 소비자를 제거하면 주 DB에는 아무 영향이 없어야 할 것처럼 느껴지지만, 실제로는 시한폭탄이 생김
      updated_at 대신 txid를 어떻게 쓰는지 더 설명해 주면 좋겠음