Message Delivery Semantics
Kafka가 프로듀서에서 컨슈머까지 메시지를 전달할 때 보장하는 신뢰성 수준을 조절할 수 있으며, 이는 시스템의 요구사항에 따라 성능과 데이터 신뢰성 사이의 균형을 맞추는 중요한 설정이다.
At-most-once (최대 한 번 전송)
섹션 제목: “At-most-once (최대 한 번 전송)”메시지가 유실될 수는 있지만 절대 중복되지는 않음을 의미하며, 가장 낮은 신뢰성 수준을 제공하고 최고의 성능을 목표로 할 때 사용된다.
- Producer 설정:
acks=0지정- 응답 대기 생략:
send()요청 후 브로커 응답을 기다리지 않아 네트워크 왕복 시간(RTT) 절약 - 위험성: 브로커 장애나 네트워크 단절 시 메시지 유실 발생
- 응답 대기 생략:
- Consumer 설정: 메시지 처리 전 오프셋 커밋
- 자동 커밋 사용 시 처리 완료 전 커밋이 발생하여 유실 가능성 존재
At-least-once (최소 한 번 전송)
섹션 제목: “At-least-once (최소 한 번 전송)”메시지가 중복될 수는 있지만 절대 유실되지는 않음을 의미하며, 대부분의 애플리케이션에서 기본적으로 채택하는 신뢰성 수준이다.
- Producer 설정:
acks=all및retries > 0지정- 전송 보장: ISR의 모든 브로커가 메시지를 받았음을 확인 후 성공 간주
- 중복 발생 시나리오: 브로커가 메시지 저장 후 응답을 보내기 전 장애 발생 시 프로듀서가 재전송을 시도하여 중복 기록됨
- Consumer 설정: 메시지 처리 완료 후 수동 오프셋 커밋
- 처리 방식:
poll()배치의 로직이 모두 끝난 후commitSync()또는commitAsync()호출 - 중복 발생 시나리오: 로직 성공 후 커밋 전 컨슈머 장애 시 재시작 후 마지막 커밋 지점부터 다시 읽음
- 처리 방식:
멱등적 소비자(Idempotent Consumer) 구현 전략
섹션 제목: “멱등적 소비자(Idempotent Consumer) 구현 전략”At-least-once 방식에서 발생하는 중복 처리를 방지하기 위해 소비자의 멱등성 확보가 필수적이다.
- 데이터베이스 활용: 메시지의 고유 키(예: 이벤트 ID, 주문 ID)를 데이터베이스의 Primary Key나 Unique Key로 사용하여 중복 삽입 방지
- 버전 관리: 데이터에 버전 번호를 두고, 현재 저장된 버전보다 낮은 버전의 데이터가 들어오면 무시
- 상태 저장소 활용: Redis나 DB에 처리된 메시지 ID를 저장하고, 메시지를 처리하기 전에 ID의 존재 여부를 먼저 확인하여 중복 실행 방지
메시지 순서 보장
섹션 제목: “메시지 순서 보장”Kafka의 순서 보장은 파티션 단위로만 성립하며, 그 외 단위에서는 별도 설계와 설정이 없으면 순서가 깨질 수 있다.
순서 보장의 단위
섹션 제목: “순서 보장의 단위”같은 파티션에 적재된 메시지는 프로듀서가 보낸 순서대로 오프셋이 매겨지고, 컨슈머도 동일한 순서로 읽는다.
- 토픽 전체에 대한 순서 보장은 제공하지 않으며, 파티션 단위 보장만 존재
- 같은 도메인 식별자(userId, orderId 등)의 순서를 유지하려면 해당 식별자를 메시지 키로 지정하여 동일 파티션으로 라우팅
- 파티션은 컨슈머 그룹 내 단 하나의 컨슈머에게만 할당되므로, 컨슈머 측 단일 스레드 처리까지 유지되어야 순서가 보존
순서가 깨질 수 있는 상황
섹션 제목: “순서가 깨질 수 있는 상황”파티션 단위 순서 보장이 깨지거나 의도와 다르게 동작할 수 있는 대표 상황은 다음과 같다.
- Producer 재시도로 인한 역전
max.in.flight.requests.per.connection이 1보다 크고enable.idempotence=false인 상태에서retries가 활성화되면 발생- 메시지 A(Seq 1)가 네트워크 오류로 실패한 사이 메시지 B(Seq 2)가 먼저 기록되고, 이후 A가 재전송되면 브로커 로그에는 B, A 순서로 저장
- Key 없는 메시지의 분산 라우팅
- 키가 지정되지 않은 메시지는 Sticky Partitioner가 처리량을 위해 임의 파티션에 배치
- 같은 도메인 식별자로 묶여야 할 메시지가 서로 다른 파티션에 흩어져 컨슈머 간 처리 순서 보장 불가
- 파티션 수 변경
- 운영 중 파티션을 추가하면
hash(key) % partition_count결과가 바뀌어 같은 키의 메시지가 변경 전후로 다른 파티션에 적재 - 기존 파티션에 처리되지 않은 lag이 남아 있는 상태에서 변경되면 신/구 파티션 사이 처리 순서가 뒤섞일 수 있음
- 운영 중 파티션을 추가하면
- Consumer 멀티스레드 처리
- 단일 파티션의 메시지를 worker pool로 분산하여 병렬 처리하면 그 시점부터 순서 보장이 깨짐
- Kafka가 제공하는 보장은 파티션 → 단일 컨슈머 스레드까지이며, 이후 처리 모델은 애플리케이션 책임
- 리밸런싱 중 중복 처리
- 파티션 소유권이 이동할 때 직전 컨슈머가 커밋하지 못한 메시지를 새 컨슈머가 다시 처리할 수 있음
- 순서 자체가 뒤바뀌는 것은 아니지만 같은 메시지가 중복 적용되어 도메인 정합성이 깨질 수 있음
예방을 위한 설정
섹션 제목: “예방을 위한 설정”상황별 원인을 사전에 차단하기 위한 프로듀서·컨슈머·운영 측 설정과 패턴이다.
- 멱등적 프로듀서:
enable.idempotence=true(Kafka 3.0 이후 기본값)- 시퀀스 검증: 브로커는 파티션별로 프로듀서의 시퀀스 번호를 추적하여 누락된 요청(Gap)은 거절하고, 이미 처리된 번호는 중복으로 판단해 저장하지 않음
- 윈도우 보장: 브로커는 최근 5개의 시퀀스 번호를 관리하므로,
max.in.flight.requests.per.connection을 5 이하로 설정하면 처리량을 유지하면서 순서 보장 가능
- 키 라우팅: 도메인 식별자를 메시지 키로 지정하여 같은 단위의 메시지가 동일 파티션에 적재되도록 강제
- 파티션 수 사전 산정: 예상 처리량을 기준으로 충분한 파티션을 미리 확보하고 운영 중 변경 최소화
- 컨슈머 측 키 기반 분배: 단일 컨슈머가 받은 메시지를 멀티스레드로 분산해야 한다면, 같은 키는 항상 같은 worker 큐로 보내 파티션 단위 순서를 처리 단계까지 연장
cooperative-sticky할당 전략과group.instance.id(Static Membership): 리밸런싱 영향 범위를 최소화하여 일시 중단과 재처리 빈도 감소
사후 대처
섹션 제목: “사후 대처”이미 순서가 뒤섞이거나 중복으로 발행된 메시지를 사후에 정렬하는 것은 어렵기 때문에, 컨슈머 측에서 도메인 상태가 잘못 갱신되지 않도록 처리하거나 발행 단계의 정합성을 강화하는 방향으로 접근한다.
- 페이로드의 시퀀스 / 버전 필드 검증
- 메시지에 도메인 단위의 단조 증가 값(version, updatedAt 등)을 포함하고, 컨슈머는 현재 저장된 값보다 낮은 버전의 메시지를 무시
- 예:
UPDATE ... SET ... WHERE id = ? AND version < ?
- 멱등적 컨슈머
- 메시지 고유 식별자(event_id)를 데이터베이스의 Unique Key로 활용하여 재처리 시 중복 삽입 방지
- 처리 결과와 오프셋 커밋을 동일 트랜잭션으로 묶어 부분 성공 상태 제거
- 발행 단계 강화 (Outbox)
- DB 트랜잭션과 메시지 발행을 분리하면 프로듀서 단계에서 순서·유실이 발생할 수 있음
- Outbox 테이블에 이벤트를 트랜잭션 내에서 기록하고 별도 프로세스(CDC 등)가 Kafka로 발행하면 DB 변경 순서가 그대로 보존
- Dead Letter Queue (DLQ) 격리
- 특정 메시지에서 처리 실패가 반복되면 후속 메시지가 모두 막혀 lag이 누적되고 사고가 확산
- N회 재시도 후 해당 메시지를 별도 DLQ 토픽으로 이동시켜 본 스트림은 진행, DLQ는 별도 배치로 분석·재처리
Exactly-once (정확히 한 번 전송)
섹션 제목: “Exactly-once (정확히 한 번 전송)”메시지가 유실되거나 중복되지 않고 정확히 단 한 번만 처리됨을 보장하는 가장 강력한 신뢰성 수준이다.
- Producer:
enable.idempotence=true/transactional.id=<UNIQUE_ID>설정enable.idempotence=true로 설정하여 브로커로의 메시지 중복 전송 방지transactional.id를 고유하고 안정적인 값으로 설정하여 트랜잭션을 활성화- 이를 통해 여러 토픽 파티션에 걸친 ‘읽기-처리-쓰기’ 작업을 하나의 원자적 단위로 묶어줌
- Consumer:
isolation.level=read_committed설정- 컨슈머는 최종적으로 커밋(Commit)된 트랜잭션에 포함된 메시지만 읽도록 설정
- 만약 트랜잭션이 처리 도중 중단(Abort)되면, 해당 트랜잭션에 포함된 모든 메시지는 컨슈머에게 노출되지 않아 데이터 정합성 보장
전송 보장 수준별 성능 및 트레이드오프
섹션 제목: “전송 보장 수준별 성능 및 트레이드오프”신뢰성이 높아질수록 추가적인 네트워크 통신과 디스크 I/O가 발생하여 성능 하락이 수반된다.
| 보장 수준 | 처리량(Throughput) | 지연 시간(Latency) | 신뢰성 | 주요 용도 |
|---|---|---|---|---|
| At-most-once | 최고 | 최저 | 낮음 | 로그 수집, 단순 모니터링 지표 |
| At-least-once | 높음 | 보통 | 높음 | 일반적인 비즈니스 이벤트 처리 |
| Exactly-once | 보통 | 높음 | 최고 | 금융 결제, 핵심 데이터 동기화 |