CDC 를 적용하여 견고한 EDA 설계하기

goyo·2026년 5월 28일

최근 진행한 PDF 변환 프로젝트에서 적용한 CDC 를 통해 견고한 Event-Driven-Architecture 를 구성한 경험을 리뷰하고자 한다.


1. AS-IS

기존의 메시징 비동기처리를 통한 데이터 흐름은 이러했다.

  1. 사용자의 변환 요청
  2. WAS 서버는 내부 비즈니스 로직을 통해 파일정보 파싱 및 데이터베이스 저장
  3. HTTP Polling Scheduling 으로 요청 테이블 스캔
  4. 파일정보 바인딩하여 메시지 브로커에게 퍼블리싱

발생할 수 있는 문제점

  1. Scheduling 의 특정 주기에 따른 동작 방식으로 인해 데이터 처리의 실시간성이 저하
  2. 데이터 유무와 상관없이 지속적인 쿼리 발생 및 스캔을 통한 데이터베이스 부하
  3. 트래픽이 급증하여 WAS 서버가 늘어나는 경우 중복되는 로우를 스캔하여 불필요한 자원 낭비 발생

2. TO-BE

API 서버와 메시지 퍼블리셔의 역할 분리

Debezium 을 별도의 독립 서버로 띄우기로 했다.

Debezium 선택 이유

  • Kafka 없이도 Debezium 서버의 아키텍처를 통해 RabbitMQ 를 Sink(목적지) 로 지원함
  • 다양한 DB 지원하여 확장성 용이함
  • 타 도구에 비한 정교한 캡처 가능 (before/after, metadata trasaction...)

독립 서버로 진행한 이유

  • WAS 서버와의 격리

    • DB 변경 로그를 읽어내거나 Json 포맷으로 직렬화하여 메시지큐로 쏘는 작업이 CPU,메모리,네트워크 I/O 자원을 꽤 소모한다.
      이에 따라 WAS 서버에 종속되는 임베디드 구축 방식은 트래픽 급증 시 서버의 자원이 고갈되어 뻗어버릴 수 있으므로 별개의 독립 서버를 통해 Debezium 전용 서버 리소스를 배정해주는 것이 안정적이라 판단했다.
      또한 독립 서버이므로 장애 전파 또한 막아낼 수 있다.
  • 낮은 결합도

    • API 서버의 생명주기와 CDC 파이프라인의 생명주기를 분리하여 API 서버가 여러번 재시작하더라도 독립된 Debezium 서버는 백그라운드 실행으로 지속적인 DB 로그를 읽어낼 수 있다.

3. 구현

디렉토리 구조

docker-compose.yml

services:
  debezium:
    image: quay.io/debezium/server:3.4
    container_name: event-server
    volumes:
      - ./debezium/config:/debezium/config
      - ./debezium/data:/debezium/data
    env_file:
      - ./debezium/config/application-local.properties


application.properties

# ---내보낼 플랫폼를 지정 (rmq)---
debezium.sink.type=rabbitmq

# ---RabbitMQ 설정---
debezium.sink.rabbitmq.connection.host=${RABBITMQ_HOST}
debezium.sink.rabbitmq.connection.port=5672
debezium.sink.rabbitmq.connection.username=${RABBITMQ_USERNAME}
debezium.sink.rabbitmq.connection.password=${RABBITMQ_PASSWORD}
debezium.sink.rabbitmq.exchange=${RABBITMQ_EXCHANGE}
debezium.sink.rabbitmq.routingKey=${RABBITMQ_ROUTINGKEY}

# ---DB 설정---
debezium.source.connector.class=io.debezium.connector.mysql.MySqlConnector
debezium.source.database.hostname=${DB_HOST}
debezium.source.database.port=3306
debezium.source.database.user=${DB_USERNAME}
debezium.source.database.password=${DB_PASSWORD}
debezium.source.database.connectionTimeZone=Asia/Seoul
debezium.source.database.server.id=15551
debezium.source.topic.prefix=cdc.event
debezium.source.database.include.list=api
debezium.source.table.include.list=api.outbox_event

# ---데이터 변환 설정---
# Key 를 JSON 포맷으로 변환
debezium.source.key.converter=org.apache.kafka.connect.json.JsonConverter
# 메시지 용량 최적화를 통해 스키마 정보 제거1
debezium.source.key.converter.schemas.enable=false
# Value 를 JSON 포맷으로 변경
debezium.source.value.converter=org.apache.kafka.connect.json.JsonConverter
# 메시지 용량 최적화를 통해 스키마 정보 제거2
debezium.source.value.converter.schemas.enable=false

# ---offset 및 스키마 히스토리 설정---
# Binlog 를 어디까지 읽었는지(offset) 기록할 파일 경로 지정
debezium.source.offset.storage.file.filename=data/offsets.dat
# offset 을 파일에 얼마나 자주 저장할지 주기 설정 (10초)
debezium.source.offset.flush.interval.ms=10000
# 테이블 변경 이력을 기록할 저장소 타입 지정(파일 시스템 사용)
debezium.source.schema.history.internal=io.debezium.storage.file.history.FileSchemaHistory
# 테이블 변경 이력 저장 경로 지정
debezium.source.schema.history.internal.file.filename=data/schemahistory.dat

별도의 Transaction Outbox 패턴을 적용했으므로,
감지 대상 테이블인 outbox_event 에서는 Insert 또는 주기적인 배치 Delete 만 진행할 예정이므로
특정 operation 에만 동작하도록 설정하진 않았다.

또한 yml 파일은 지원하지않으므로 가급적 properties 형식으로 작성해야한다.


4. TO-BE 아키텍처

  1. 사용자의 변환 요청
  2. WAS 서버는 내부 비즈니스 로직을 통해 파일정보 파싱 및 history 요청 데이터, outbox 에 payload(사용자 요청 메타데이터) 테이블에 저장
  3. Debezium 이 outbox 테이블의 insert log 를 감지하고 메시지 브로커에게 payload 를 전송

5. 자잘한 트러블 슈팅

1) Docker 컨테이너로 실행한 Debezium 이 실행 즉시 종료되는 현상 발생하여 Docker 로그를 확인해보니 애플리케이션의 8080 포트와 Debezium 포트가 충돌하는 것으로 확인. 포트를 변경해주어서 정상 실행되었다.

Debezium Server 는 내부적으로 Red Hat 의 경량 자바 프레임워크인 Quarkus 를 기반으로 빌드되어 있다. 이 Quarkus 의 기본 HTTP 서비스 포트가 8080이기 때문에, Debezium Server 역시 8080 포트를 사용하게 된다고 한다.


2) Debezium 이 정상적으로 메시지를 발행하나, 메시지큐에 도달하지못하는 상황이 발생하였다. 먼저 RabbitMQ Management UI 를 통해 로그를 확인해보았다.

이 경우 스프링 컨테이너에 RabbitAdmin 을 빈으로 등록해줘야한다.

RabbitMQ Admin Tracing 에서 Debezium 이 전달한 메시지를 확인해보았다.

문제 항목: Routing keys: [<<>>]

Debezium 이 RabbitMQ 에서 설정한 RoutingKey 를 찾지못하는 것 같았다.

AS-IS

TO-BE


결과

문제 원인은 Debezium 내부의 변환 필터나 속성명이 카멜 케이스 형식을 기반으로 라우팅 키를 자동 생성하거나 매핑하기 때문이었다. 최근 Debezium Server의 버전이 올라가면서 설정 사양이 하이픈을 사용하는 케밥 케이스에서 카멜 케이스로 표준화되었다고 한다.

위 내용을 적용하면서 내가 설정한 "cdc.event.message" 라는 라우팅 키가 성공적으로 바인딩된 것을 확인할 수 있었다.

0개의 댓글