변경 데이터 캡처(CDC)와 Kafka를 활용한 실시간 데이터 스트리밍 아키텍처

이준규·2025년 1월 18일
0

Back-End

목록 보기
8/10


Kafka

저희 팀은 실시간 데이터 동기화를 위해 카프카를 도입했는데요
실시간성을 보장하기 위한 토픽과 컨슈머를 설계해도 데이터 변경점에서 이벤트를 발행(produce) 해주지 않으면 실시간성을 보장할 수 없어집니다.

때문에, 서비스에서 데이터 변경점을 모두 찾아서 적절한 토픽에 메시지를 발행하는 코드를 심어줘야 했습니다.

Batch

개발자가 실수로 데이터 변경점에 메시지를 발행하는 코드를 넣지 않으면 동기화가 이루어지지 않습니다.
신규 기능을 추가하거나 기존 기능을 변경할 때, 카프카에 메시지를 발행해야하는 코드인지 항상 놓치지 않고 판단해야 하죠
코드를 추가하는걸 깜빡하는 걸 에러로 감지할 수도 없습니다

초기에는 이러한 실수를 잡기 위헤 Batch를 추가했습니다
일정 시간마다 동기화를 놓친 데이터를 동기화해주는 일종의 보완성 검증 배치였습니다

안정적인 시스템을 위해 과한 리소스가 들어가게 된 것이죠
데이터의 양이 늘어나면서 BatchJob의 부하는 점점 커지게 되었습니다.

저는 실시간 동기화를 위해 휴먼에러의 위험과 Batch에 대한 부하를 문제로 정의했습니다

변경 데이터 캡처 (CDC)

데이터 중심 애플리케이션 설계 라는 책에서 읽은 변경 데이터 캡처라는 기법에서 문제 해결에 대한 아이디어를 얻었습니다.

여러 아티클을 뒤져보니 EDA(Event Driven Architecture) 에 CDC를 도입한 사례를 찾아볼 수 있었습니다.

Debezium이라는 툴을 사용해서 CDC를 구현한 사례가 꽤나 많았습니다.
저는 AWS DMS에서 제공하는 CDC 기능을 사용하기로 결정했습니다.
AWS의 RDS에서 MSK로 데이터를 복제할 것이기 때문에 네트워크와 엔드포인트 설정이 용이한 관리형 서비스를 사용하는 것이 좋다고 생각했고, 실시간 복제속도 또한 충분히 빨랐습니다.

EventStreaming

CDC가 변경 이벤트를 카프카 토픽에 발행하면, 그 메시지를 서비스 애플리케이션에서 소비하려고 했지만, 데이터베이스에 너무 직접적으로 의존하게 된다는 위험성이 있었습니다.
테이블 자체에 변경(alter, drop)이 생기는 상황에 대한 의존성 분리가 필요했습니다.

Apache-Flink와 같은 스트리밍 프레임워크를 통해 보다 안정적인 이벤트 스트리밍이 가능하다는 것을 알게 되어 도입하려고 했습니다
하지만 Node.js 진영에서 사용하기 위한 레퍼런스가 부족한 점도 있고, 당장에는 consume>produce 라는 간단한 기능만 수행하면 충분했기에 스트리밍 프레임워크 도입은 추후로 미뤘습니다.

Lambda vs ECS(Serverless vs Server-based)

스트리밍 기능으로써 처음에 Lambda가 떠올랐습니다.
DB 커넥션이나 HttpContext가 필요없었고 간단한 기능을 수행하기도 하고
트리거로써 MSK의 토픽을 바로 연결할 수 있어서였습니다.

하지만 서버를 띄우는 것보다 나을지는 미지수였습니다.
NestJS의 @nestjs/microservies 를 사용하여 간단한 코드구현이 가능함과 동시에 기능이 고도화될 때 팀의 여러 모듈을 적용하며 유지보수하기 용이하기 떄문입니다.
비용도 오히려 더 저렴할 것 같고요

생각할 것 없이 직접 구현해서 비교해보기로 했습니다

Go vs Node.js

람다를 먼저 올려보려 하는데 문득 굳이 Node.js를 써야할까? 라는 생각이 들었습니다.

콜드스타트에 이점이 있다는 Golang이 떠올라서 Golang 함수와 Node.js 함수를 둘 다 구현해서 비교해보기로 했습니다

Golang을 써본적은 없었지만 간단한 기능이었기에 구현은 어렵지 않게 할 수 있었고
카프카 라이브러리는 sarama를 사용했고
성능 최적화를 위해 메시지를 produce 하는 부분에 고루틴을 사용했습니다.

Node.js 함수도 kafkajs를 사용해서 구현했고 성능 최적화를 위해 Promise.allSettled 사용했습니다.

고루틴과 Promise.allSettled를 사용한 이유는 람다가 트리거로 이벤트를 받을 때,
배치 단위로 받기 때문에 기본적으로 배열이 입력되기 때문입니다.

Batch 개수는 1~10000개까지 직접 설정할 수 있습니다.

결과

6천개 이상의 메시지를 발행해보았습니다
윗 줄이 Go 아랫줄이 Node.js입니다 (Lambda Insight)

메모리는 압도적으로 Go가 우위여서 비용은 조금 아낄 수 있겠으나,
생각과 다르게 속도는 Node.js가 더 우위에 있었습니다.
(BatchSize를 달리해봐도 항상 같은 결과였습니다.)

특징으로는 Golang은 BatchSize가 달라져도 일정한 평균치를 유지했지만, Node.js는 BatchSize에 따라 100ms이하의 속도를 보이기도, 200ms 이상의 속도를 보이기도 했습니다.
콜드스타트 지속시간은 Go가 더 짧았습니다.

언어의 차이도 있을 수 있겠으나 sarama, kafkajs의 라이브러리 성능 차이도 있겠죠.

아무튼 유지보수성까지 생각하면, 서비스에도 사용중인언어로 Node.js가 나았다고 생각했지만
서버와 비교해봐야겠습니다.

Lambda vs ECS

NestJS의 @nestjs/microservies 라이브러리를 사용해서
같은 로직의 서버를 띄워보고 람다와 성능을 비교했습니다.

CDC로 이어진 토픽에 동시에 컨슈머로 등록해서 consumer lag을 비교했습니다.

kafka-ui를 통해 확인한 장면입니다.
위부터 go-lambda / nodejs-lambda / nestjs-server 입니다

약 6천개의 메시지를 처리하는 동안 서버는 거의 lag을 가지지 않았습니다.
메시지를 스트리밍하는데에 지연이 거의 없다는 뜻이겠죠

바로 서버를 올리기로 결정했습니다.


결론

AWS DMS 를 통한 CDC를 통해 이벤트 기반 실시간 데이터 동기화 아키텍처를 설계하고 개발하면서 겪을 과정을 공유해봤습니다.
우리 서비스는 이제 개발자가 동기화를 신경쓰지 않아도 실시간으로 다중 채널(외부 채널, 캐시)의 동기화를 보장할 수 있게 되었습니다.
이젠 이 아키텍처를 기반으로 다양하게 안정적이고 효율적인 서비스를 구축해나갈 생각입니다
읽어주셔서 감사합니다

profile
백엔드

0개의 댓글

관련 채용 정보