이 도메인의 학습 목표

이 도메인이 묻는 것

Application Development는 4장 Producer 심화, 5장 Consumer 심화, 6장 전달 보장과 트랜잭션의 범위와 거의 겹칩니다. 출제 형태는 크게 세 가지입니다.

  1. 설정값과 소속 — 어떤 설정의 기본값은 무엇이고, 그 설정은 프로듀서·컨슈머·브로커·토픽 중 어디에 두는가. matching 유형에서 특히 자주 나옵니다.
  2. 시나리오 판단 — "컨슈머가 계속 리밸런스한다", "중복이 생겼다", "순서가 뒤바뀌었다"에서 원인 설정과 1차 처방을 고르는 문제.
  3. 코드 판독 — 10~20줄의 Java 코드나 properties를 보여 주고 동작 결과·문제점·개선점을 고르는 문제. 아래 코드 읽기 대비 절에서 훈련합니다.

핵심 개념 (1) 프로듀서

send()는 비동기, 콜백은 Sender 스레드에서

producer.send()는 레코드를 직렬화하고 파티션을 결정해 RecordAccumulator라는 메모리 버퍼에 넣고 즉시 반환합니다. 실제 네트워크 전송은 별도의 Sender(I/O) 스레드가 배치 단위로 수행합니다. 따라서 콜백 onCompletion(metadata, exception)애플리케이션 스레드가 아니라 Sender 스레드에서, 브로커 응답을 받은 시점(또는 최종 실패가 확정된 시점)에 호출됩니다.

멱등성과 순서 보장

Kafka 4.3의 enable.idempotence 기본값은 true입니다(3.0부터). 멱등 프로듀서는 PID(Producer ID) + 파티션별 시퀀스 번호를 붙여 보내고, 브로커가 시퀀스 번호로 중복을 판별해 폐기합니다. 재시도 때문에 같은 배치가 두 번 도착해도 로그에는 한 번만 기록됩니다.

멱등성이 켜져 있으면 max.in.flight.requests.per.connection5 이하여야 합니다. 기본값이 정확히 5인 이유가 여기 있습니다. 브로커가 파티션별로 최근 5개 배치의 시퀀스 번호를 기억하므로, 그 범위 안에서는 재시도로 순서가 어긋나도 브로커가 재정렬·중복 제거를 해 줍니다. 6 이상으로 설정하면 프로듀서 생성 시 ConfigException이 발생합니다.

파티셔닝 — 키가 있을 때와 없을 때

키가 있으면 murmur2(key) % numPartitions로 결정됩니다. 결정적이므로 같은 키는 항상 같은 파티션으로 갑니다. 이것이 파티션 단위 순서 보장의 근거입니다.

키가 null이면 기본 파티셔너가 sticky batching으로 분배합니다. 한 파티션에 배치를 채우고, 배치가 마감되면 다음 파티션으로 전환하는 방식입니다. 레코드 하나하나를 돌려 담는 순수 라운드로빈이 아닙니다. Kafka 2.4 이전의 DefaultPartitioner는 레코드 단위 라운드로빈이었습니다.

재시도 가능한 에러와 불가능한 에러

기준은 예외가 RetriableException을 상속하는지입니다. 재시도 가능한 예외는 프로듀서가 스스로 다시 보냅니다. 재시도 불가능한 예외는 즉시 콜백으로 올라옵니다.

재시도 가능/불가능 예외 — 시험에서 matching 유형으로 자주 나옵니다
구분 대표 예외 대응
재시도 가능
(retriable)
LeaderNotAvailableException, NotLeaderOrFollowerException, NotEnoughReplicasException, NetworkException, TimeoutException, CoordinatorNotAvailableException 프로듀서가 자동 재시도. 총 예산은 delivery.timeout.ms가 결정
재시도 불가
(non-retriable)
RecordTooLargeException, SerializationException, AuthorizationException, InvalidTopicException 재시도해도 같은 결과. 코드·설정·권한을 고쳐야 함

핵심 개념 (2) 컨슈머

타임아웃 4종 — 하트비트 축과 poll 축은 별개입니다

이 도메인에서 가장 많이 틀리는 지점입니다. 하트비트는 백그라운드 스레드가, poll은 애플리케이션 스레드가 담당합니다. 두 축은 서로 독립적으로 타임아웃됩니다.

컨슈머 타임아웃 4종 (Apache Kafka 4.3 기본값)
설정 기본값 무엇을 재는가 초과하면
heartbeat.interval.ms 3000 백그라운드 스레드가 하트비트를 보내는 주기 (주기이므로 초과 개념 없음). session.timeout.ms의 1/3 이하 권장
session.timeout.ms 45000 하트비트가 끊긴 뒤 코디네이터가 기다리는 시간 컨슈머 사망으로 판정 → 리밸런스
max.poll.interval.ms 300000 poll() 호출 사이의 최대 허용 시간 그룹에서 제거 → 리밸런스. 프로세스는 살아 있음
max.poll.records 500 한 번의 poll()이 반환하는 최대 레코드 수 (상한이므로 초과 없음). 처리 시간을 좌우하는 실질 손잡이

subscribe()assign()

subscribe() vs assign()
관점 subscribe(topics) assign(partitions)
그룹 관리참여 (코디네이터가 할당)참여하지 않음
리밸런스발생발생하지 않음
group.id필수없어도 동작 (커밋하려면 필요)
파티션 수 증가자동으로 새 파티션을 할당받음감지하지 않음 — 코드를 고쳐야 함
용도일반적인 확장형 소비특정 파티션만 읽기, 정밀 제어, 재처리 도구

리밸런스가 발생하는 조건 (전부)

반대로 assign()을 쓰는 컨슈머에서는 위의 어떤 이유로도 리밸런스가 일어나지 않습니다. group.instance.id를 지정한 static membership은 재시작 시 같은 멤버로 복귀해 재시작 때문에 생기는 리밸런스를 피합니다 (리밸런스 자체를 없애는 것이 아닙니다).

오프셋 커밋과 auto.offset.reset

enable.auto.commit의 기본값은 true이고 auto.commit.interval.ms5000입니다. 자동 커밋은 poll()이 호출될 때 "이전에 반환한 레코드들"을 커밋하는 방식이라, 처리 도중 크래시하면 이미 커밋된 레코드가 유실될 수 있습니다. 정확한 제어가 필요하면 enable.auto.commit=false + 수동 커밋입니다.

auto.offset.reset의 기본값은 latest이며, 커밋된 오프셋이 없을 때 또는 커밋된 오프셋이 로그 범위를 벗어났을 때만 적용됩니다. 정상적으로 커밋된 오프셋이 있으면 이 설정은 아무 일도 하지 않습니다.

핵심 개념 (3) 전달 보장과 트랜잭션

세 가지 전달 보장과 설정 조합
보장 프로듀서 컨슈머 결과
at-most-once acks=0 또는 재시도 없음 처리 커밋 유실 가능, 중복 없음
at-least-once
(기본 동작)
acks=all + 재시도 처리 커밋 유실 없음, 중복 가능
exactly-once
(Kafka 내부)
transactional.id + enable.idempotence=true isolation.level=read_committed + sendOffsetsToTransaction Kafka 토픽 경계 안에서만 정확히 한 번

트랜잭션 API 호출 순서

consume-transform-produce 패턴의 필수 호출 순서 (list order 유형 단골)
// 1. 프로듀서 생성 시 transactional.id 필수. 애플리케이션 시작 시 한 번만.
producer.initTransactions();

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(200));
    if (records.isEmpty()) continue;

    // 2. 트랜잭션 시작
    producer.beginTransaction();
    try {
        for (ConsumerRecord<String, String> r : records) {
            // 3. 결과 레코드 전송
            producer.send(new ProducerRecord<>("out-topic", r.key(), transform(r.value())));
        }
        // 4. 읽은 오프셋을 "같은 트랜잭션 안에서" 커밋한다.
        //    consumer.commitSync() 를 쓰면 원자성이 깨진다.
        producer.sendOffsetsToTransaction(offsetsOf(records), consumer.groupMetadata());

        // 5. 커밋
        producer.commitTransaction();
    } catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
        // 복구 불가 — 프로듀서를 닫고 종료해야 한다
        producer.close();
        throw e;
    } catch (KafkaException e) {
        // 6. 그 밖의 실패는 중단하고 다시 시도한다
        producer.abortTransaction();
    }
}

isolation.level은 컨슈머 설정입니다

기본값은 read_uncommitted입니다. 즉 프로듀서가 트랜잭션을 써도, 컨슈머가 아무 설정을 하지 않으면 중단(abort)된 트랜잭션의 메시지까지 읽습니다. EOS는 양쪽을 다 설정해야 성립합니다.

read_committed로 바꾸면 컨슈머는 LSO(Last Stable Offset)까지만 읽습니다. LSO는 "아직 끝나지 않은 트랜잭션의 시작 지점"이므로, 진행 중인 트랜잭션이 하나 있으면 그 뒤에 커밋 완료된 메시지도 보이지 않습니다. 길게 열린 트랜잭션은 컨슈머 지연으로 직결됩니다. 트랜잭션 커밋/중단을 표시하는 컨트롤 레코드는 애플리케이션에 노출되지 않지만 오프셋 번호는 소비하므로, 오프셋에 빈 칸이 생길 수 있습니다.

반드시 외워야 할 설정값

Apache Kafka 4.3 기준입니다. 굵게 표시한 값은 3.x 자료와 다르므로 예전 자료로 공부했다면 여기서 반드시 갱신하세요.

프로듀서 필수 암기 설정 (Apache Kafka 4.3)
설정 기본값 시험 포인트
acks all 3.0부터 1all. all일 때만 min.insync.replicas가 의미를 가짐
enable.idempotence true 3.0부터 기본 활성. 켜지면 in-flight ≤ 5, acks=all, retries>0이 강제됨
linger.ms 5 4.0에서 05로 변경. 시중 자료 대부분이 0으로 적혀 있음
batch.size 16384 바이트 단위. linger.ms둘 중 먼저 충족되는 쪽에 전송
buffer.memory 33554432 32MB. 가득 차면 send()max.block.ms까지 블로킹
max.block.ms 60000 버퍼 대기 + 메타데이터 대기의 상한. 초과 시 TimeoutException
max.in.flight.requests.per.connection 5 멱등성 켜면 5 초과 금지. 6 이상이면 ConfigException
request.timeout.ms 30000 한 번의 요청에 대한 응답 대기 시간
delivery.timeout.ms 120000 send()부터 성공/실패 확정까지의 전체 예산
retries 2147483647 실질 상한은 횟수가 아니라 delivery.timeout.ms
max.request.size 1048576 정확히 1MB. 브로커의 message.max.bytes(1048588)와 값이 다름
compression.type none 프로듀서 기본은 압축 없음. 토픽 기본값은 producer(프로듀서가 준 대로 저장)
transaction.timeout.ms 60000 트랜잭션이 이 시간 안에 끝나지 않으면 코디네이터가 중단시킴
컨슈머 필수 암기 설정 (Apache Kafka 4.3)
설정 기본값 시험 포인트
group.protocol classic KIP-848은 4.0 GA지만 컨슈머 기본값이 아님. consumer로 명시해야 새 프로토콜
session.timeout.ms 45000 하트비트 축. 예전 10000에서 변경됨
heartbeat.interval.ms 3000 session.timeout.ms의 1/3 이하 권장
max.poll.interval.ms 300000 poll 축. 하트비트와 다른 축
max.poll.records 500 poll 간격 초과의 1차 처방 손잡이
enable.auto.commit true 트랜잭션·정밀 제어 시 false
auto.commit.interval.ms 5000 자동 커밋 주기
auto.offset.reset latest 커밋 오프셋이 없거나 범위를 벗어났을 때만 발동
isolation.level read_uncommitted 컨슈머 설정. EOS를 원하면 read_committed로 바꿔야 함
partition.assignment.strategy [RangeAssignor,
CooperativeStickyAssignor]
classic 프로토콜에서만 유효. 목록의 첫 항목이 우선 협상됨
fetch.min.bytes 1 fetch.max.wait.ms(500)와 함께 지연/처리량을 조절
fetch.max.bytes 52428800 50MB. 요청 전체 상한
max.partition.fetch.bytes 1048576 1MB. 파티션당 상한

자주 나오는 함정

이 도메인에서 특히 자주 뒤바뀌어 출제되는 쌍입니다. 40쌍 전체는 함정 사전에 있습니다.

함정 1 — session.timeout.ms vs max.poll.interval.ms

두 타임아웃은 서로 다른 것을 감시합니다
관점session.timeout.msmax.poll.interval.ms
감시 대상하트비트 (백그라운드 스레드)poll() 호출 간격 (앱 스레드)
기본값45000300000
터지는 상황프로세스 사망 · GC 장기 정지 · 네트워크 단절레코드 처리가 오래 걸림
1차 처방인프라 점검, 값 상향은 신중히max.poll.records 축소
출제 형태"프로세스가 죽었을 때 감지하는 설정은?""프로세스는 살아 있는데 그룹에서 빠지는 이유는?"

함정 2 — acks vs min.insync.replicas

하나는 프로듀서, 하나는 브로커/토픽입니다
관점acksmin.insync.replicas
어디 설정프로듀서브로커 · 토픽
기본값all1
의미몇 개의 응답을 기다릴지쓰기를 성립시키는 최소 ISR 수
상호작용acks=all일 때만 min.insync.replicas가 검사됩니다. acks=1이면 무시됩니다
미달 시NotEnoughReplicasException (재시도 가능)
출제 형태"RF=3, min.insync.replicas=2일 때 브로커 몇 대까지 죽어도 쓰기가 되는가" → 1대

함정 3 — 멱등 프로듀서 vs 트랜잭션

둘 다 "중복"을 다루지만 범위가 다릅니다
관점멱등 프로듀서트랜잭션
켜는 방법enable.idempotence=true (기본값)transactional.id 지정 + API 호출
보장 범위한 세션 × 한 파티션의 재시도 중복 제거여러 파티션·토픽에 걸친 원자적 쓰기 + 오프셋 커밋
컨슈머 쪽 설정불필요isolation.level=read_committed 필요
프로듀서 재시작PID가 새로 발급되어 보장이 끊김같은 transactional.id로 이전 세션을 fence
출제 형태"EOS를 위해 필요한 것을 모두 고르시오" — 멱등성만 고르면 오답

함정 4 — request.timeout.ms vs delivery.timeout.ms

하나는 요청 단위, 하나는 전체 예산입니다
관점request.timeout.msdelivery.timeout.ms
기본값30000120000
범위브로커에 보낸 한 번의 요청send() 반환부터 콜백까지 전체
포함 관계linger.ms + request.timeout.msdelivery.timeout.ms 가 성립해야 합니다
초과 시재시도 대상최종 실패TimeoutException으로 콜백 호출
출제 형태"전송이 최대 얼마나 걸릴 수 있는가" → delivery.timeout.ms

함정 5 — read_uncommitted vs read_committed

isolation.level은 컨슈머 설정입니다
관점read_uncommittedread_committed
기본값이것이 기본값명시해야 함
읽는 범위high watermark까지 (중단된 트랜잭션 포함)LSO까지 (커밋된 것만)
지연낮음열린 트랜잭션이 있으면 그 뒤 전체가 대기
혼동 시 결과프로듀서만 트랜잭션을 쓰고 컨슈머를 그대로 두면 중단된 메시지까지 처리됩니다
출제 형태"트랜잭션을 썼는데 abort된 데이터가 하위 시스템에 들어갔다. 원인은?"

함정 6 — 할당 전략 4종

파티션 할당 전략 비교 (classic 프로토콜)
전략 할당 방식 기존 할당 유지 리밸런스 방식
RangeAssignor토픽별로 파티션을 연속 구간으로 나눔아니오eager
RoundRobinAssignor모든 토픽-파티션을 순서대로 돌려 담음아니오eager
StickyAssignor균등 + 기존 할당 최대 유지eager
CooperativeStickyAssignor균등 + 기존 할당 유지 + 증분 재할당cooperative

eager는 리밸런스 시작 시 모든 컨슈머가 파티션 전체를 반납합니다(stop-the-world). cooperative는 옮겨야 할 파티션만 두 라운드에 걸쳐 회수·재할당하므로 나머지 파티션은 계속 소비됩니다. 기본값 목록에 RangeAssignor가 먼저 오므로, cooperative를 쓰려면 명시적으로 CooperativeStickyAssignor만 지정해야 합니다. eager에서 cooperative로 옮길 때는 두 단계 롤링 배포가 필요합니다 (먼저 두 전략을 함께 나열해 배포하고, 다음 배포에서 eager를 제거).

코드 읽기 문제 대비

시험에 나오는 형태의 스니펫과 판독 포인트입니다. 정답을 보기 전에 스스로 문제점을 찾아보세요.

스니펫 1 — 무엇이 잘못되었는가

producer.properties
acks=all
enable.idempotence=true
max.in.flight.requests.per.connection=10
compression.type=snappy
linger.ms=20

스니펫 2 — 이 컨슈머의 문제는

SlowConsumer.java
Properties p = new Properties();
p.put("bootstrap.servers", "kafka-1:9092,kafka-2:9092");
p.put("group.id", "billing-worker");
p.put("key.deserializer", StringDeserializer.class.getName());
p.put("value.deserializer", StringDeserializer.class.getName());
// max.poll.records 와 max.poll.interval.ms 는 기본값을 사용

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(p);
consumer.subscribe(List.of("invoices"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
    for (ConsumerRecord<String, String> r : records) {
        callExternalPaymentApi(r.value());   // 레코드당 평균 1.2초
    }
}

스니펫 3 — 이 커밋 코드의 결과

CommitOrder.java
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(200));
    consumer.commitSync();          // (A) 처리 전에 커밋
    for (ConsumerRecord<String, String> r : records) {
        process(r);                  // (B) 여기서 예외가 나면?
    }
}

스니펫 4 — 순서가 보장되는가

OrderEvents.java
// orders 토픽은 파티션 6개
producer.send(new ProducerRecord<>("orders", null, "order-42 CREATED"));
producer.send(new ProducerRecord<>("orders", null, "order-42 PAID"));
producer.send(new ProducerRecord<>("orders", null, "order-42 SHIPPED"));

도메인 미니 퀴즈

단일 선택 · 복수 선택 · 연결형 · 순서 배열형이 섞여 있습니다. 실제 시험에도 matching과 list order가 출제되므로 함께 연습하세요.

공식 문서 출처