Data & Platform/Event Streaming
Kafka - 03. producer
producer — send가 반환됐다고 도달한 게 아니다
주문 API가 producer.send(orderEvent)를 호출했다. 메서드는 즉시 반환됐다. 그런데 이 순간 메시지는 broker에 도달한 게 아니다 — accumulator라는 배치 버퍼에 쌓여 있을 뿐이고, 배경 스레드가 나중에 보낸다. "send가 반환됐으니 안전하겠지"라고 가정하면, 장애 순간에 accumulator에 남아 있던 메시지들이 전부 날아간다. 이게 producer를 다룰 때 가장 먼저 부딪히는 함정이다.
producer는 애플리케이션에 내장되는 클라이언트다. 별도 서버가 아니라 주문 API·배치 잡·로그 수집기 같은 앱 프로세스 안에서 동작한다. 비동기 send, batch, acks, idempotent producer가 어떻게 엮여서 "메시지를 잃지 않으면서도 빠르게"를 만드는지를 추적한다. acks=all 한 줄만 보고 "안전하다"고 믿으면 ISR이 1개 남았을 때 구멍이 드러나므로, 각 설정이 지키는 경계와 그 경계가 무너지는 조건을 정확히 짚어야 한다.
producer는 앱에 내장되는 클라이언트다
producer는 앱에 내장되는 클라이언트 라이브러리다. 별도 서버가 아니라, 주문 API·배치 잡·로그 수집기 같은 앱 프로세스 안에서 동작한다.
Java producer 기본 코드
// 1. 설정
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("acks", "all");
props.put("enable.idempotence", "true");
// 2. producer 생성
Producer<String, String> producer = new KafkaProducer<>(props);
// 3. 메시지 전송
producer.send(new ProducerRecord<>("orders", "order-123", "주문 데이터"));
// 4. 종료
producer.close();
결제 버튼이 producer를 부를 때
사용자가 "결제" 버튼을 누르면: 주문 API가 DB에 저장 → producer로 orders topic에 이벤트 전송 → 주문 처리·분석·알림 서비스가 각자 소비(장 02 시나리오).
flowchart LR
U[결제 클릭] --> API[주문 API]
API -->|1. DB 저장| DB[(주문 DB)]
API -->|2. producer.send| P[producer]
P -->|메시지| T[orders topic]
T --> C1[주문 처리]
T --> C2[분석]
send는 비동기다 — 즉시 반환, 뒤에서 전송
send()는 비동기. 즉시 Future를 반환하고, 실제 전송은 배경 스레드(sender)가 처리. 애플리케이션이 broker 응답을 기다리며 멈추지 않도록 한다.
flowchart LR
A["send(record)"] --> ACC["accumulator<br/>(batch 모음)"]
ACC --> SND["sender 스레드<br/>(배경 전송)"]
SND --> L["broker"]
send 콜백으로 성공/실패 확인
producer.send(new ProducerRecord<>("orders", "order-123", "data"), (metadata, exception) -> {
if (exception == null) {
System.out.println("전송 성공: " + metadata.partition() + "/" + metadata.offset());
} else {
System.err.println("전송 실패: " + exception.getMessage());
}
});
batch — 왜 모아서 보내는가
메시지를 하나씩 보내지 않고 여러 개를 batch로 모아 보낸다. linger.ms(대기)나 batch.size(크기)에 도달하면 전송. 네트워크 왕복 비용을 줄인다.
batch는 압축과 짝 — batch로 뭉쳐야 압축 효율이 올라간다. linger.ms=0 + 압축은 무의미(메시지 1개씩이라 압축 안 걸림).
linger.ms와 batch.size가 처리량·지연을 어떻게 가르는가
이 두 설정은 trade-off 축이다:
linger.ms=0: batch를 안 모으고 즉시 전송 → 지연 최소, 처리량 낮음. 메시지 1개마다 네트워크 왕복이므로 RTT 비용이 그대로 든다.linger.ms증가: batch를 모으는 시간을 줌 → 지연 증가, 처리량 증가. 한 batch에 더 많은 메시지가 들어가 네트워크 효율이 오른다.batch.size(기본 16384=16KB)는 batch의 최대 바이트. 이 크기에 도달하면linger.ms를 안 기다리고 바로 전송. 너무 작으면 batch가 자주 차서 linger 의미가 없고, 너무 크면 메모리 낭비(producer는 partition마다 batch.size만큼 버퍼를 잡음).
실측 감(수치는 환경 의존, 미검증): 단일 partition에 초당 1000메시지, 메시지당 100바이트. linger.ms=0이면 초당 1000번의 네트워크 왕복. linger.ms=10이면 batch당 약 10메시지 → 초당 100번 왕복. 10배 차이. 처리량이 병목이면 linger를 키우는 게 가장 큰 한 방이다.
처리량 튜닝의 첫 단계는 거의 항상
linger.ms와compression.type을 키우는 것이다. 단, end-to-end 지연에 민감한(실시간 UX) 워크로드면 linger를 0에 가깝게 둬야 한다. "처리량 vs 지연"의 트레이드오프 축이 producer 설정의 핵심이다.
delivery.timeout.ms — 재시도 시간 모델
구형 Kafka 자료는 retries=MAX_INT를 권장했지만, 2.1+부터는 시간 기반 재시도 모델로 바뀌었다. 핵심 설정 두 개:
delivery.timeout.ms(기본 120000=2분): 메시지가 send 호출된 시점부터, 최종 성공/포기까지의 총 상한. 재시도를 포함한 전체 시간.request.timeout.ms(기본 30000): 단일 요청의 응답 대기 시간.
retries는 여전히 있지만, delivery.timeout.ms가 먼저 도달하면 그만둔다. "무한 재시도"는 사라졌다 — 대신 "2분 안에 안 되면 포기하고 예외"가 기본 동작. 이게 왜 중요한가 — 무한 재시도는 producer가 영원히 블록되거나 메모리를 채울 수 있어서, 시간 상한이 producer의 건강을 지킨다.
# 재시도 시간 모델 (4.x 기본값)
delivery.timeout.ms=120000 # 총 2분
request.timeout.ms=30000 # 단일 요청 30초
retries=2147483647 # 상한은 delivery.timeout.ms가 결정
delivery.timeout.ms가 작으면 네트워크 일시 장애에 민감해진다(재시도할 시간이 부족). 크면 장애 시 producer가 오래 블록된다. SLA(지연 허용치)에 맞춰 설정하는 게 정석이다.
acks — "얼마나 확실히 썼다고 볼 것인가"
acks |
의미 | 신뢰성 | 언제 |
|---|---|---|---|
0 |
확인 안 기다림 | 가장 낮음 | 손실 감내 로그 |
1 |
리더까지만 확인 | 중간 | 성능 우선 |
all(-1) |
리더 + ISR 전원 확인 | 가장 높음 | 신뢰성 필수 |
acks=all은 min.insync.replicas(예: 2)와 세트여야 의미 — ISR이 1개면 사실상 신뢰성 없음. 프로덕션 표준: replication.factor=3, min.insync.replicas=2, acks=all(장 06).
"acks=all이면 안전하다"가 틀리는 순간
acks=all은 이름과 달리 "모든 복제본"이 아니라 "현재 ISR에 있는 모든 복제본"을 기다린다. 여기서 함정이 드러난다 — ISR이 1개(leader만)로 줄어들면, acks=all은 사실상 acks=1과 같아진다. leader 한 번의 디스크 쓰기로 성공을 반환하므로, 그 직후 leader가 죽으면 미복제 메시지가 유실된다.
이 함정을 막는 게 min.insync.replicas다. ISR 수가 이 값 미만이면 broker가 쓰기를 거부(NotEnoughReplicasException). producer는 타임아웃/재시도로 대응한다. 즉 "안전한 쓰기"는 acks=all 혼자 만드는 게 아니라, acks=all + min.insync.replicas≥2 + replication.factor=3의 셋트가 만든다. 하나라도 빠지면 특정 장애 시나리오에서 구멍이 열린다.
flowchart TD
A["producer.send()"] --> B{"acks 설정"}
B -->|"0"| C["즉시 다음 라인<br/>(broker 응답 무시)"]
B -->|"1"| D["리더 append 대기<br/>(복제 안 기다림)"]
B -->|"all"| E{"ISR ≥ min.insync.replicas?"}
E -->|"O"| F["ISR 전원 fetch 대기<br/>→ HW 전진 → ack"]
E -->|"X"| G["NotEnoughReplicas<br/>쓰기 거부"]
acks=all을 "모든 복제본"으로 읽으면 ISR 동적 변화를 놓친다. ISR은 고정이 아니라 liveness·lag에 따라 줄었다 늘었다 하므로(장 06), "all"의 대상도 계속 바뀐다.
idempotent producer — 재시도해도 중복이 안 생기는 비밀
재시도를 켜면 같은 메시지가 두 번 쓰일 수 있다. idempotent producer는 broker가 중복을 걸러낸다:
enable.idempotence=true→ broker가 PID(producer ID) 발급 → 각 메시지에 sequence number → 중복 검출.
4.x 기본값은
true. 구형 자료(false 기본)와 혼동 주의.
idempotent 모드는 acks=all, retries>0, max.in.flight<=5를 자동 강제.
PID와 sequence number가 어떻게 중복을 잡는가
producer가 initTransactions() 없이 enable.idempotence=true로 처음 접속하면, broker는 이 producer에게 PID(producer ID) 와 epoch를 발급한다(InitProducerId 요청). 이후 producer가 보내는 모든 batch에는 (PID, partition, sequence number)가 붙는다. sequence number는 partition마다 0부터 시작해 단조증가한다.
broker는 partition별로 "마지막으로 commit된 sequence number"를 기억한다. 같은 (PID, partition, sequence)가 또 오면 broker가 중복으로 판단해 무시하고, 정상 ack는 반환한다. producer 입장에선 "재시도했지만 broker가 알아서 걸렀다"로 보인다.
sequenceDiagram
participant P as producer (PID=42)
participant B as broker
P->>B: InitProducerId → PID=42, epoch=0
P->>B: send batch(seq=0) → ack 타임아웃
P->>B: retry send batch(seq=0) → broker: seq=0 이미 받음 → 무시, ack
P->>B: send batch(seq=1) → 정상 commit
Note over P,B: consumer는 seq=0, seq=1 각각 한 번씩만 봄
max.in.flight가 5 이하인 이유
idempotent producer가 순서를 보존하려면, 재시도 중에도 sequence number 순서대로 broker에 commit돼야 한다. max.in.flight.requests.per.connection이 크면 여러 batch가 병렬로 날아가는데, 하나가 재시도하면 뒤 batch가 먼저 commit되는(out-of-order) 상황이 생길 수 있다. Kafka는 이를 막기 위해 idempotent 모드에서 max.in.flight ≤ 5를 강제하고, 그 범위 내에서 재시도 순서를 보존한다(Out Of Order Sequence Exception 방지). 이 5라는 상한은 5개 batch까지는 순서 복구가 가능하지만 그 이상은 보장 못 한다는 설계 한계다.
producer.properties — 전체 설정 예제
신뢰성 우선 (프로덕션 권장)
# 연결
bootstrap.servers=localhost:9092
# 직렬화
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.StringSerializer
# 신뢰성
acks=all
enable.idempotence=true
retries=2147483647
max.in.flight.requests.per.connection=5
# 배치/처리량
linger.ms=10
batch.size=16384
compression.type=zstd
# 재시도 상한 (총)
delivery.timeout.ms=120000
request.timeout.ms=30000
처리량 우선 (로그 수집)
acks=1
linger.ms=50
batch.size=131072
compression.type=zstd
→ 지연 수십 ms 희생, 처리량·네트워크 효율 최대.
지연 최소 (실시간 이벤트)
acks=1
linger.ms=0
compression.type=none
→ 처리량 희생, end-to-end 지연 최소.
Kafka 4.3 실습 — 직접 확인하기
console-producer
# Kafka 4.3, KRaft 단일 노드
bin/kafka-console-producer.sh --topic prod-demo --bootstrap-server localhost:9092
>Hello
>World
확인할 것: 각 줄이 topic에 메시지로 들어감. 다른 터미널에서 consumer로 읽어 확인.
추가 확인: producer 프로세스 종료 후에도 메시지는 broker에 남아 있다(로그 모델, 장 01). 다른 consumer를 --from-beginning으로 띄워 처음부터 다시 읽을 수 있다.
실행 팁: console-producer는 한 줄이 곧 한 메시지. 빈 줄을 입력하면 무시되고, Ctrl-D로 종료한다.
key와 함께 produce
bin/kafka-console-producer.sh --topic prod-demo \
--property "parse.key=true" --property "key.separator=:" \
--bootstrap-server localhost:9092
# userA:evt1 / userA:evt2 / userB:evt3
확인할 것: 같은 key는 같은 partition으로 감(장 02).
idempotence 기본값 확인
# Kafka 4.3, KRaft 단일 노드
# producer 설정 없이 console-producer를 띄워도 4.x는 idempotence=true가 기본.
# 다른 터미널에서 broker 로그를 보면 InitProducerId 요청이 찍히는지 확인(미검증: 로그 레벨 의존).
bin/kafka-console-producer.sh --topic prod-demo --bootstrap-server localhost:9092
# required:acks 컨피그가 -1(=all)로 강제됐는지 producer 로그에서 확인.
확인할 것: idempotence=true일 때 producer가 acks=all과 max.in.flight.requests.per.connection<=5를 자동 강제하는지. 구형 자료(false 기본)와 4.x(true 기본)의 차이를 눈으로 확인하는 게 함정 방지의 첫걸음이다.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "producer는 별도 서버다" | 앱에 내장되는 클라이언트 |
| "send() 반환됐으면 broker에 도달했다" | 비동기. accumulator에 있을 수 있음 |
| "acks=1이면 안전하다" | 리더 장애 시 미복제 손실 위험 |
| "재시도하면 무조건 중복" | idempotent(4.x 기본)는 broker가 제거 |
| "linger.ms=0이 항상 빠르다" | 지연은 최소지만 처리량은 낮음 |
| "4.x도 idempotence=false 기본" | 아니다. 4.x는 기본 true |
| "압축은 producer만 손해" | broker가 압축된 채 저장·전송 |
producer 메트릭 — 건강을 어떻게 읽는가
producer는 JMX(보통 포트 9999)로 메트릭을 노출한다. 운영에서 반드시 봐야 할 것들:
| 메트릭 | 의미 | 건강 신호 |
|---|---|---|
record-send-rate |
초당 전송 record 수 | 처리량 기준선 |
record-error-rate |
초당 실패 record 수 | 0이 정상. >0이면 broker/네트워크 문제 |
retry-rate |
초당 재시도 batch 수 | 높으면 불안정(broker 과부하·네트워크 불안) |
request-latency-avg |
평균 요청 지연 | 길어지면 broker 병목 |
batch-size-avg |
평균 batch 크기 | batch.size 대비 너무 작으면 batch가 안 참(linger/batch.size 튜닝 필요) |
records-per-request-avg |
요청당 평균 record 수 | linger 영향 확인 |
retry-rate가 계속 0보다 크면 producer가 빈번히 재시도하고 있다는 뜻이다. 흔한 원인: broker GC·디스크 포화·네트워크 지터. 처리량은 정상처럼 보여도 지연이 늘고, 결국 delivery.timeout.ms 도달 시 메시지 유실로 이어진다. 따라서 retry-rate는 producer 건강의 선행 지표다.
더 깊이
- max.in.flight와 순서: 1보다 크면 재시도 시 순서 뒤바뀔 수 있음. idempotent 모드는 5 이하로 제한해 순서 보존.
- delivery.timeout.ms: 총 재시도 상한(기본 2분). 시간 기반 모델(무한 retries 아님).
- transactional producer:
transactional.id로 여러 partition 원자 쓰기 → EOS(장 08). idempotent가 "단일 partition 중복 제거"라면, transactional은 "여러 partition 원자 쓰기 + offset commit까지 원자". 둘은 다른 층이다. - JMX metrics:
record-send-rate,retry-rate(높으면 불안정),batch-size-avg. - compression.type:
none/gzip/snappy/lz4/zstd. zstd가 압축율·속도 균형이 좋아 4.x 권장. 압축은 batch 단위로만 걸리므로,linger.ms=0이면 압축 효과가 거의 사라진다(batch에 메시지 1개). - partitioner 선택: 기본은 murmur2 해시(장 02). 사용자 정의 Partitioner를 끼워 key→partition 매핑을 직접 제어할 수 있다.
- buffer.memory: producer가 사용하는 총 버퍼(기본 64MB). 버퍼가 가득 차면
send()가 블록되거나 예외. 처리량이 큰 앱에선 늘려야 할 수 있다.
요약 — 이 글의 결론
- producer = 앱 내장 클라이언트.
send()로 topic에 메시지. - 비동기 send: accumulator→sender 배경 전송. 콜백으로 성공/실패 확인.
- batch:
linger.ms/batch.size로 모아 효율↑. 압축과 세트. acks: 0/1/all.all은min.insync.replicas와 세트. ISR이 1개면all도 무의미.- idempotent producer(4.x 기본): PID+sequence로 중복 제거 + 순서 보존.
max.in.flight≤5강제. - 설정 프로필: 신뢰성 우선(acks=all+idempotent) / 처리량 우선(linger 큼) / 지연 최소(linger=0).
- 시간 기반 재시도:
delivery.timeout.ms(기본 2분)가 총 상한.retries가 무한이어도 시간 도달 시 포기. - producer 메트릭:
retry-rate(선행 지표),record-error-rate,batch-size-avg로 건강 모니터링.
생각해 볼 문제
- producer가 "앱 안의 클라이언트"라는 것이 운영상 어떤 의미인가?
- send()가 비동기인 것을 어떻게 확인하는가? (콜백)
acks=0/1/all의 차이를 복제 관점에서 설명하라.- idempotent producer가 중복을 어떻게 제거하는가? 4.x 기본값은?
- 신뢰성 우선 producer.properties를 작성해 보라.
linger.ms=0+ compression이 무의미한 이유는?acks=all인데 ISR이 1개 남았을 때 신뢰성이 무너지는 이유를 설명하라. 이를 막는 설정은?delivery.timeout.ms가 2분인데 broker가 3분간 응답하지 않으면 producer는 어떻게 되는가?retry-rateJMX 메트릭이 지속적으로 0보다 크면 무엇을 의심해야 하는가?
참고
- Producer configs (producer-configs.txt) - 접근 2026-07-09
- ProducerConfig.java (idempotence/delivery.timeout) - 접근 2026-07-09
- KIP-98(idempotent producer/transactions), KIP-588(zombie fencing)
'Data & Platform > Event Streaming' 카테고리의 다른 글
| Kafka - 06. replication (0) | 2026.07.09 |
|---|---|
| Kafka - 05. consumer group (0) | 2026.07.09 |
| Kafka - 04. consumer (0) | 2026.07.09 |
| Kafka - 02. topic and partition (0) | 2026.07.09 |
| Kafka - 01. architecture (0) | 2026.07.09 |