Post

Kafka 기반 이벤트 아키텍처

Kafka를 메시지 큐가 아닌 이벤트 로그로 설계하며 키·순서·중복·스키마·재처리를 고정하는 아키텍처 원칙

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개

  1. 즉시 일관성이 정말 필요한가
  2. 순서를 보장해야 하는 비즈니스 단위는 무엇인가
  3. 키를 무엇으로 잡을 것인가
  4. 중복 이벤트를 어떻게 무시/흡수할 것인가
  5. 이벤트 스키마의 소유자는 누구인가
  6. 스키마 변경 시 하위 호환 원칙은 무엇인가
  7. 재처리를 언제/누가/어떻게 할 것인가
  8. 실패 이벤트를 어디에 보관할 것인가(DLQ)
  9. 컨슈머 lag을 어떤 기준으로 위험 신호로 볼 것인가
  10. “Kafka 없이도 되는가?”에 답해봤는가

흔한 오해 / 실수 3가지

  1. Kafka를 “메시지 큐”로만 보는 오해
    • 문제: 요청 전달용으로만 사용, 이벤트를 즉시 소비·삭제된 것으로 인식
    • 결과: 재처리 불가, 과거 상태 복원 불가
    • 교정 기준: Kafka는 로그 저장소이며, 재생 가능한 이벤트 스트림이라는 전제로 설계
  2. 키 없이 토픽을 설계하는 실수
    • 문제: key = null 또는 무작위 키 사용
    • 결과: 파티션 간 순서 붕괴 → 상태 전이 오류
    • 교정 기준: “어떤 비즈니스 단위의 순서가 중요한가?”에 답하고 키를 고정
  3. 중복은 예외라고 가정하는 사고
    • 문제: 중복 수신을 버그로 간주하고 방어 로직 없음
    • 결과: 결제/상태 전이 중복 실행
    • 교정 기준: at-least-once는 정상 동작, 멱등 처리 필수

대표 실패 시나리오 3가지

  1. 이벤트 순서가 깨져 상태 전이가 실패하는 경우
    • 상황: PaymentApprovedOrderCreated보다 먼저 소비
    • 원인: 잘못된 키 설계 또는 다중 파티션
    • 결과: 상태 머신 오류, 예외 폭증
    • 대응: 키를 orderId로 고정 + 조건 미충족 이벤트는 대기/무시
  2. 재처리 시 동일 이벤트가 다시 적용되는 경우
    • 상황: 오프셋 리셋 후 전체 리플레이
    • 원인: 비멱등 로직
    • 결과: 중복 승인/중복 집계
    • 대응: 이벤트 ID/버전 기반 멱등 업서트
  3. 스키마 변경으로 컨슈머가 전부 죽는 경우
    • 상황: 필드 삭제 또는 의미 변경
    • 원인: 이벤트를 내부 DTO처럼 취급
    • 결과: 역직렬화 실패 → 서비스 중단
    • 대응: 이벤트는 계약, 하위 호환만 허용 + DLQ 분리

실무 체크리스트 (Kafka 이벤트 설계 점검)

  1. 이벤트가 “사실”만 표현하고 있는가
  2. 순서를 보장해야 하는 비즈니스 단위가 명확한가
  3. 토픽 키가 그 단위를 정확히 반영하는가
  4. 중복 수신 시 결과가 동일한가(멱등성)
  5. 컨슈머 재시작/리밸런싱을 가정했는가
  6. 이벤트에 버전 또는 고유 ID가 포함돼 있는가
  7. 스키마 변경 시 하위 호환 원칙이 정의돼 있는가
  8. 역직렬화 실패를 DLQ로 분리하는가
  9. 재처리를 운영 시나리오로 포함했는가
  10. 컨슈머 lag을 장애 신호로 모니터링하는가
  11. 이벤트 처리 로직이 상태 머신 검증을 포함하는가
  12. “이 이벤트를 6개월 뒤 다시 재생해도 안전한가?”에 답할 수 있는가

This post is licensed under CC BY 4.0 by the author.