변경은 이미 로그에 있다
한 줄 요약
CDC(Change Data Capture)는 애플리케이션이 전문을 보내 주기를 기다리지 않고, 데이터베이스에 이미 일어난 변경을 읽어 다른 시스템으로 흘려보내는 연동 방식이다. 가장 믿을 만한 방법은 DB 의 트랜잭션 로그를 읽는 것이고, 그 대가로 로그 보존·스키마 변경·순서 같은 새로운 운영 책임이 생긴다.
왜 이게 필요했나
지금까지 이 코스의 연동은 모두 보내는 쪽이 보내 주는 구조였다. 계정계가 처리 결과를 허브에 넘기고, 허브가 정보계와 대외계로 나른다. 그런데 현장에는 이렇게 붙일 수 없는 시스템이 많다. 소스를 고칠 수 없는 패키지, 20년 된 원장 배치, 수십 개 화면이 같은 테이블을 직접 고치는 레거시가 그렇다. "변경이 생길 때마다 전문을 보내 주세요" 라는 요구는 그 시스템의 모든 쓰기 지점을 찾아 고치라는 말이고, 한 곳이라도 빠뜨리면 조용히 데이터가 어긋난다.
두 번째 이유는 이중 쓰기다. 애플리케이션이 DB 에 쓰고 나서 MQ 에도 발행하면, 둘 사이에서 프로세스가 죽는 순간 DB 에는 있고 MQ 에는 없는 변경이 생긴다. 두 자원을 하나의 트랜잭션으로 묶는 분산 트랜잭션은 무겁고 대부분의 브로커가 지원하지 않는다. DB 에 쓰는 것 하나만 트랜잭션으로 하고, 발행은 DB 가 이미 확정한 변경을 읽어서 하면 이 틈이 없어진다. CDC 가 그 읽는 쪽이다.
CDC 는 애플리케이션이 보내 주기를 기다리지 않고 이미 일어난 변경을 읽어 다른 시스템으로 흘린다. 본문의 세 방식 가운데 가장 흔한 조회와 가장 믿을 만하다는 로그 기반을 견준다.
- 조회: updated_at 이 마지막 시각보다 큰 행을 읽는다삭제를 보지 못한다. 같은 시각의 변경이나 늦게 커밋된 트랜잭션도 놓치고, 원본 DB 에 조회 부하를 준다.
- 로그 기반: DB 가 커밋 순서대로 이미 기록한 로그를 읽는다PostgreSQL 은 WAL, MySQL 은 binlog 이다. 애플리케이션이 어떤 경로로 썼든 삭제든 대량 수정이든 로그에 남는다. 대가는 로그 보존과 권한과 버전 호환을 운영하는 일이다.
여기서 구분할 것 PostgreSQL 문서가 뒷받침하는 것은 논리 디코딩이 WAL 의 내용을 해석해 변경 흐름을 만들고 복제 슬롯이 변경을 원본에서 일어난 순서로 내보낸다는 것까지이다. 조회가 삭제를 놓친다는 것과 트리거 방식의 대가는 본문의 서술이다. 질문의 시각은 설명용 예시이다.
잠깐, 예측해 보세요 배치가 10시 정각에 updated_at 이 마지막 시각보다 큰 행을 읽고 마지막 시각을 10시로 갱신했다. 9시 59분에 시작한 트랜잭션이 10시 1분에 커밋되었고 그 행의 updated_at 은 9시 59분이다. 다음 배치는 이 행을 읽을까?
설명 확인 · 채점 없는 자가 점검
읽지 못한다. 조건이 마지막 시각보다 큰 행만 고르는데 이 행의 시각은 이미 지나간 9시 59분이기 때문이다. 본문이 조회 방식의 약점으로 든 늦게 커밋된 트랜잭션을 놓친다는 말이 이런 경우이다. 로그 기반은 DB 가 커밋 순서대로 기록한 것을 읽는다.
어떻게 동작하나
변경을 잡는 방법은 크게 세 가지다.
| 방식 | 원리 | 대가 |
|---|---|---|
| 조회(폴링) | 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 는 이미 끝난 일을 다른 시스템이 알아야 할 때 — 정보계 적재, 검색 색인, 캐시 무효화, 알림 — 에 맞는다.
로그는 영원히 남지 않아서 커넥터는 처음 붙을 때 테이블 전체를 읽고 그다음 변경을 이어서 흘린다. 본문의 순서대로 옮겼다.
- 스냅숏: 일관된 시점의 테이블 전체를 읽는다테이블 전체를 이벤트로 내보낸다. 이때 이벤트의 op 는 r 이다.
- 그 시점의 로그 위치부터 변경을 스트리밍한다문서는 스냅숏 중에 읽은 로그 위치에서 스트리밍을 시작하므로 그 사이의 변경을 놓치지 않는다고 설명한다. 변경은 c, u, d 이벤트로 나온다.
- 멈췄다 재시작하면 멈춘 자리부터 이어 읽는다복제 슬롯이 어디까지 읽었는지를 DB 에 남긴다. 전달은 최소 한 번이라서 커넥터가 재시작하면 이미 보낸 이벤트가 다시 올 수 있다.
여기서 구분할 것 PostgreSQL 문서가 뒷받침하는 것은 슬롯을 만들 때 내보낸 스냅숏이 그 이후의 모든 변경이 변경 흐름에 들어가는 시점의 상태를 보여 준다는 것, 슬롯이 연결과 무관하게 남는다는 것, 비정상 종료 뒤에는 최근 변경이 다시 올 수 있어 소비자가 중복을 걸러야 한다는 것이다. 커넥터의 스냅숏 절차와 op 코드는 Debezium 문서를 근거로 한 본문의 서술이다.
잠깐, 예측해 보세요 스냅숏을 읽는 동안 원본에서 어떤 계좌 행이 바뀌었다. 그 변경은 스냅숏에도 스트리밍에도 잡히지 않고 사라질 수 있을까?
설명 확인 · 채점 없는 자가 점검
본문이 인용하는 문서에 따르면 사라지지 않는다. 스냅숏 중에 읽은 로그 위치에서 스트리밍을 시작하므로 그 사이의 변경도 흘러간다. 다만 재시작 때 같은 이벤트가 다시 올 수 있어서 소비자는 멱등하게 처리해야 한다.
정리와 퀴즈
이 모듈은 실습 없이 개념을 정리한다. 폴링·트리거·로그 기반의 차이, 스냅숏과 스트리밍, 변경 이벤트의 모양, 복제 슬롯과 로그 보존의 운영 책임, 아웃박스를 퀴즈로 확인한다.