1년 전 작년 이맘때 새 시스템에 대한 개발을 시작했다. 새 시스템과 기존 시스템 간 데이터 연동이 필요했는데 몇 가지 이유(선택한 이유는 https://youtu.be/guVFK09yjXw 영상 참고)로 CDC(Change Data Capture)를 연동 기술로 선택했다.
MariaDB에 대한 CDC 모듈을 조사했는데 검색 결과로 많이 나온 게 Debezium이었다. 흥미롭긴 했는데 원하는 것을 하기에는 알맞지 않았다. 필요한 것은 다음과 같았다.
- 여러 테이블이 변경되어도 한 개 이벤트 발생
- 예를 들어 한 트랜잭션에서 A 주문의 주문 개요, 주문 상세, 계약 상세 등의 테이블이 변경되었을 때 각 테이블의 변경 내역을 이벤트로 만들지 않고 A 주문의 요약 정보를 한 개 이벤트로 생성해서 기존 시스템에 전달 필요
- 한 모델의 변경 시 영향받는 다른 모델 데이터도 전파
- 예를 들어 B회원의 결제 수단 정보가 변경되면 B회원과 관련된 모든 주문의 요약 정보를 이벤트로 생성해서 기존 시스템에 전달(주문 요약 정보에 결제 수단 정보가 포함되어 있어 전파 필요)
- 변경 데이터를 칼럼명으로 접근하고 싶음
- 변경 데이터를 칼럼 인덱스가 아닌 칼럼명으로 접근하고 싶음
이런 걸 하기에 Debezium은 적합하지 않아 요구에 맞는 CDC 모듈을 직접 만들기로 했다.
CDC 모듈 개발
다행히 MariaDB의 바이너리 로그를 읽을 수 있는 mysql-binlog-connector-java가 있어 이걸 활용해서 모듈을 만들었다(MySQL과 MariaDB가 프로토콜이 같아서 다행이었다). 다음 그림은 구현하는 과정에서 도출된 설계 결과물이다.
MariadbCdc에는 CDC 시작/중지 처리와 변경 데이터를 MariadbCdcListener에 전달하는 기능을 넣었다. 변경 데이터의 칼럼명 조회를 위해 ColumnNameGetter 인터페이스를 추가했고, (위 그림엔 표시하지 않았지만) 테이블 스키마 변경 시 칼럼명을 다시 조회할 수 있도록 스키마 변경 감지 기능을 넣었다.
MariadbCdcListener는 onDataChanged() 메서드로 변경 데이터를 RowChangedData에 담아서 전달받도록 했다. RowChangedData 클래스는 변경 데이터와 관련된 변경 타입, 변경된 테이블 이름, 변경 데이터, (UPDATE면) 변경 전 데이터를 갖도록 했다. DataRow는 데이터를 표현하며 칼럼명을 이용한 조회 기능과 칼럼 인덱스를 이용한 조회 기능을 제공하도록 구현했다.
한 트랜잭션에서 변경된 데이터를 모아서 한 번에 처리할 수 있도록 하기 위해 onXid() 메서드도 추가했다. 트랜잭션 커밋 로그를 읽을 때 이 메서드를 호출하도록 했다.
CDC 모듈 초기 버전을 만들고 https://github.com/madvirus/mariadb-cdc 리포지토리에 올리고 메이븐 의존 설정에 쉽게 추가할 수 있도록 jitpack.io를 사용했다.
개밥 먹기 1년
개발 환경에 적용한 뒤 1년이 지났다. 그 사이에 새 시스템을 오픈해서 운영 환경에도 적용했다. 이 과정에서 몇 가지 놓친 점을 발견했고 점진적으로 보완해 나갔다. 보완한 것 중에서는 다음이 기억에 남는다.
- 동시에 두 프로세스를 실행하면 서로 연결이 끊기는 증상: 같은 슬레이브 ID를 사용하면서 발생한 것으로 슬레이브ID를 지정할 수 있는 기능 추가하고 각 CDC 모듈이 겹치지 않는 슬레이브ID를 사용하도록 처리
- 한 테이블에서 많은 행이 변경될 때 감지가 누락되는 증상: 연속된 변경 ROWS 이벤트 발생 시 두 번째 이벤트부터 테이블 이름 처리가 잘못되어 발생한 것으로 테이블 이름 매핑 처리 버그 수정
- 칼럼 이름을 제대로 가져오지 못하는 버그(다른 테이블에 참조키를 제공하는 테이블 변경 시 발생): TABLE_MAP 이벤트가 연속될 때 발생한 버그 수정
이 중 두 번째는 운영 환경에서 알게 되었다. 관련 데이터가 일정 개수 이상으로 증가하면서 발생하기 시작했다. 처음에는 원인을 몰라 주기적으로 손으로 보정하는 짓을 좀 했다.
세 번째는 최근에 다른 DB에 CDC를 적용하면서 발견했다. 문제를 해결하는 과정에서 리플리케이션 프로토콜에 대해 조금 더 알 수 있었다.
두 번째 개밥 먹기 중
보완을 하면서 동시에 실험 목적의 바이너리 로그 리더 구현도 병행했다. mysql-binlog-connector-java가 충분히 좋았지만 리플리케이션 프로토콜에 대한 호기심이 발동해 바이너리 로그를 직접 구현해 보고 싶은 마음이 생겼기 때문이다. 주로 주중 저녁이나 주말에 구현했는데 토요일과 일요일에 이틀 동안 꼬박 이것만 했던 날도 있었다.
MariaDB/MySQL 사이트에서 제공하는 프로토콜 문서를 주로 참조했고 프로토콜 문서만으로 감이 안 올 때는 mysql-binlog-connector-java 코드를 뒤져가면서 점진적으로 구현했고 최근에 필요한 최소 기능 개발을 완료했다. 모듈을 설계할 때 mysql-binlog-connector-java의 타입 노출을 최소화하고 MariadbCdc, MariadbCdcListener, DataRow 등으로 한 번 감싸서 구현했기에 실험용 버전을 사용하도록 변경하는 것도 간단한 설정으로 진행할 수 있었다.
마침 CDC를 다른 곳에 적용해야 할 일이 생겨 새로 구현한 바이너리 로그 리더를 사용해보고 있다. 이 과정에서 또 다른 보완점을 찾을 수 있었는데 그건 바로 enum 타입에 대한 처리였다. enum 타입 처리를 위한 코드를 보완하고 있는데 MySQL과 MariaDB에서는 enum 타입을 최대한 쓰면 안 되겠다는 생각을 갖게 되었다. 내가 결정에 영향을 줄 수 있는 한 최대한 못 쓰게 할 거다!
다음 목표는 직접 구현한 바이너리 로그 리더를 별도 모듈로 분리하고 SSL이나 GTID 등에 대한 지원을 추가하는 것이다. 생각나면 하고 틈나면 하고 그런지라 언제 될지 모르지만 언젠가는 다음 목표를 달성하고 싶다.