Kafka란?
링크드인에서 출발한 분산 이벤트 스트리밍 플랫폼이며, 초당 수십만 ~ 수백만 건의 메세지를 처리하는 고성능 메세지 시스템이다.
데이터를 디스크에 영구 저장하고, 여러 컨슈머(메세지 처리하는 주체) 가 반복 소비할 수 있다.
분산 스트리밍 플랫폼으로, 대량의 데이터를 처리하고 실시간으로 전송하는 데 사용된다. 모든 데이터는 로그 형식으로 파일 시스템에 기록하고, 로그를 한곳에 모아 처리할 수 있도록 중앙집중화되어 있으며, 대용량 데이터를 수집하고 실시간 스트리밍으로 소비가 가능.

기존 메세지 큐와는 무엇이 다를까?
전통적인 메세지 큐는 메모리 중심이며, 소비 후 삭제하는 구조이다. 메세지 재처리가 처리 했나/ 안 했나 단순 이 기준으로만 가능하지만, 카프카는 Offset 기반으로 어디까지 처리했는지를 알고 자유롭게 재처리가 가능하다.
또한 이벤트 스트리밍, 로그 수집, 데이터 파이프라인을 짤 때도 유용하며, 컨슈머도 파티션 단위로 자유롭게 확장할 수 있다.
파티션(물리적인 저장) 안에서만 offset이 보장이 됨.
- 예를 들어서, 파티션안에 들어가있는 1,3 / 2,4 이면 1->3은 보장, 2->4는 보장이나 1->2는 보장이 되지 않음.
- 예시로, 같은 결제건 안에서는 순서가 일치해야하기 때문에 TID같은 거래 아이디로 키값을 설정해주는 방법
이 파티션의 키는 프로듀서가 결정하며, 해시값으로 나뉜다. 이것은 카프카가 보장해주는 것이 아니며, 프로듀서 쪽에서 해시값을 잘 설정해야 한다.
Kafka 구성요소
Producer = Spring 결제 서버가 Kafka에 이벤트 보내는 애
Broker = Kafka 서버, 파티션 로그를 디스크에 저장하는 애
Topic = payment-events 같은 논리적 이벤트 이름
Partition = 이벤트가 실제 순서대로 쌓이는 로그 단위
Offset = 파티션 안에서 이벤트의 번호
Consumer = 이벤트를 읽어서 후속 처리하는 Spring 서버
Consumer Group = 여러 consumer가 파티션을 나눠 읽는 묶음
[결제 서비스]
Producer
↓
[Kafka Broker]
Topic / Partition / Offset 저장
↓
[알림 서비스]
Consumer
Producer는 메시지를 보내는 애플리케이션이다.
Broker는 메시지를 저장하고 Consumer가 읽을 수 있게 관리하는 Kafka 서버이다.
Consumer는 메시지를 읽고 후속 작업을 처리하는 애플리케이션이다.
Producer와 Consumer는 Broker 안에 있는 것이 아니라 Broker에 접속하는 클라이언트 애플리케이션이다.
카프카는 메모리에 기록이 아니라, 디스크에 기록을 하는데, 파티션(물리적인 큐 안에 파일로 기록) 을 단위로 기록한다.
Kafka에서 파티션을 어떻게 나누는가?
파티션 키를 통해 메세지를 어느 파티션에 넣을지를 결정한다.
프로듀서가 카프카에 메세지를 보낼 때 보통 이렇게 보낸다.
"topic은 이거고, key는 이거고, value는 이거야."
카프카 프로듀서는 키를 해시해서 파티션을 정하는데,
- hash(key) % partitionCount
예를 들어 파티션이 3개면:
paymentId=100 → hash(100) % 3 → partition 1
paymentId=101 → hash(101) % 3 → partition 0
paymentId=102 → hash(102) % 3 → partition 2
중요한 건 같은 key는 같은 파티션으로 간다.
paymentId=100, PAYMENT_APPROVED → partition 1
paymentId=100, COIN_ISSUED → partition 1
paymentId=100, SMS_REQUESTED → partition 1
중요한 점이, 카프카는 같은 토픽의 파티션 내에서만 이벤트 순서가 보장되기 때문에, 같은 결제 건의 이벤트 순서를 보장하고 싶으면 키를 같게하여 같은 파티션으로 보내야한다.
키가 없으면, 카프카가 라운드로빈 방식을 사용하여 파티션에 나눠넣을 수도 있다.
Kafka 에서의 브로커
브로커는 카프카 클러스터를 구성하는 서버이다. 프로듀서가 보낸 메세지를 받고, 토픽의 파티션 로그에 저장하고, 컨슈머가 오프셋 기준으로 읽어갈 수 있게 해준다.
프로듀서가 해당 키의 파티션의 리더 브로커에게 메세지를 보낸다.
리더 브로커는 받은 메세지를 파티션 로그에 저장한다.
메세지는 같은 파티션 안에서 순서대로 오프셋을 가진다. 이 오프셋도 브로커가 부여한다.
컨슈머에게 메세지 제공하는 역할도 한다. (컨슈머: 뫄뫄토픽의 파티션0의 오프셋 0부터 줘봐)
카프카 메세지는 디스크에 저장되는데, 브로커가 1대면 모든 파티션 로그가 한 서버 디스크에 몰릴 수 있다.하지만 브로커를 여러대 두면 파티션을 나눠 저장할 수 있다.
또한 브로커를 여러대 두면 프로듀서의 쓰기 요청/ 컨슈머의 메세지 읽기 요청이 여러 브로커로 분산된다.
또한 하나의 카프카로는 카프카 장애 시 복구하기가 힘드므로, 여러개를 둬 하나가 죽으면 하나를 리더로 승격시킨다.
Kafka Consumer에서 후속 작업은 어떻게 실행되는가
Spring Boot에서는 보통 @KafkaListener를 사용한다.
@Component
@RequiredArgsConstructor
public class PaymentCompletedConsumer {
private final NotificationService notificationService;
@KafkaListener(
topics = "payment-completed-topic",
groupId = "notification-service"
)
public void consume(PaymentCompletedEvent event) {
notificationService.sendPaymentCompletedNotification(event);
}
}
동작 흐름:
Kafka Broker
↓
Spring Boot 내부 Kafka Consumer가 polling
↓
메시지 수신
↓
@KafkaListener 실행
↓
Service 호출
↓
알림 발송 / 쿠폰 지급 / DB 저장
↓
offset commit
서버가 여러 대일 때 Kafka Consumer 동작
같은 Consumer Group에 속한 Consumer들은 메시지를 나눠서 처리한다.
Topic: payment-completed
Partition 0 → 서버1 Consumer
Partition 1 → 서버2 Consumer
Partition 2 → 서버3 Consumer
같은 groupId라면 같은 메시지를 여러 서버가 중복 처리하지 않는다. Kafka가 파티션 단위로 Consumer에게 할당한다.
Consumer 수가 Partition 수보다 많으면 남는 Consumer는 대기한다.
Partition 3개
Consumer 5개
→ 3개 Consumer만 처리
→ 2개 Consumer는 대기
Kafka Broker에 메시지를 저장해야 하는 이유
핵심은 Kafka가 단순 전달 장치가 아니라 “내구성 있는 이벤트 로그”이기 때문이다.
DB에 저장할 수도 있지만, DB는 주로 현재 상태를 저장하는 데 적합하고 Kafka는 서비스 간 이벤트 전달과 재처리에 적합하다.
예를 들어 결제 완료 후 알림, 쿠폰, 정산이 각각 필요하다고 하자.
결제 완료
↓
알림 서비스
쿠폰 서비스
정산 서비스
이걸 DB만으로 처리하면 각 서비스가 결제 DB를 계속 조회하거나, 상태 컬럼을 두고 polling해야 한다.
알림 서비스: 결제 DB polling
쿠폰 서비스: 결제 DB polling
정산 서비스: 결제 DB polling
문제는 다음과 같다.
1. DB 부하 증가
2. 서비스 간 결합도 증가
3. 어떤 이벤트를 어디까지 처리했는지 추적 어려움
4. 장애 후 재처리 구조를 직접 만들어야 함
5. 여러 후속 서비스가 같은 이벤트를 독립적으로 소비하기 어려움
Kafka는 메시지를 Broker에 저장하고 Consumer가 각자 offset을 관리한다.
payment-completed-topic
↓
notification-service group offset
coupon-service group offset
settlement-service group offset
그래서 같은 이벤트를 여러 서비스가 독립적으로 처리할 수 있다.
결제 완료 이벤트 1개
↓
알림 서비스도 소비
쿠폰 서비스도 소비
정산 서비스도 소비
즉 Kafka Broker에 메시지를 저장하는 이유는 다음과 같다.
1. Producer와 Consumer를 분리하기 위해
2. Consumer 장애 시 나중에 다시 읽기 위해
3. 여러 서비스가 같은 이벤트를 독립적으로 소비하기 위해
4. 처리 위치를 offset으로 관리하기 위해
5. 대량 이벤트를 순서 있게 처리하기 위해
6. DB polling 구조를 피하기 위해
왜 DB가 아니라 Kafka인가
DB와 Kafka는 목적이 다르다.
DB
= 비즈니스 상태 저장
= 현재 데이터 조회/수정에 강함
= 트랜잭션 정합성 중심
Kafka
= 이벤트 로그 저장
= 서비스 간 비동기 전달에 강함
= 재처리, fan-out, 순차 처리 중심
DB에 이벤트 테이블을 만들 수도 있다. 하지만 DB만 사용하면 Consumer Group, offset, partition, replay, fan-out 같은 기능을 직접 구현해야 한다.
Kafka는 이걸 기본 기능으로 제공한다.
DB로 구현해야 하는 것
= 이벤트 테이블
= 처리 상태 테이블
= polling 스케줄러
= 중복 처리 방지
= 재시도 로직
= 장애 복구 로직
= 서비스별 처리 위치 관리
Kafka가 제공하는 것
= topic
= partition
= offset
= consumer group
= replay
= replication
= retention
Kafka 선택의 장점
Kafka를 선택했을 때의 장점은 다음과 같다.
1. 서비스 간 결합도 감소
2. Producer는 Consumer 성공 여부를 기다리지 않아도 됨
3. Consumer 장애 시 offset 기반 재처리 가능
4. 여러 서비스가 같은 이벤트를 독립적으로 소비 가능
5. Partition 기반 병렬 처리 가능
6. Broker에 메시지가 저장되어 일시 장애에 강함
7. 대량 이벤트 처리에 적합
예시:
결제 서비스는 payment-completed-topic에 이벤트만 발행
알림 서비스는 알림만 처리
쿠폰 서비스는 쿠폰만 처리
정산 서비스는 정산만 처리
결제 서비스는 알림, 쿠폰, 정산 서비스의 내부 로직을 알 필요가 없다.
Kafka의 페인포인트
Kafka도 만능은 아니다.
1. 운영 복잡도 증가
2. Broker, Topic, Partition, Consumer Group 관리 필요
3. 중복 처리 가능성 존재
4. 순서 보장은 Partition 내부에서만 가능
5. 정확히 한 번 처리 설계가 어렵다
6. 메시지 스키마 관리 필요
7. 장애 상황에서 offset commit 전략을 잘못 잡으면 유실 또는 중복 처리 발생
Kafka Consumer는 메시지를 처리한 뒤 offset을 commit한다. 그런데 처리 성공 전에 offset을 commit하면 메시지 유실이 생길 수 있다.
메시지 수신
↓
offset 먼저 commit
↓
비즈니스 로직 실패
↓
Kafka는 이미 처리했다고 봄
↓
유실
반대로 비즈니스 로직은 성공했는데 offset commit 전에 장애가 나면 중복 처리가 생길 수 있다.
메시지 수신
↓
DB 저장 성공
↓
offset commit 전 서버 장애
↓
재시작 후 같은 메시지 재수신
↓
중복 처리
그래서 Consumer 로직은 멱등하게 설계해야 한다.
Kafka 사용 시 보완책
Kafka를 사용할 때는 다음 보완책이 필요하다.
1. Consumer 멱등성 보장
2. 이벤트 ID 기반 중복 처리 방지
3. DLQ 구성
4. 재시도 정책 설계
5. offset commit 시점 명확화
6. 메시지 스키마 버전 관리
7. 모니터링 구성
예를 들어 알림 발송 이력 테이블에 이벤트 ID를 unique key로 둘 수 있다.
event_id unique
그러면 같은 메시지를 두 번 받아도 중복 저장을 막을 수 있다.
if (notificationHistory.existsByEventId(event.eventId())) {
return;
}
notificationHistory.save(...);
notificationSender.send(...);
Kafka의 대안
1. Spring Event
같은 애플리케이션 내부에서 간단히 관심사를 분리할 때 적합하다.
장점:
- 구현 간단
- 외부 인프라 불필요
- 트랜잭션 AFTER_COMMIT 연계 가능
단점:
- 인메모리라 유실 가능
- 재처리 어려움
- 서비스 간 통신에는 부적합
DB Outbox Pattern
DB 트랜잭션 안에서 비즈니스 데이터와 이벤트 데이터를 같은 DB에 저장하는 방식이다.
결제 DB 저장
+
outbox_event 테이블 저장
하나의 트랜잭션으로 commit
그 후 별도 Publisher가 outbox 테이블을 읽어 Kafka나 메시지 브로커로 발행한다.
payment table
outbox_event table
↓
Outbox Publisher
↓
Kafka
장점:
1. DB 저장과 이벤트 저장의 원자성 보장
2. 서버 장애 시에도 outbox 테이블에 이벤트 남음
3. 재시도 가능
단점:
1. outbox 테이블 관리 필요
2. publisher 구현 필요
3. 이벤트 발행 지연 가능
RabbitMQ
RabbitMQ는 전통적인 메시지 큐에 가깝다. 작업 큐, 라우팅, 즉시 전달에 강하다.
장점:
- 라우팅 기능 강함
- 작업 큐에 적합
- 메시지 ack/nack 처리 직관적
단점:
- Kafka처럼 긴 기간 이벤트 로그를 보관하고 replay하는 구조에는 상대적으로 약함
- 대규모 이벤트 스트리밍에는 Kafka가 더 적합한 경우가 많음
DB Polling
이벤트 테이블이나 상태 컬럼을 두고 스케줄러가 주기적으로 조회하는 방식이다.
장점:
- 구현이 단순
- 별도 브로커 불필요
단점:
- DB 부하 증가
- 실시간성 낮음
- 중복/재처리/상태 관리 직접 구현 필요
기술 선택 기준
Spring Event를 선택할 때
- 같은 애플리케이션 내부 관심사 분리
- 유실되어도 큰 문제가 없는 부가 작업
- 간단한 알림, 로그, 캐시 삭제
- 외부 인프라를 늘리기 부담스러운 경우
Kafka를 선택할 때
- 서비스 간 이벤트 전달이 필요할 때
- 이벤트 유실이 치명적일 때
- 장애 후 재처리가 필요할 때
- 여러 Consumer가 같은 이벤트를 독립적으로 처리해야 할 때
- 대량 이벤트를 병렬 처리해야 할 때
Outbox Pattern을 선택할 때
- DB 저장과 이벤트 발행의 원자성이 중요할 때
- 결제, 정산, 포인트 지급처럼 유실되면 안 되는 이벤트
- Kafka 발행 실패까지 고려해야 할 때
27. 결제 시스템에서의 선택 예시
결제 완료 후 알림만 보내는 단순 구조라면 Spring Event + AFTER_COMMIT도 가능하다.
결제 commit 성공
↓
AFTER_COMMIT
↓
알림 발송
하지만 코인 지급, 쿠폰 지급, 정산 생성처럼 반드시 처리되어야 하는 후속 작업이라면 Spring Event만으로는 부족하다.
더 안정적인 구조는 다음과 같다.
결제 트랜잭션
↓
payment 저장
↓
outbox_event 저장
↓
commit
↓
Outbox Publisher
↓
Kafka 발행
↓
각 Consumer 처리
이 구조는 결제 저장과 이벤트 저장을 하나의 DB 트랜잭션으로 묶을 수 있어 이벤트 유실 위험을 줄인다.
28. 면접 답변 정리
TransactionalEventListener 동작 구조
@TransactionalEventListener(phase = AFTER_COMMIT)은 이벤트가 발행되면 즉시 리스너를 실행하지 않고, 현재 트랜잭션에 TransactionSynchronization 콜백을 등록합니다. 이후 트랜잭션이 정상 commit되면 afterCommit 시점에 리스너가 실행됩니다. 이 구조는 Kafka처럼 외부 브로커에 저장되는 것이 아니라 같은 JVM 안에서 동작하는 인메모리 이벤트 구조입니다.
AOP인가?
@Transactional은 AOP 기반이지만, @TransactionalEventListener 자체는 AOP라기보다는 이벤트 리스너와 트랜잭션 동기화 콜백을 결합한 구조입니다.
TransactionSynchronization이란?
TransactionSynchronization은 트랜잭션 커밋/롤백 전후에 실행할 콜백을 정의하는 스프링 인터페이스입니다. TransactionSynchronizationManager가 현재 쓰레드의 트랜잭션 컨텍스트에 해당 콜백을 등록하고 관리합니다.
Kafka Broker에 메시지를 저장하는 이유
Kafka Broker에 메시지를 저장하는 이유는 Producer와 Consumer를 분리하고, Consumer 장애 시 offset 기반으로 재처리하기 위해서입니다. DB는 현재 상태 저장과 트랜잭션 정합성에 강하지만, Kafka는 이벤트 로그 저장, fan-out, replay, consumer group 기반 병렬 처리에 강합니다. DB만으로 처리하면 polling, 처리 상태 관리, 재시도, 중복 방지, 서비스별 offset 관리를 직접 구현해야 합니다.
Kafka 선택의 장점과 단점
Kafka의 장점은 서비스 간 결합도를 낮추고, 메시지를 Broker에 저장해 장애 후 재처리할 수 있으며, 여러 Consumer가 같은 이벤트를 독립적으로 소비할 수 있다는 점입니다. 단점은 운영 복잡도, offset commit 전략, 중복 처리 가능성, Partition 단위 순서 보장, 메시지 스키마 관리가 필요하다는 점입니다. 따라서 Kafka를 사용할 때는 Consumer 멱등성, DLQ, 재시도 정책, 이벤트 ID 기반 중복 방지 설계가 필요합니다.
'F-LAB' 카테고리의 다른 글
| [가상 면접 사례로 배우는 대규모 시스템 설계 기초] 4장 (0) | 2026.09.16 |
|---|---|
| [가상 면접 사례로 배우는 대규모 시스템 설계 기초] 2장 (0) | 2026.08.31 |
| Redis SET NX와 MySQL Unique 제약을 활용한 TID 중복 처리 검증 (0) | 2026.08.25 |
| Reveal2021 - 쿠팡의 대규모 트래픽을 다루는 백앤드 전략 (0) | 2026.08.19 |
| 토스증권의 실시간 시세 적용기 정리 (0) | 2026.08.18 |
꾸준하면 언젠가
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!