박종훈 기술블로그
close menu

CDC 기반 Outbox 패턴에서 INSERT 직후 DELETE 해도 괜찮은 이유

발단: INSERT 직후 DELETE?

CDC 기반 Outbox 패턴을 사용하는 프로젝트에서 Outbox 테이블에 INSERT 하고 트랜잭션이 커밋되자마자 바로 DELETE 하는 구조의 코드를 보게 되었다. Outbox 패턴이라면 이벤트가 발행될 때까지 레코드를 보관해야 하는 것 아닌가? 삭제하면 뭘 기반으로 이벤트를 발행한다는 건지 생각이 들었다.

Outbox 패턴 복습

Outbox 패턴은 DB 트랜잭션과 메시지 발행의 원자성을 보장하기 위한 패턴이다. 비즈니스 데이터와 Outbox 테이블을 같은 트랜잭션에서 기록하면, 둘 다 커밋되거나 둘 다 롤백된다. 이렇게 하면 “DB에는 저장됐는데 이벤트는 안 나갔다”는 상황을 방지할 수 있다.

Outbox 테이블에서 이벤트를 꺼내 Kafka 같은 메시지 브로커로 보내는 방식은 크게 두 가지다.

  • 폴링(Polling): 스케줄러가 주기적으로 Outbox 테이블을 조회해서 미발행 레코드를 발행
  • CDC(Change Data Capture): DB의 변경 로그(WAL/binlog)를 실시간으로 읽어서 발행

폴링 방식에서는 당연히 발행이 완료될 때까지 레코드를 보관해야 한다. 스케줄러가 테이블을 직접 조회하기 때문이다. 그런데 CDC 방식에서는 이야기가 달라진다.

CDC는 테이블이 아니라 WAL을 읽는다

CDC의 핵심을 한 줄로 요약하면 이렇다.

CDC는 테이블의 “현재 상태”를 읽지 않는다. WAL(Write-Ahead Log) / binlog를 읽는다.

WAL은 DB에서 발생하는 모든 변경을 순서대로 기록하는 append-only 로그다. INSERT가 커밋되면 WAL에 기록이 남고, 그 뒤에 DELETE가 실행되어도 INSERT 기록은 WAL에 그대로 남아 있다.

WAL/binlog (append-only 로그)
┌─────────────────────────────┐
│ 1. INSERT outbox (id=123)   │  ← CDC가 이 이벤트를 캡처
│ 2. DELETE outbox (id=123)   │  ← INSERT와 별개의 이벤트
└─────────────────────────────┘

Debezium 같은 CDC 도구는 이 WAL을 순차적으로 읽으면서 모든 변경 이벤트(INSERT, UPDATE, DELETE)를 감지한다. 이 중 INSERT 이벤트만 Kafka로 발행하도록 필터링하는 것도 가능하다. 이 때 테이블에 데이터가 현재 존재하는지는 전혀 상관없다. WAL에 기록만 됐으면 CDC가 읽어간다.

전체 흐름

[애플리케이션]                    [DB]                     [Debezium CDC]
      │                           │                            │
      ▼                           │                            │
  INSERT outbox ─────────────→ WAL에 기록                      │
      │                           │                            │
      ▼                           │                            │
  트랜잭션 커밋                    │                            │
      │                           │                            │
      ▼                           │                            │
  DELETE outbox ─────────────→ WAL에 기록                      │
      │                           │                            │
      ▼                           │                       WAL 읽기
    완료                          │                            │
                                  │                            ▼
                                  │                    INSERT 이벤트 감지
                                  │                            │
                                  │                            ▼
                                  │                     Kafka로 발행

여기서 INSERT와 DELETE는 서로 다른 트랜잭션에서 실행된다. INSERT는 비즈니스 데이터와 같은 트랜잭션 안에서 이루어지고, 이 트랜잭션이 커밋되는 순간 WAL에 기록이 확정된다. DELETE는 커밋 이후 별도의 트랜잭션에서 실행되므로, INSERT 기록에는 영향을 주지 않는다. CDC는 WAL을 순서대로 읽기 때문에 INSERT 이벤트를 놓치지 않는다.

바로 DELETE 하는 이유

CDC 기반에서 Outbox 테이블의 레코드는 이벤트 발행에 필요하지 않다. 발행은 WAL을 통해 이루어지기 때문이다. 그렇다면 Outbox 테이블에 데이터를 쌓아둘 이유가 없다. 오히려 쌓아두면 문제가 된다.

  • 테이블 크기가 무한히 커진다: 이벤트가 발생할 때마다 레코드가 쌓이면 디스크를 소모하고, 인덱스도 비대해진다
  • 별도 정리 작업이 필요해진다: 배치로 오래된 레코드를 삭제하는 스케줄러를 추가로 관리해야 한다
  • 불필요한 I/O: 테이블이 커지면 관련 쿼리나 관리 작업의 비용도 올라간다

INSERT 직후 DELETE하면 Outbox 테이블은 항상 비어 있는 상태를 유지한다. 테이블이 일종의 통로(passthrough) 역할만 하는 셈이다. WAL에 흔적만 남기면 되니까.

폴링 vs CDC 비교

 폴링 방식CDC 방식
발행 주체스케줄러가 테이블 조회CDC가 WAL에서 감지
테이블 역할미발행 이벤트 저장소WAL 기록을 위한 통로
DELETE 시점발행 성공 확인 후INSERT 직후
테이블 크기발행 지연 시 커질 수 있음항상 비어 있음

CDC lag에 대해

INSERT 직후 DELETE 하는 구조에서 한 가지 주의할 점은 CDC lag이다. 예를 들어 아래와 같은 상황이 발생할 수 있다.

T+0s  - INSERT 커밋 → WAL 기록
T+0s  - DELETE 커밋 → WAL 기록
  ...   (CDC lag 수 초 ~ 수십 분)
T+N   - CDC가 WAL에서 INSERT 이벤트 읽음 → Kafka 발행

CDC 커넥터의 부하, 네트워크 상태, Kafka Connect 클러스터 상태 등에 따라 WAL을 읽는 데 지연이 발생할 수 있다. 하지만 이건 데이터 유실과는 다른 문제다. WAL에 기록은 남아 있기 때문에, 지연이 있더라도 결국 이벤트는 발행된다. 하지만 발행되기 전까지는 Outbox 테이블에서는 이미 삭제되었고 Kafka에는 아직 도착하지 않은 상태이므로, 데이터가 어디에도 없는 것처럼 느껴질 수 있다.

정리

CDC 기반 Outbox 패턴에서 기억할 것은 하나다.

진짜 데이터 소스는 Outbox 테이블이 아니라 WAL이다.

WAL에 INSERT가 기록되는 순간 이벤트 발행은 보장된다. 그래서 Outbox 테이블은 WAL에 기록을 남기기 위한 수단일 뿐이고, 역할을 다한 레코드는 바로 삭제해도 된다. 이렇게 하면 별도의 정리 작업 없이도 Outbox 테이블을 깨끗하게 유지할 수 있다.

CDC가 테이블의 데이터가 아니라 WAL을 읽는다는 것은 알고 있었지만, 이렇게 활용하는 것은 처음 봐서 신선하였기에 내용을 정리해보았다.