Kafka 기반 이벤트 아키텍처
Kafka를 메시지 큐가 아닌 이벤트 로그로 설계하며 키·순서·중복·스키마·재처리를 고정하는 아키텍처 원칙
Posted
By okorion
Kafka 기반 이벤트 아키텍처
이 글의 결론: Kafka는 요청을 전달하는 도구가 아니라 상태 변화의 사실을 저장·전파·재처리하는 이벤트 로그다.
Kafka 설계의 핵심은 API 사용법이 아니라 정합성의 정의, 키/순서 보장, 중복 허용 전제, 스키마 계약, 재처리 전략이다. 이 다섯 가지를 고정하지 않으면 운영 단계에서 반드시 사고가 난다.
이벤트 기반에서 ‘정합성’의 의미 결론: 즉시 일관성을 포기하는 대신 최종 일관성을 설계로 확보한다.
- 즉시 일관성: 모든 쓰기가 동시에 반영됨(분산 환경에서 비용 과다/취약).
- 최종 일관성: 이벤트 전파 후 시간이 지나면 모두 같은 상태에 수렴.
이벤트 아키텍처의 정합성은 “지금 당장 같음”이 아니라,
- 언제까지 같아져야 하는가(SLO),
- 중간 상태에서 허용되는 불일치,
- 불일치가 외부에 노출되는 방식 을 명시하는 문제다.
토픽 설계 핵심 결론: 키는 성능 옵션이 아니라 순서와 의미를 고정하는 설계 결정이다.
왜 키를 잡는가
- Kafka의 순서 보장 단위는 파티션
- 같은 키 → 같은 파티션 → 순서 보장
주문ID 같은 키 선택의 의미
key = orderId- 한 주문의 모든 이벤트는 순서가 보장
- 서로 다른 주문 간 순서는 의미 없음(병렬 처리)
잘못된 키 선택의 결과
key = null→ 라운드로빈 → 순서 붕괴key = userId→ 한 사용자의 모든 주문이 직렬화 → 불필요한 병목
키 선택은 “무엇의 순서가 중요하냐”에 대한 답이어야 한다.
중복/재처리 결론: at-least-once를 받아들이지 못하면 Kafka를 쓰면 안 된다.
at-least-once의 결과
- 컨슈머 재시작/리밸런싱/타임아웃 → 같은 이벤트 재전달
- 중복은 버그가 아니라 정상 동작
idempotency가 필요한 이유
- 중복 수신에도 결과가 한 번 처리한 것과 동일해야 함
수단:
- 자연키(orderId + eventType + version)
- 처리 로그/인박스 테이블
- 상태 머신 전이 검증(이미 전이된 상태면 무시)
중복을 막으려 하지 말고, 중복을 견디게 설계한다.
Producer/Consumer 공통 모듈 분리 결론: 에러·재시도·관측성은 비즈니스 코드가 아니다.
왜 분리하는가
모든 프로듀서/컨슈머가
- 재시도 정책
- 역직렬화 실패 처리
- 메트릭/로그/트레이싱 을 매번 구현하면 일관성 붕괴
분리 대상
- 공통 Serializer/Deserializer
- 에러 분류(비가역/가역)
- 재시도 백오프
- DLQ 전송
- 공통 메트릭(lag, failure rate)
비즈니스 로직은 “이 이벤트를 처리할지 말지”만 판단해야 한다.
스키마 진화 결론: 이벤트는 내부 구현이 아니라 장기 계약(Contract)이다.
“이벤트는 계약”의 의미
- 발행자는 마음대로 필드를 바꿀 수 없다.
- 소비자는 과거 이벤트도 처리 가능해야 한다.
원칙
- 필드 추가는 허용
- 필드 삭제/의미 변경은 파괴적
- 버전 필드 포함
- 기본값으로 하위 호환 유지
예시 1 결론: 내부 DB 스키마 노출은 미래의 재처리를 망친다.
❌ 나쁜 예
1
2
3
4
5
6
7
8
{
"order_id": 10,
"status": "APPROVED",
"created_at": "2025-01-01T10:00:00",
"updated_at": "2025-01-01T10:05:00",
"deleted": false,
"jpa_version": 3
}
문제:
- 내부 컬럼 의미 변경 시 모든 컨슈머 파손
- 재처리 시 “왜 이 필드가 필요한지” 불명확
예시 2 결론: 이벤트 계약은 ‘사실’만 최소로 담는다.
✅ 개선 예
1
2
3
4
5
6
{
"eventType": "OrderApproved",
"orderId": "O-123",
"occurredAt": "2025-01-01T10:05:00",
"version": 1
}
특징:
- 상태 변화의 사실만 표현
- 읽기 모델/후속 처리에서 자유롭게 해석
운영 관점 결론: 재처리는 옵션이 아니라 복구 시나리오의 핵심이다.
장애 시 리플레이 전략
- 읽기 모델 오류 → 오프셋 리셋 후 전체 재생
- 특정 기간 오류 → 타임스탬프 기준 재생
- 버그 수정 후 → 동일 이벤트 재적용
전제:
- 이벤트 처리 로직은 멱등
- 스키마는 하위 호환
DLQ/재시도 토픽을 고민해야 하는 이유
- 모든 실패가 재시도로 해결되지는 않음
- 역직렬화 실패/계약 위반은 분리 보관 후 수동 판단
- 재시도 폭주는 시스템 전체 장애로 전이
문제 대응 표 결론: “사고 유형을 미리 분류하지 않으면 운영에서 판단이 늦어진다.”
| 문제 | 원인 | 탐지 | 대응 패턴 |
|---|---|---|---|
| 순서 꼬임 | 잘못된 키 | 상태 전이 오류 | 키 재설계 |
| 중복 처리 | at-least-once | 중복 로그 | 멱등 처리 |
| 유실 | 커밋/발행 분리 | 상태 불일치 | Outbox |
| 역직렬화 실패 | 스키마 변경 | 컨슈머 에러 | DLQ |
| 재처리 실패 | 비멱등 로직 | 재생 중 오류 | 상태머신 검증 |
Kafka 도입 전 질문 10개
- 즉시 일관성이 정말 필요한가
- 순서를 보장해야 하는 비즈니스 단위는 무엇인가
- 키를 무엇으로 잡을 것인가
- 중복 이벤트를 어떻게 무시/흡수할 것인가
- 이벤트 스키마의 소유자는 누구인가
- 스키마 변경 시 하위 호환 원칙은 무엇인가
- 재처리를 언제/누가/어떻게 할 것인가
- 실패 이벤트를 어디에 보관할 것인가(DLQ)
- 컨슈머 lag을 어떤 기준으로 위험 신호로 볼 것인가
- “Kafka 없이도 되는가?”에 답해봤는가
흔한 오해 / 실수 3가지
- Kafka를 “메시지 큐”로만 보는 오해
- 문제: 요청 전달용으로만 사용, 이벤트를 즉시 소비·삭제된 것으로 인식
- 결과: 재처리 불가, 과거 상태 복원 불가
- 교정 기준: Kafka는 로그 저장소이며, 재생 가능한 이벤트 스트림이라는 전제로 설계
- 키 없이 토픽을 설계하는 실수
- 문제:
key = null또는 무작위 키 사용 - 결과: 파티션 간 순서 붕괴 → 상태 전이 오류
- 교정 기준: “어떤 비즈니스 단위의 순서가 중요한가?”에 답하고 키를 고정
- 문제:
- 중복은 예외라고 가정하는 사고
- 문제: 중복 수신을 버그로 간주하고 방어 로직 없음
- 결과: 결제/상태 전이 중복 실행
- 교정 기준: at-least-once는 정상 동작, 멱등 처리 필수
대표 실패 시나리오 3가지
- 이벤트 순서가 깨져 상태 전이가 실패하는 경우
- 상황:
PaymentApproved가OrderCreated보다 먼저 소비 - 원인: 잘못된 키 설계 또는 다중 파티션
- 결과: 상태 머신 오류, 예외 폭증
- 대응: 키를 orderId로 고정 + 조건 미충족 이벤트는 대기/무시
- 상황:
- 재처리 시 동일 이벤트가 다시 적용되는 경우
- 상황: 오프셋 리셋 후 전체 리플레이
- 원인: 비멱등 로직
- 결과: 중복 승인/중복 집계
- 대응: 이벤트 ID/버전 기반 멱등 업서트
- 스키마 변경으로 컨슈머가 전부 죽는 경우
- 상황: 필드 삭제 또는 의미 변경
- 원인: 이벤트를 내부 DTO처럼 취급
- 결과: 역직렬화 실패 → 서비스 중단
- 대응: 이벤트는 계약, 하위 호환만 허용 + DLQ 분리
실무 체크리스트 (Kafka 이벤트 설계 점검)
- 이벤트가 “사실”만 표현하고 있는가
- 순서를 보장해야 하는 비즈니스 단위가 명확한가
- 토픽 키가 그 단위를 정확히 반영하는가
- 중복 수신 시 결과가 동일한가(멱등성)
- 컨슈머 재시작/리밸런싱을 가정했는가
- 이벤트에 버전 또는 고유 ID가 포함돼 있는가
- 스키마 변경 시 하위 호환 원칙이 정의돼 있는가
- 역직렬화 실패를 DLQ로 분리하는가
- 재처리를 운영 시나리오로 포함했는가
- 컨슈머 lag을 장애 신호로 모니터링하는가
- 이벤트 처리 로직이 상태 머신 검증을 포함하는가
- “이 이벤트를 6개월 뒤 다시 재생해도 안전한가?”에 답할 수 있는가
This post is licensed under CC BY 4.0 by the author.
