티스토리 뷰
서비스가 어느 정도 자리 잡고 나면 한 번씩 겪는 순간이 있다. 누가 분석 쿼리 하나 던지면 운영 DB가 픽픽 쓰러지기 시작하는 순간.
우리도 그랬다. 비개발 직군에서 "이번 달 액티브 유저 수가 얼마야?", "이 콘텐츠 통계 좀 뽑아줘" 같은 요청이 늘어나면서 운영 DB CPU가 점점 천장을 찍기 시작했다. 처음엔 슬로우 쿼리만 잡으면 됐는데 어느 순간부터는 그것만으론 부족했다. 분석용 데이터를 따로 떼어내야 할 시점이 온 거다.
그래서 CDC(Change Data Capture) 파이프라인을 구축하게 됐다. 보통 CDC 하면 Debezium을 떠올리는데, 우리는 Kafka Connect의 JDBC source connector로 갔다. 오늘은 왜 그렇게 골랐는지에 대한 얘기다.
우리가 풀어야 했던 진짜 문제
처음엔 단순하게 봤다. 운영 DB에 부담 안 주면서 분석 쿼리 돌릴 수 있는 환경만 만들면 된다. 그래서 처음 떠올린 게 read replica. 운영 DB 복제본 하나 더 띄워서 분석은 거기서 돌리자는 거다.
근데 이 옵션엔 문제가 있었다.
우리 운영 DB는 정규화가 잘 되어 있다. 비즈니스 로직 짤 땐 이게 좋다. 무결성 보장되고, 업데이트 충돌도 적고.
근데 분석 쿼리 입장에선 답답하다. 화면 하나 보여주려고 4~5개 테이블을 조인하고, 그 안에서 또 group by, 또 join. 인덱스 잘 걸어봤자 분석에 친화적인 구조는 아니다.
분석 환경엔 비정규화된 구조가 필요했다. 자주 조인되는 테이블은 미리 합쳐두고, 자주 계산되는 값은 미리 계산해놓는 그런 구조. 보통 데이터 마트라고 부르는 패턴이다.
그래서 정확한 요구사항은 이렇게 정리됐다.
운영 DB의 변경 사항을 실시간으로 분석 DB에 전달하되, 동기화 시점에 비정규화·조인 변환도 함께 해야 한다.
이 한 줄에 우리가 어떤 도구를 고를지의 트레이드오프가 다 들어 있었다.
Debezium을 안 쓴 이유
CDC 관련해서 검색하면 거의 항상 Debezium이 나온다. 로그 기반 CDC의 사실상 표준이고, 강력하다. Binlog를 읽어서 변경 이벤트를 그대로 Kafka로 흘려보낸다. DB 부하 거의 없고, 정확하고, 지연도 짧다.
그래서 처음엔 Debezium으로 가려고 했다. 도큐먼트 읽고, 데모 환경 띄워보고, 잘 도는 것까지 확인했다. 근데 진행하다가 이상한 게 보였다.
Debezium은 운영 DB에서 일어난 변경을 그대로 복제해주는 도구다. INSERT면 INSERT, UPDATE면 UPDATE. 토픽엔 변경된 행 데이터가 들어간다. 이걸 받아서 다른 DB에 적재하면 1:1 복제가 된다.
근데 우리에게 필요한 건 1:1 복제가 아니었다. 우리는 동기화 과정에서 여러 테이블을 조인하고, 비즈니스 의미가 살아있는 형태로 변환해서 분석 DB에 넣어야 했다. 예를 들어 사용자 활동 통계 마트를 만들려면 여러 테이블의 정보를 미리 합쳐서 하나의 비정규화 행으로 만들어야 한다.
Debezium은 그걸 안 한다. 일부러 안 한다. 정확한 변경 캡처에 집중하고, 변환은 다음 단계(Kafka Streams, Flink 같은 스트림 처리 엔진)에 맡기는 게 Debezium 쪽 방식이다.
근데 우리 회사 상황을 보면, 백엔드 개발자 몇 명이 데이터 인프라까지 분담해서 보고 있었다. 별도 데이터 엔지니어 팀도 없다. 이 상황에서 Kafka Streams나 Flink 클러스터까지 새로 띄워서 운영할 여유는 없었다. 변환 단계를 별도 인프라로 떼어내는 건 우리한텐 좀 과했다.
여기서 JDBC source connector가 매력적이었던 이유가 나온다.
JDBC source connector를 선택한 이유
Kafka Connect에 포함된 JDBC source connector는 단순하다. SQL 쿼리를 주기적으로 실행해서 결과를 Kafka 토픽으로 보낸다.
이게 왜 우리한테 잘 맞았냐면, 쿼리를 우리가 직접 정의할 수 있어서다.
토픽 단위로 "여긴 이런 형태의 데이터가 들어가야 해"라고 정해두고, 그 SQL 안에 조인이든 case 문이든 다 박아 넣을 수 있다.
대충 이런 느낌이다.
topic: message.created
query: |
SELECT
u.id AS user_id,
u.country_code,
COUNT(DISTINCT m.id) AS message_count,
SUM(p.amount) AS total_purchase,
...
FROM users u
LEFT JOIN messages m ON ...
LEFT JOIN user_purchases p ON ...
WHERE u.updated_at > ?
동기화하면서 비정규화까지 같이 처리되는 셈이다. 변환용 별도 인프라가 필요 없다.
물론 단점도 명확하다.
- 로그 기반이 아니라 폴링 기반이다. 주기마다 쿼리를 돌려서 변경을 감지한다. 그래서 실시간성이 Debezium보다 떨어진다. 우리는 분석용이라 N분 단위 지연은 허용 가능했다.
- 운영 DB에 부하를 준다. 폴링 쿼리가 운영 DB에 직접 가니까. 그래서 timestamp나 incrementing 컬럼에 적절한 인덱스를 걸어야 한다.
- DELETE를 잡지 못한다. SQL 쿼리 결과에 잡히지 않으니까. 우리는 hard delete를 안 쓰는 정책이라 영향 없었다.
- 스키마 변경에 약하다. 컬럼이 바뀌면 토픽 스키마가 깨질 수 있다. 이건 운영하면서 종종 부딪힌다.
이 단점들을 우리 환경에선 다 감수할 만했다. 그래서 JDBC source connector로 정했다.
한 가지 짚어두면, Debezium이 나쁘다는 얘기가 아니다. 그냥 지금 우리 환경엔 JDBC source connector가 더 맞았을 뿐이다.
추후에 데이터 동기화만 처리하면 될 상황이 생기게 되면 그땐 Debezium 도 같이 도입하는걸 고려할 수 있다.
전체 파이프라인 구조는 이런 모습이다.

운영하면서 부딪힌 것들
구조는 이렇게 깔끔한데 막상 운영해보면 별별 일이 다 생긴다. 그중 몇 가지.
1. UTC와 KST의 전쟁
우리 운영 DB는 UTC 기준. 근데 분석은 KST 기준 일별 집계가 필요했다. SQL에서 DATE_FORMAT(createdAt + INTERVAL 9 HOUR, '%Y-%m-%d') 같은 걸 쓰면 결과는 맞는데 인덱스가 안 먹는다. 함수 적용된 컬럼에는 일반 인덱스가 작동하지 않으니까. 일별 집계 한 번 돌리는데 풀스캔이 일어난다.
결국 분석용 컬럼을 따로 두기로 했다. Stored Procedure로 적재 시점에 KST 변환된 datetime 컬럼이랑 yyyy-mm-dd 컬럼을 미리 계산해서 넣고 거기 인덱스를 걸었다. 비즈니스 로직 쪽은 이 컬럼을 안 보니까 영향 없고, 분석 쿼리는 인덱스 잘 타서 빨라졌다.
사소해 보여도 데이터가 쌓이면 차이가 꽤 크다.
2. 분석 클러스터를 따로 분리
처음엔 Kafka Connect를 하나의 클러스터로 묶어서 운영했다.
근데 한번은 분석 쪽 커넥터가 이상하게 동작하면서 클러스터 전체가 영향을 받은 적이 있다. 비즈니스 데이터 동기화까지 같이 흔들리는 걸 보고 이건 좀 아니다 싶었다.
그래서 인프라팀에 요청해서 분석용 Kafka Connect를 별도 EC2로 분리했다.
같은 Kafka 브로커를 쓰지만 워커 클러스터는 물리적으로 다른 곳에서 돈다. 이후로 분석 쪽 사고가 비즈니스에 전파되는 일은 없어졌다.
결국 분석 쪽이랑 비즈니스 쪽은 물리적으로 격리해두는 게 안전했다.
3. 커넥터 자동 재시작 — 그리고 자동화의 한계
JDBC source connector는 가끔 죽는다. 운영 DB가 잠깐 끊긴다거나, 일시적인 네트워크 이슈라거나. 자체적으로 복구되는 경우도 있지만 가끔은 FAILED 상태로 멈춘다.
새벽 3시에 깨서 커넥터 살리는 거 한두 번 하다가 자동화 작업을 진행하기로 결정했다.
10분 주기로 모든 커넥터 상태를 체크하고, FAILED면 백언된 config 조회 → 자동 재시작 → Slack 알림. 이걸 돌려놓으니까 야간·주말에 알람 받을 일이 줄었다.
물론 한계도 있다. 스키마 변경처럼 근본 원인이 있는 경우엔 아무리 재시작해도 또 죽는다. 이런 건 Slack 알림 받고 직접 DDL 쳐서 풀어야 한다. 자동화는 일시적인 장애에만 잘 통한다.
4. 작은 운영 도구의 효과
Kafka Connect는 REST API로 모든 걸 한다. 커넥터 등록, 시작, 중지, config 조회. 그래서 보통 Postman이나 IntelliJ HTTP 같은 도구로 호출한다.
근데 사람마다 쓰는 도구가 다르다 보니 문제가 생긴다. 누구는 Postman, 누구는 IntelliJ, 누구는 그것도 없고. 그러다 한번은 잘못된 Config 로 payload를 보내는 사고가 있었다.
그래서 작은 내부 UI 서비스를 만들었다. config 파일 폴더를 읽어서 커넥터 목록을 보여주고, 상태 조회·재시작·bulk mode 토글 같은 자주 쓰는 액션을 버튼 한 번에 처리할 수 있게. 화려한 건 아니다. 그냥 우리 팀 내부용. 근데 이거 만들고 나서 운영 실수가 확 줄었다.
작은 도구 하나가 팀이 일하는 방식을 이렇게 바꿀 수 있구나, 라는 걸 처음 느꼈다.
지금 다시 한다면
돌아보면 잘한 것도 있고, 다음엔 이렇게 해야겠다 싶은 것도 있다.
잘한 선택:
- 도구를 먼저 정해두고 요구사항을 거기 끼워맞추는 식이 아니라 요구사항을 기반으로 선택한것.
- Jdbc Connector 를 사용하여 "동기화 + 비정규화 변환" 을 한번에 처리했다.
- 분석 클러스터를 분리한 것. 사후 대응이긴 했지만 결과적으로 옳았다.
- 자동화 범위를 분명히 한 것.
- 일시적 장애만 자동 재시작, 근본 원인은 사람이 본다. 이 경계가 분명해서 야간에 울리는 알람은 진짜 중요한 것만 울린다.
다음에 보완하고 싶은 것:
- 스키마 변경 대응.
- 지금은 컬럼이 바뀌면 토픽 스키마가 깨질 위험이 있다. Debezium의 schema registry 같은 걸 어떻게 흉내 낼지 미리 고민해뒀어야 했다.
- 데이터 정합성 검증.
- 운영 DB랑 분석 DB의 카운트가 항상 맞는지 좀 더 적극적으로 체크하는 장치. 지금은 사후에 발견하는 경우가 많다.
- 지연(latency) 모니터링.
- 커넥터 상태는 잘 보고 있는데 동기화 지연 같은 메트릭은 적극적으로 안 보고 있다. Lag이 쌓이기 전에 감지할 수 있는 장치가 있으면 좋겠다.
결국 다 트레이드오프다. 1주일에 스프린트 1개 도는 환경에서 모든 걸 완벽하게 할 순 없다.
다만 "지금 뭘 모니터링하지 않고 있는지", "어떤 위험을 감수하고 있는지" 정도는 우리가 알고 있는 상태로 운영하는 게 중요한 것 같다.
솔직히 CDC 파이프라인 처음 만들 때보다 지금이 더 부담스럽다. 데이터에 의존하는 서비스가 늘어날수록 이게 멈췄을 때 영향받는 사람이 많아지니까. 그래서 점점 더 조심하게 된다.
다음 글에선 운영하면서 부딪힌 구체적인 케이스들(스키마 변경 대응, 정합성 검증 자동화)에 대한 이야기를 정리해볼까한다.
'데이터 엔지니어링' 카테고리의 다른 글
| Kafka Connect JDBC Source Connector, 스키마 변경 때마다 부서지는 이야기 (1) | 2026.05.13 |
|---|---|
| RDB에서 S3 + Athena로 전환할 때, 비용을 어떻게 계산했나 (0) | 2026.05.13 |
- Total
- Today
- Yesterday
- jdbc
- LLM비활성결정
- 수수료슬리피지
- 코인별손익분석
- LLM동적호출
- BOJ #JS
- Haiku4096토큰
- Telegram알림
- LLM비용오차
- Kafka
- 한달운영진단
- DataFramebool
- 코인별전략배정
- Page_DownPage_DownPage_Down
- P0P4우선순위
- 데모모드
- 일일주간월간스케줄러
- 5중검증
- CDC
- 코드자체감사
- 비용80%절감
- LLM파라미터머지
- AnthropicCaching
- 일경계처리
- 트레일링스탑버그
- 메이저화이트리스트
- kafka connect
- CryptoBot
- 데이터엔지니어링
- SlidingWindowTTL
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |