变更早已在日志里
한국어 원문으로 표시합니다.
한 줄 요약
CDC(Change Data Capture)는 애플리케이션이 전문을 보내 주기를 기다리지 않고, 데이터베이스에 이미 일어난 변경을 읽어 다른 시스템으로 흘려보내는 연동 방식이다. 가장 믿을 만한 방법은 DB 의 트랜잭션 로그를 읽는 것이고, 그 대가로 로그 보존·스키마 변경·순서 같은 새로운 운영 책임이 생긴다.
왜 이게 필요했나
지금까지 이 코스의 연동은 모두 보내는 쪽이 보내 주는 구조였다. 계정계가 처리 결과를 허브에 넘기고, 허브가 정보계와 대외계로 나른다. 그런데 현장에는 이렇게 붙일 수 없는 시스템이 많다. 소스를 고칠 수 없는 패키지, 20년 된 원장 배치, 수십 개 화면이 같은 테이블을 직접 고치는 레거시가 그렇다. "변경이 생길 때마다 전문을 보내 주세요" 라는 요구는 그 시스템의 모든 쓰기 지점을 찾아 고치라는 말이고, 한 곳이라도 빠뜨리면 조용히 데이터가 어긋난다.
두 번째 이유는 이중 쓰기다. 애플리케이션이 DB 에 쓰고 나서 MQ 에도 발행하면, 둘 사이에서 프로세스가 죽는 순간 DB 에는 있고 MQ 에는 없는 변경이 생긴다. 두 자원을 하나의 트랜잭션으로 묶는 분산 트랜잭션은 무겁고 대부분의 브로커가 지원하지 않는다. DB 에 쓰는 것 하나만 트랜잭션으로 하고, 발행은 DB 가 이미 확정한 변경을 읽어서 하면 이 틈이 없어진다. CDC 가 그 읽는 쪽이다.
어떻게 동작하나
변경을 잡는 방법은 크게 세 가지다.
| 방식 | 원리 | 대가 |
|---|---|---|
| 조회(폴링) | updated_at > 마지막 시각 으로 주기적으로 읽는다 |
삭제를 못 본다. 같은 시각의 변경이나 늦게 커밋된 트랜잭션을 놓친다. 원본 DB 에 조회 부하 |
| 트리거 | 테이블 트리거가 변경 이력 테이블에 한 줄씩 쓴다 | 쓰기마다 트리거 비용. 트리거를 원본 DB 에 심어야 한다 |
| 로그 기반 | DB 의 트랜잭션 로그(PostgreSQL WAL, MySQL binlog)를 읽는다 | 로그 보존·권한·버전 호환을 운영해야 한다 |
로그 기반이 가장 믿을 만한 이유는 DB 가 커밋 순서대로 이미 기록한 것을 읽기 때문이다. 애플리케이션 코드가 어떤 경로로 썼든, 삭제든, 대량 수정이든 전부 로그에 남는다.
대표적인 오픈소스 구현이 Debezium 이다. PostgreSQL 커넥터는 논리 디코딩(logical decoding)으로 WAL 을 읽는다. 출력 플러그인은 PostgreSQL 10 이상에 기본으로 들어 있는 pgoutput 이나 Debezium 이 관리하는 decoderbufs 를 쓰고, 논리 디코딩을 쓰려면 원본 DB 의 wal_level 이 logical 이어야 한다(PostgreSQL 문서). MySQL 커넥터는 binlog 를 읽어 행 단위 INSERT·UPDATE·DELETE 를 이벤트로 만든다(Debezium MySQL).
처음 한 번은 스냅숏, 그다음은 스트리밍이다. 로그는 영원히 남지 않으므로, 커넥터는 처음 붙을 때 테이블 전체를 일관된 시점으로 읽어(스냅숏) 이벤트로 내보내고, 그 시점의 로그 위치부터 이어서 변경을 흘린다. 문서는 스냅숏 중에 읽은 로그 위치에서 스트리밍을 시작하므로 그 사이의 변경을 놓치지 않는다고 설명한다.
이벤트는 전과 후를 함께 싣는다. Debezium 변경 이벤트의 본문에는 before(변경 전 행), after(변경 후 행), source(어느 DB·테이블·로그 위치에서 왔는가), op(연산), ts_ms(처리 시각)가 있다. op 는 c 생성, u 수정, d 삭제, r 스냅숏 읽기, t 테이블 비우기다. PostgreSQL 에서 before 에 무엇이 담기는지는 테이블의 REPLICA IDENTITY 가 정한다 — 기본값(DEFAULT)이면 수정·삭제 이벤트에 기본 키 열의 이전 값만, FULL 이면 모든 열의 이전 값이 담긴다. "잔액이 얼마에서 얼마로 바뀌었나" 가 필요하다면 이 설정을 먼저 봐야 한다.
복제 슬롯은 로그를 붙잡는다. PostgreSQL 커넥터는 복제 슬롯으로 자기가 어디까지 읽었는지를 DB 에 남긴다. 커넥터가 멈춰도 DB 는 슬롯이 아직 읽지 않은 WAL 을 지우지 않는다 — 그래서 재시작하면 멈춘 자리부터 이어 읽는다. 뒤집어 말하면, 커넥터가 며칠 죽어 있으면 원본 DB 의 디스크가 WAL 로 찬다. CDC 를 붙이는 순간 원본 DB 운영에 감시 항목이 하나 는다. MySQL 쪽은 반대 방향의 위험이 있다. binlog 는 보존 기간이 지나면 지워지므로, 커넥터가 그보다 오래 멈추면 읽던 위치가 사라져 새 스냅숏이 필요하다고 문서는 적는다.
아웃박스 패턴과 함께 쓴다. 테이블 변경을 그대로 흘리면 소비자가 원본 테이블 구조에 묶인다(열 이름을 바꾸면 소비자가 깨진다). 그래서 업무 트랜잭션 안에서 발행할 이벤트를 outbox 테이블에 한 줄 함께 쓰고, CDC 는 그 outbox 테이블만 읽어 내보낸다. 원본 스키마는 숨기고, 이중 쓰기 문제는 없앤다. Debezium 은 outbox 테이블의 행을 이벤트로 바꿔 주는 Outbox Event Router 변환을 제공한다.
현장에서 만나는 모습
첫째, "변경분만 주세요" 를 updated_at 조회로 구현한 배치가 삭제를 영영 놓친다. 해지된 계좌가 정보계에는 몇 달째 살아 있다. 둘째, CDC 는 변경을 옮길 뿐 뜻을 옮기지 않는다. 원장 테이블의 행 변경 세 건(출금 행·입금 행·잔액 행)이 이체 한 건이라는 사실은 로그에 없다. 업무 이벤트가 필요하면 아웃박스로 뜻을 적어야 한다. 셋째, 전달은 최소 한 번이다. 커넥터가 재시작하면 이미 보낸 이벤트가 다시 올 수 있으므로 소비자는 8모듈의 멱등 처리를 그대로 해야 한다 — source 의 로그 위치나 이벤트 키가 그 근거가 된다. 넷째, 스키마 변경. 원본에 열이 추가되면 이벤트 모양이 바뀐다. 소비자 계약을 원본 테이블이 아니라 아웃박스의 이벤트 형식에 두는 이유가 이것이다.
EAI 와의 관계도 정리해 두자. CDC 는 중계 계층을 대체하지 않는다. 요청·응답이 필요한 거래(이체 승인, 한도 조회)는 여전히 동기 중계로 한다. CDC 는 이미 끝난 일을 다른 시스템이 알아야 할 때 — 정보계 적재, 검색 색인, 캐시 무효화, 알림 — 에 맞는다.
정리와 퀴즈
이 모듈은 실습 없이 개념을 정리한다. 폴링·트리거·로그 기반의 차이, 스냅숏과 스트리밍, 변경 이벤트의 모양, 복제 슬롯과 로그 보존의 운영 책임, 아웃박스를 퀴즈로 확인한다.