| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- springboot
- 이벤트드리븐
- DI
- Kafka순서보장
- 카프카실무
- 카프카설계
- 자기계발
- kafka
- 추천
- frontend
- GC
- KafkaOrdering
- enum
- Javascript
- 자바스크립트
- 의존성주입
- 타입스크립트
- HTTP란
- TypeScript
- 백엔드개발
- KafkaFollower
- TCP/IP
- MSA
- vue store
- 실천방법
- Vue+Typescript
- 멱등성Producer
- webpack
- 인생꿀팁
- ECMAScript
- Today
- Total
끄적끄적
Kafka 실무 완전 정복 — Consumer Lag 폭증·메시지 중복·파티션 설계 실수 직접 겪고 해결한 이야기 본문
Kafka 실무 완전 정복 — Consumer Lag 폭증·메시지 중복·파티션 설계 실수 직접 겪고 해결한 이야기
mashko 2026. 2. 25. 23:22Kafka, 개념
"실무에서 터진 문제들과 해결법"
Consumer Lag 폭증 · 파티션 설계 실수 · 메시지 중복/유실 — 직접 겪고 해결한 이야기
📋 목차
- 들어가며 — Kafka를 처음 실무에 도입했을 때
- Kafka 핵심 개념 — 딱 알아야 할 것만
- Topic · Partition · Consumer Group 설계 전략
- Spring Boot + Kafka 실전 연동 코드
- Producer 설정 — 메시지 유실 없애는 법
- Consumer 설정 — 중복 처리와 멱등성
- 실무에서 터진 트러블슈팅 BEST 5
- Consumer Lag 모니터링 세팅
- Kafka vs RabbitMQ — 언제 뭘 쓸까
들어가며 — Kafka를 처음 실무에 도입했을 때
사내 서비스 간 이벤트 처리를 REST API로 직접 호출하다가 한계에 부딪혔습니다. 서비스 A가 서비스 B를 직접 호출하는 구조에서 B가 잠깐 다운되면 이벤트가 그냥 사라졌고, 트래픽 폭증 시 API 타임아웃이 연쇄적으로 터졌어요.
그때 Kafka를 도입했는데, 처음엔 개념 이해도 어렵고 설정값 하나 잘못 건드렸다가 메시지 수십만 건을 중복 처리하거나 유실시키는 사고를 겪었습니다. 이 글은 그 과정에서 몸으로 배운 것들을 정리한 내용입니다.
① Kafka를 왜 쓰는지, 언제 쓰면 안 되는지
② 파티션 개수를 잘못 설계하면 왜 망하는지
③ at-least-once vs exactly-once — 실무에선 뭘 선택해야 하는지
④ Consumer Lag이 갑자기 폭증할 때 어떻게 대응하는지
Kafka 핵심 개념 — 딱 알아야 할 것만
메시지 발행
Topic: order-events
주문 처리 서비스
offset: 0~N
offset: 0~N
offset: 0~N
알림 서비스
| 개념 | 한 줄 설명 | 실무 포인트 |
|---|---|---|
| 🟠 Topic | 메시지를 분류하는 카테고리 (DB 테이블 비유) | 서비스 도메인 단위로 분리 추천 |
| 🟠 Partition | Topic의 물리적 분할 단위, 병렬 처리의 핵심 | 한 번 늘리면 줄이기 매우 어려움 ⚠️ |
| 🟠 Offset | 파티션 내 메시지의 순번 (변경 불가) | Consumer가 어디까지 읽었는지 추적 |
| 🟠 Consumer Group | 같은 Topic을 독립적으로 소비하는 Consumer 묶음 | 서비스별로 그룹 분리 필수 |
| 🟠 Broker | Kafka 서버 1대 | 최소 3대 클러스터 구성 권장 |
| 🟠 Consumer Lag | Producer가 보낸 것과 Consumer가 읽은 것의 차이 | 이게 늘어나면 장애 신호 🚨 |
| 🟠 Replication Factor | 메시지 복제 수 (장애 대비) | 운영환경 최소 3 권장 |
Topic · Partition · Consumer Group 설계 전략
파티션 개수 — 처음부터 넉넉하게, 하지만 무작정 늘리면 안 된다
파티션은 늘릴 수 있지만 줄일 수 없습니다. 또 파티션 수 = Consumer 최대 병렬 처리 수입니다. Consumer가 3개인데 파티션이 1개면 나머지 2개는 놀고, 파티션이 10개인데 Consumer가 3개면 한 Consumer가 여러 파티션을 담당합니다.
Partition Key 설계 — 순서 보장이 필요하다면 반드시
Kafka는 같은 파티션 내에서만 순서를 보장합니다. 예를 들어 사용자 A의 주문 이벤트가 순서대로 처리돼야 한다면, userId를 파티션 키로 써야 합니다. 그래야 사용자 A의 메시지가 항상 같은 파티션으로 들어가요.
Consumer Group 분리 전략 — 서비스별로 독립 그룹
같은 Topic을 여러 Consumer Group이 독립적으로 읽을 수 있는 것이 Kafka 최대 강점입니다. 예를 들어 order-events Topic을 주문처리 서비스, 알림 서비스, 통계 서비스가 각각 독립적인 Consumer Group으로 소비하면, 한 서비스가 느려져도 다른 서비스에 영향이 없습니다.
Spring Boot + Kafka 실전 연동 코드
spring:
kafka:
bootstrap-servers: kafka1:9092,kafka2:9092,kafka3:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
# 메시지 유실 방지 핵심 설정
acks: all # 모든 replica 확인 후 ack (유실 방지)
retries: 3
properties:
enable.idempotence: true # 중복 방지 (exactly-once producer)
max.in.flight.requests.per.connection: 5
consumer:
group-id: order-service-prod
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
# 오프셋 자동 커밋 OFF — 수동 커밋으로 메시지 처리 보장
enable-auto-commit: false
auto-offset-reset: earliest
properties:
spring.json.trusted.packages: "com.myapp.event"
@Service
@RequiredArgsConstructor
public class OrderEventProducer {
private static final String TOPIC = "order-events";
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
private static final Logger log = LoggerFactory.getLogger(OrderEventProducer.class);
public void sendOrderEvent(OrderEvent event) {
// userId를 파티션 키로 → 같은 사용자 메시지는 같은 파티션
var record = new ProducerRecord<>(
TOPIC,
event.getUserId(), // partition key
event
);
kafkaTemplate.send(record)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Kafka 전송 실패 | orderId={} | error={}",
event.getOrderId(), ex.getMessage());
// TODO: Dead Letter Queue 또는 DB Outbox 패턴으로 보상
} else {
log.info("전송 완료 | partition={} | offset={}",
result.getRecordMetadata().partition(),
result.getRecordMetadata().offset());
}
});
}
}
@Service
@RequiredArgsConstructor
public class OrderEventConsumer {
private final OrderService orderService;
private final ProcessedEventRepository processedEventRepo;
@KafkaListener(
topics = "order-events",
groupId = "order-service-prod",
containerFactory = "kafkaListenerContainerFactory"
)
public void consume(
ConsumerRecord<String, OrderEvent> record,
Acknowledgment ack // 수동 커밋용
) {
OrderEvent event = record.value();
String eventId = event.getEventId();
try {
// 멱등성 체크 — 이미 처리한 이벤트면 스킵
if (processedEventRepo.existsByEventId(eventId)) {
log.warn("중복 이벤트 스킵 | eventId={}", eventId);
ack.acknowledge(); // 중복이어도 커밋은 해야 함
return;
}
// 비즈니스 로직 처리
orderService.processOrder(event);
// 처리 완료 기록 (중복 방지용)
processedEventRepo.save(new ProcessedEvent(eventId));
// 처리 성공 시에만 offset 커밋
ack.acknowledge();
} catch (Exception e) {
log.error("이벤트 처리 실패 | eventId={} | partition={} | offset={}",
eventId, record.partition(), record.offset(), e);
// ack 안 함 → 재처리 (at-least-once 보장)
// 무한 재시도 방지는 DLT(Dead Letter Topic)로 해결
}
}
}
자동 커밋을 켜두면 메시지를 받는 순간 offset이 커밋됩니다. 그러다 처리 도중 서버가 죽으면? 메시지는 유실됩니다. 수동 커밋으로 "처리 완료 후 커밋" 패턴을 쓰면 최소 한 번(at-least-once) 처리를 보장할 수 있습니다.
실무에서 터진 트러블슈팅 BEST 5
Case 1 — Consumer Lag이 갑자기 수십만으로 폭증
해결책 → Consumer 인스턴스 스케일 아웃 (파티션 수만큼 늘릴 수 있음) + 외부 API에 타임아웃 설정 + 비동기 처리로 전환.
Case 2 — 메시지 중복 처리로 결제가 2번 됨
Case 3 — Rebalancing 폭풍으로 Consumer 전체 멈춤
Case 4 — 파티션 수 부족으로 병렬 처리 불가
Case 5 — 역직렬화 오류로 Consumer 완전 멈춤
Consumer Lag 모니터링 세팅
Kafka Exporter → Prometheus → Grafana 조합이 현재 실무 표준입니다.
핵심 지표: kafka_consumergroup_lag (Consumer Lag), kafka_topic_partitions, kafka_brokers
Lag이 임계값(예: 10,000건) 초과 시 Slack 알림 연동 필수입니다.
# Consumer Group 목록 확인
kafka-consumer-groups.sh \
--bootstrap-server kafka1:9092 \
--list
# 특정 Consumer Group의 Lag 상세 확인
kafka-consumer-groups.sh \
--bootstrap-server kafka1:9092 \
--describe \
--group order-service-prod
# 출력 예시:
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# order-events 0 1500 1500 0 ← 정상
# order-events 1 800 5000 4200 ← 위험!
Kafka vs RabbitMQ — 언제 뭘 쓸까?
| 항목 | Kafka | RabbitMQ |
|---|---|---|
| 처리량 | 초당 수백만 건 | 초당 수만 건 |
| 메시지 보관 | 디스크 저장, 재처리 가능 | 소비 후 삭제 (기본) |
| Consumer 그룹 | 여러 그룹이 독립 소비 | 하나의 큐, 하나의 소비자 그룹 |
| 순서 보장 | 파티션 내 보장 | 단일 큐 내 보장 |
| 학습 난이도 | 높음 | 낮음 |
| 운영 난이도 | 높음 (클러스터) | 낮음 |
| 적합한 상황 | 대용량 이벤트 스트리밍, 로그 수집 | 소규모 작업 큐, RPC 패턴 |
📌 Kafka를 쓸 때: 대용량 이벤트, 여러 서비스가 같은 이벤트를 소비, 메시지 재처리가 필요할 때, 데이터 파이프라인
📌 RabbitMQ를 쓸 때: 단순 작업 큐, 소규모 트래픽, 빠른 구축이 필요할 때, 복잡한 라우팅 규칙이 필요할 때
⚡ "Kafka는 도입보다 운영이 어렵습니다"
Kafka를 도입하고 나서 가장 많은 시간을 쓴 건 개발이 아니라 운영이었습니다.
Consumer Lag 모니터링, 파티션 재설계, 멱등성 처리...
이 글에서 다룬 내용들을 처음부터 알았더라면 훨씬 수월했을 텐데요 😅
비슷한 경험이 있으시면 댓글로 공유해 주세요!
'Back-end > Java' 카테고리의 다른 글
| 서버가 새벽마다 OOM으로 죽었다 — Java 메모리 누수 패턴 7가지와 Heap Dump로 범인 잡는 법 (1) | 2026.02.26 |
|---|---|
| Spring Boot Filter vs Interceptor — 실무에서 겪은 차이점 완전 정리 (코드 예제 포함) (0) | 2026.02.25 |
| [SPOCK] Spock의 Mock, Stub, Spy에 대해서 알아보자 (0) | 2022.09.13 |
| [JAVA] @Transaction 전파 및 격리에 대한 정리 (0) | 2022.04.28 |
| gradle 환경에서 apache log4j 2.15.0 버전 업데이트하기 (0) | 2021.12.13 |
