Apache Kafka로 이벤트 드리븐 아키텍처 구현하기
그래서 Kafka의 본질은 브로커 기능보다 계약 설계와 운영 규율에 더 가깝습니다.
이벤트는 시스템 계약입니다
가장 중요한 판단은 이벤트를 내부 구현 디테일로 볼 것인지, 다른 시스템이 의존할 수 있는 계약으로 볼 것인지입니다.
실전 시스템에서는 대개 이벤트를 계약으로 다뤄야 합니다.
- 이름은 비즈니스 사실을 반영해야 함
- payload는 downstream이 이해할 수 있는 의미를 담아야 함
- 소비자가 늘기 전에 버전 전략을 정해야 함
- 스키마 변경은 점진적으로 배포해야 함
UserUpdated, RowChanged 같은 모호한 이벤트는 소비자에게 의미 추론 책임을 떠넘기게 됩니다.
Topic과 Partition은 비즈니스 의미를 가져야 합니다
Kafka topic은 단순 전송 채널이 아니라 이벤트 종류와 보관 정책의 경계입니다. Partition은 단순 확장성 단위가 아니라 순서 보장의 단위입니다.
그래서 partition key를 정할 때는 최소한 다음을 답해야 합니다.
- 어떤 엔터티가 순서를 필요로 하는가
- 어느 정도 병렬성이 필요한가
- hot key로 인한 skew 위험은 없는가
좋은 partition 설계는 비즈니스가 필요한 순서는 지키고, 시스템이 활용할 수 있는 병렬성은 열어둡니다.
DB 변경과 이벤트 발행 경계는 명시적으로 풀어야 합니다
Kafka 시스템에서 가장 위험한 가정 중 하나는 “DB 쓰기와 Kafka 발행은 보통 같이 성공하겠지”입니다.
실무에서는 보통 Outbox 패턴이 가장 현실적인 해법입니다.
- 비즈니스 상태와 outbox 이벤트를 한 DB 트랜잭션으로 저장
- 별도 relay가 Kafka로 비동기 발행
- relay 지연과 실패를 모니터링
이 경계를 풀지 않으면, 서비스 내부 상태와 외부에 공개된 이벤트 사이의 침묵하는 불일치가 결국 발생합니다.
Consumer는 멱등해야 합니다
at-least-once 전달을 쓰는 이상, 중복 메시지는 정상 상황입니다.
그래서 소비자는 다음을 만족해야 합니다.
- 메시지 식별자를 기준으로 중복을 감지
- side effect를 한 번만 안전하게 적용
- 일시 오류와 비즈니스 거절을 구분
- replay 상황에서도 상태를 오염시키지 않음
중복을 견디지 못하는 consumer는 브로커 구성이 아무리 깔끔해도 운영 준비가 된 것이 아닙니다.
Replay는 비상 수단이 아니라 기능입니다
Kafka의 강점 중 하나는 retained event를 다시 읽어 상태를 재구축할 수 있다는 점입니다. 하지만 replay는 미리 준비된 시스템에서만 진짜 기능이 됩니다.
replay 친화적인 시스템은 보통 다음 특징을 가집니다.
- deterministic한 consumer 로직
- 멱등한 side effect
- 명확한 버전 전략
- backfill과 offset 관리 도구
- replay 중 lag와 오류 급증을 볼 수 있는 관측 체계
replay를 “정말 큰일 날 때만 하는 작업”으로 보면, 정작 필요할 때 실패하기 쉽습니다.
DLT는 쓰레기통이 아니라 운영 제어 장치입니다
Dead-letter topic은 반복 실패 메시지를 격리하는 데 유용하지만, 해결되지 않은 문제를 쌓아두는 장소가 되어서는 안 됩니다.
건강한 DLT 운영에는 보통 다음이 필요합니다.
- 실패 원인 분류
- malformed payload와 transient infrastructure issue 구분
- 누가 조사하고 어떻게 replay할지 절차 정의
- correlation ID와 원본 메타데이터 보존
이 규율이 없으면 DLT는 그냥 방치된 정합성 적체가 됩니다.
실제로 봐야 하는 지표
Kafka 운영 성공을 클러스터 상태만으로 판단하면 위험합니다. 브로커가 살아 있다고 해서 애플리케이션 정합성이 건강한 것은 아닙니다.
최소한 다음은 봐야 합니다.
- consumer lag
- rebalance 빈도
- producer error rate
- retry와 dead-letter 유입량
- consumer group별 처리 지연
- hot partition skew
이 지표가 실제 이벤트 흐름이 건강한지 보여줍니다.
흔한 아키텍처 실수
다음 패턴은 실제 운영에서 자주 문제를 만듭니다.
- 테이블 변경을 그대로 노출한 이벤트 발행
- topic 전체 또는 모든 partition에 글로벌 순서가 있다고 가정
- DB/event atomicity를 풀지 않은 채 Kafka만 붙임
- 선언되지 않은 필드 의미에 consumer가 의존
- replay와 DLT 처리를 수동 영웅 작업에 맡김
이것들은 브로커 실패가 아니라 설계 실패입니다.
체크리스트
설계가 성숙했다고 말하려면 최소한 다음이 확인되어야 합니다.
- 이벤트 이름이 도메인 사실을 반영하는가
- partition key가 순서 요구를 반영하는가
- outbox 또는 동등한 경계 해법이 있는가
- consumer가 멱등한가
- replay 절차가 검증되었는가
- lag, rebalance, DLT, skew를 관측하는가
마무리
좋은 Kafka 설계는 브로커 자체보다, 이벤트를 계약으로 다루고 순서를 의도적으로 선택하며 replay와 중복을 정상 운영 조건으로 받아들이는 규율에서 갈립니다.
그것이 비동기 시스템을 단순한 메시지 전송이 아니라 신뢰 가능한 이벤트 아키텍처로 만드는 기준입니다.
Continue Reading
다음으로 읽기 좋은 글
CQRS + Event Sourcing 실전 구현 가이드
CQRS와 Event Sourcing을 도메인 경계와 운영 복잡성의 관점에서 설명하고, Aggregate, Event Store, Projection, Snapshot, 정합성 모델의 선택 기준을 정리합니다.
⚙️ Backend백엔드 학습 경로: 입문부터 고급까지
API 기초, 안정성 패턴, 분산 아키텍처까지 백엔드 지식을 체계적으로 쌓을 수 있는 실전 로드맵입니다.
🖥️ Frontend마이크로 프론트엔드 — Module Federation 실전 적용
마이크로 프론트엔드를 기술 데모가 아니라 팀 경계와 배포 독립성의 관점에서 정리합니다. Module Federation 구조, shared 의존성, 런타임 로딩, 상태 공유, 운영 함정과 적용 기준까지 실무적으로 설명합니다.
💬 LanguageI/O 경계에서의 타입 좁히기 전략
언어의 타입 시스템은 경계 안에서 강하지만, 외부 입력 앞에서는 다시 확인이 필요합니다. I/O 경계에서 타입을 좁히는 전략을 정리합니다.
다음 탐색