Java 아키텍처·Spring 용어 사전
메시징컨슈머 그룹 · at-least-once · 멱등 컨슈머

파티션

Kafka 토픽을 나눈 단위이자 병렬성의 상한. 같은 키는 같은 파티션으로 가 순서가 보존된다.

Kafka 토픽을 나눈 순서가 보장되는 append-only 로그 단위. 병렬성과 순서를 동시에 결정한다.

병렬성의 상한이다

한 파티션은 컨슈머 그룹 안에서 '한 컨슈머' 만 읽는다

  • 그룹의 최대 병렬성 = 파티션 수
  • 컨슈머를 파티션보다 많이 띄우면 남는 건 그냥 논다

처리량을 늘리려면 컨슈머가 아니라 파티션을 먼저 늘려야 한다

@KafkaListener(topics = "orders", groupId = "order-consumer", concurrency = "3")
void onOrder(OrderPlaced e) { ... }   // 이 인스턴스가 컨슈머 스레드 3개를 띄운다

인스턴스를 여러 대 띄워도 groupId가 같으면 전체 스레드가 하나의 그룹으로 파티션을 나눠 갖는다.

순서와 병렬성을 함께 얻는 법 — 키

template.send("orders", event.orderId(), event);   // (토픽, 키, 값)

같은 키 → 같은 파티션 (해시)

  • 한 주문의 이벤트는 순서대로
  • 서로 다른 주문은 병렬로

전역 순서를 원하면 파티션 1개뿐 → 병렬성을 포기하게 된다

파티션은 늘리기만 하고 줄일 수 없다

늘리면 key → partition 매핑이 바뀐다

  • 그 시점 이후로 같은 키가 다른 파티션으로 갈 수 있다
  • 순서 보장이 경계에서 깨진다

그래서 처음에 여유 있게 잡는다. 다만 너무 많으면

  • 파일 핸들 · 메모리 · 리밸런스 시간 · 종단 지연이 는다

기본 보장은 at-least-once

컨슈머가 처리하고 오프셋을 커밋하기 직전에 죽으면

  • 재시작 후 그 메시지를 다시 처리한다 → 중복

그래서 실무의 답은 '멱등 컨슈머' 다

  • 메시지 고유 id 로 중복 검사
  • UPSERT
  • 조건부 갱신 (WHERE status = 'PENDING')

정확히 한 번(EOS) 도 있지만(멱등 프로듀서 + 트랜잭션) 비용과 복잡도가 커서, 대개 at-least-once + 멱등 컨슈머가 더 단순하고 견고하다.

실패 처리 — 독약 메시지를 치운다

@Bean
DefaultErrorHandler errorHandler(KafkaTemplate<String, Object> template) {
    var recoverer = new DeadLetterPublishingRecoverer(template);   // "<토픽>.DLT" 로
    return new DefaultErrorHandler(recoverer, new FixedBackOff(1000L, 3L));
}

역직렬화 실패는 재시도해도 영원히 실패한다 — 재시도 없이 즉시 DLT로 분류해야 그 파티션이 막히지 않는다.

면접 함정

  • "Kafka는 순서를 보장한다"파티션 단위로만 보장한다.
  • "컨슈머를 늘리면 계속 빨라진다" → 파티션 수가 상한이다.

리밸런싱이 잦으면 무슨 일이 생기나

컨슈머가 들고 날 때마다 파티션을 다시 나눈다

  • 그동안 소비가 멈춘다
  • 커밋 전 처리분이 다른 컨슈머에게 재배정된다 → 중복

가장 흔한 원인: max.poll.interval.ms 초과 한 번에 가져온 레코드를 처리하는 데 이 시간(기본 5분)을 넘기면 죽은 것으로 보고 그룹에서 쫓아낸다 → 리밸런싱 → 또 느려짐 → 악순환

max.poll.records=100              # 처리가 느리면 이것부터 줄인다
max.poll.interval.ms=300000
group.instance.id=order-consumer-1  # 정적 멤버십 — 재시작해도 리밸런싱을 피한다
session.timeout.ms=45000

밀리고 있는지 보는 지표

kafka.consumer.fetch.manager.records.lag 파티션별 랙 (Micrometer) kafka.consumer.fetch.manager.records.lag.max

알림은 순간값이 아니라 '추세' 로 건다

  • 배포마다 튀었다 회복되므로 순간값이면 오탐이 쏟아진다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 메시징 — Kafka·RabbitMQ·이벤트