본문 바로가기
Dev

Kafka 간보기

by Day-T 2026. 4. 7.

 

취미로 진행 중인 사이드 프로젝트 커머스 플랫폼(pick-me)에서 Kafka를 사용하면서 배운 점을 정리했다.


1. Event Driven Architecture (EDA)

Event Driven Architecture는 분산 시스템에서 이벤트의 발행과 구독을 통해 서비스 간 통신을 처리하는 아키텍처다. 동기 통신의 요청-응답(Request-Response) 모델 대신, 비동기 통신의 발행-구독(Pub-Sub) 모델을 사용한다.

이벤트가 생성되면 이벤트 로그에 보관되고 해당 이벤트를 필요로 하는 구독자가 이를 받아 처리한다.

### EDA 적용 전

주문 서비스와 결제, 재고, 통지 서비스가 REST로 동기 통신하는 구조를 가정한다.

주문이 생성되면 주문 서비스가 결제 서비스를 직접 호출하고, 결제가 완료되면 다시 재고 서비스와 통지 서비스를 순차적으로 호출한다. 이 구조에서는 결제 서버가 다운되면 결제 실패 에러가 주문 서비스까지 전파된다. 재고 서비스나 통지 서비스에 장애가 발생하더라도 마찬가지다. 책임과 역할이 완벽하게 분리되지 않은 상태이며 서비스 간 장애 전파의 위험이 존재한다.

### EDA 적용 후

pick-me 프로젝트에서는 주문이 생성되면 `OrderPlacedEvent`를 Kafka로 발행하고, 결제/재고/통지 서비스가 각각 해당 토픽을 구독하여 독립적으로 처리한다.


이를 통해 다음과 같은 이점을 얻을 수 있다.

이점 설명
책임과 역할 분리 주문 서비스는 이벤트 발행만, 결제/재고/통지 서비스는 각자 메시지 소비만 담당한다
서비스 간 의존도 감소 결제 서버 장애가 주문 서비스에 영향을 주지 않는다
발행자 정보 은닉 구독하는 서비스는 메시지를 발행한 서비스의 정보를 알 필요가 없다 
재시도 처리 실패한 메시지는 Dead Letter Topic에 보관하여 재처리할 수 있다


또한, MSA 환경에서 서로 다른 서비스에 걸친 기능 수행 중 일관된 롤백이 필요한 경우, Failed 이벤트를 발행하고 보상 트랜잭션을 수행할 수 있다. pick-me에서는 결제가 실패하면 `PaymentFailedEvent`를 발행하고 주문 서비스가 이를 구독하여 `OrderCancelledEvent`를 발행하며 재고 서비스가 이를 구독하여 예약된 재고를 복원(`InventoryRestoredEvent`)하는 방식으로 보상 트랜잭션을 구현한다.


2. Apache Kafka 기본 개념

Apache Kafka는 분산 환경에서 대규모 메시지를 안정적으로 전송, 수집, 활용할 수 있도록 설계된 발행-구독(Pub-Sub) 모델의 이벤트 스트리밍 플랫폼이다.

기존의 RabbitMQ와 같은 메시지 큐 대비 높은 처리량과 안정성, 확장성을 제공한다.

### 핵심 용어

용어 설명
Broker Kafka 애플리케이션이 설치되어 있는 서버 또는 노드
Topic Producer와 Consumer가 메시지를 구분하기 위한 고유 이름
Producer 메시지를 생산하여 Broker의 Topic으로 전송하는 애플리케이션
Consumer Broker의 Topic에 저장된 메시지를 가져가는 애플리케이션
Partition Topic을 분할하여 병렬 처리를 가능하게 하는 단위
KRaft Kafka 3.3+에서 ZooKeeper를 대체하는 자체 메타데이터 관리 모드


> pick-me는 Confluent Kafka 7.6.0을 KRaft 모드로 운영한다. ZooKeeper 없이 Kafka 자체적으로 클러스터 메타데이터를 관리하며, 운영 복잡도를 줄이고 부트스트랩 시간을 단축한다.

### pick-me 토픽 구조

pick-me는 도메인별로 토픽을 분리하여 8개의 이벤트 토픽과 1개의 Dead Letter Topic을 운영한다.


트래픽이 높은 도메인(주문, 결제, 재고)은 6개 파티션으로 병렬 처리 성능을 확보하고, 트래픽이 상대적으로 낮은 도메인(상품, 회원)은 3개, 파트너/정산은 1개로 설정하여 리소스를 효율적으로 사용한다.

### Kafka의 핵심 특징

멀티 프로듀서, 멀티 컨슈머
Kafka의 중앙 집중형 구조로 인해 하나의 Producer가 여러 Topic에 메시지를 발행할 수 있고, 하나의 Consumer가 여러 Topic을 구독할 수 있다. pick-me의 통지 서비스(`NotificationEventConsumer`)는 5개 토픽(order, payment, member, inventory, settlement)을 동시에 구독하여 알림 관련 이벤트를 통합 처리한다.

디스크 기반 메시지 영속성
Consumer가 메시지를 읽더라도 정해진 보관 주기 동안 디스크에 메시지가 유지된다. 트래픽이 일시적으로 급증하거나 Consumer에 오류가 발생하더라도 메시지 손실 없이 처리가 가능하다.

수평 확장성
하나의 Kafka 클러스터는 일반적으로 3대의 Broker로 시작하며, 수십 대의 Broker로 무중단 확장이 가능하다.


3. Producer — 메시지 전송 구조

### 메시지 전송 과정

Producer가 메시지를 전송하는 과정은 다음과 같다.

1. 애플리케이션에서 비즈니스 로직에 의해 메시지가 생성된다.
2. Producer(Kafka Client)가 메시지를 받아 전송을 준비한다.
3. Partitioner가 메시지의 키 존재 여부에 따라 대상 파티션을 결정한다.
4. 결정된 메시지는 Record Batch에 묶여 배치 단위로 전송된다.
5. 배치가 네트워크를 통해 Kafka Broker의 토픽/파티션에 저장된다.

### pick-me의 Transactional Outbox 패턴

pick-me에서는 도메인 로직과 이벤트 발행의 원자성을 보장하기 위해 Transactional Outbox 패턴을 사용한다. Kafka에 직접 메시지를 전송하는 대신 비즈니스 트랜잭션 안에서 Outbox 테이블에 이벤트를 저장하고 별도 스케줄러가 이를 Kafka로 릴레이 한다.


주문 서비스에서의 이벤트 발행:


Outbox Relay 스케줄러:


이 구조를 통해 비즈니스 트랜잭션이 커밋되면 이벤트 저장도 성공하고 Kafka 브로커 장애 시에도 이벤트가 유실되지 않는다. 릴레이 실패 시 최대 5회 재시도하며, 재시도 초과 이벤트는 Dead Letter Topic으로 이동한다.

### 파티션 분산 저장

메시지 키의 존재 여부에 따라 파티션 배치 전략이 달라진다.

 

조건 전략 결과
키 있음 해시 기반 파티셔닝 동일 키의 메시지는 항상 같은 파티션에 저장된다
키 없음 라운드 로빈 각 파티션에 순차적으로 분배된다


pick-me의 Outbox Relay 스케줄러는 `aggregateId`(주문 ID, 결제 ID 등)를 메시지 키로 사용하여, 동일 주문에 대한 이벤트는 항상 같은 파티션에 저장되도록 한다. 이를 통해 하나의 주문에 대한 이벤트 순서가 보장된다.

동일 파티션 내에서는 메시지 순서가 보장되지만 파티션 간에는 순서가 보장되지 않는다. 이 점은 Kafka를 사용할 때 반드시 고려해야 하는 특성이다.

### Producer 설정

pick-me의 Kafka Producer 설정을 살펴본다.


- `acks=all`: 모든 ISR(In-Sync Replica)에 메시지가 저장된 것을 확인한 후에야 전송 완료로 처리한다.
- `enable.idempotence=true`: 네트워크 장애로 인한 재전송 시에도 Broker가 중복을 제거하여 정확히 한 번만 저장한다.


4. Consumer — 메시지 소비 구조

### Consumer Group

Consumer는 GroupId를 통해 Consumer Group으로 묶을 수 있다. 같은 그룹에 속한 Consumer들은 하나의 Topic의 파티션을 나눠 가져 병렬로 처리한다.


같은 그룹 내에서는 파티션을 나눠서 처리한다. 하나의 파티션은 그룹 내 하나의 Consumer에만 할당된다.

다른 그룹은 독립적으로 모든 메시지를 처리한다. 동일한 Topic을 구독하더라도 각 그룹은 별도의 오프셋을 관리한다.

### pick-me의 Consumer Group 구조

pick-me에서는 각 서비스가 독립적인 Consumer Group으로 토픽을 구독한다.

Consumer Group 구독 토픽 처리 역할
`order-saga-consumer` `pickme.payment.events`, 
`pickme.inventory.events`
Saga 보상 트랜잭션 (결제 성공/실패, 재고 부족 처리)
`order-snapshot-consumer` `pickme.product.events`,
`pickme.member.events`
상품/회원 스냅샷 동기화 (읽기 모델 유지)
`payment-consumer` `pickme.order.events` 주문 생성 시 결제 처리, 환불 요청 처리
`inventory-consumer` `pickme.order.events`,
`pickme.product.events`
재고 예약/확정/취소, 상품 등록 시 재고 초기화
`notification-consumer` 5개 토픽 주문/결제/회원/재고/정산 알림 통합 처리
`settlement-consumer` `pickme.payment.events`,
`pickme.partner.events`
매출 스냅샷 적재, 파트너 정산 처리
`dlt-monitor` `pickme.dead-letter` 실패 이벤트 모니터링 및 Slack 알림


`pickme.order.events` 토픽은 `payment-consumer`, `inventory-consumer`, `notification-consumer` 3개 그룹이 독립적으로 구독한다. 각 그룹은 모든 메시지를 각자 처리하되, 그룹 내에서는 6개 파티션을 나눠 병렬로 소비한다.

Consumer 구현 예시 — OrderSagaConsumer:


### Consumer Lag

토픽에 메시지가 쌓이는 속도를 Consumer가 따라가지 못하는 경우, 밀리는 메시지의 수를 Consumer Lag이라고 한다.

Lag이 증가하면 메시지 처리 지연이 발생하고 장애로 이어질 수 있다. 이때 같은 Consumer Group에 Consumer를 추가하여 처리 속도를 높일 수 있으며, 이 과정을 Rebalancing이라고 한다.

pick-me에서 `pickme.order.events` 토픽의 파티션 수는 6이므로, `inventory-consumer` 그룹에 최대 6개의 Consumer 인스턴스를 배치할 수 있다. 이보다 많은 Consumer를 추가해도 유휴 상태로 대기하게 된다.


5. Offset 관리

### Offset의 종류

Kafka에서 Offset은 파티션 내 메시지의 위치를 나타내는 순차적인 번호다. 

Consumer는 Offset을 기반으로 어디까지 메시지를 읽었는지 추적한다.

 

Offset 설명
Log Start Offset 파티션에서 가장 오래된 메시지의 위치
Current Offset Consumer가 현재 읽고 있는 위치
Committed Offset Consumer가 처리를 완료하고 커밋한 위치
High Water Mark 파티션에 마지막으로 저장된 메시지의 위치


Consumer Lag은 다음과 같이 계산된다.

> Consumer Lag = High Water Mark - Current Offset

Lag이 0이면 실시간 처리 상태(이상적), 1,000 미만이면 양호한 상태다. Lag이 지속적으로 증가하면 Consumer 증설 또는 처리 로직 최적화가 필요하다.

### Offset 리셋 전략

Consumer가 처음 시작하거나 기존 Offset 정보가 없을 때 어디서부터 메시지를 읽을지 결정하는 전략이다.


전략 동작 사용 시점
none 오프셋이 없으면 예외를 발생시킨다 격한 오프셋 관리가 필요한 경우
latest 가장 최신 메시지부터 읽는다 실시간 데이터 처리 시 (가장 많이 사용)
earliest 가장 오래된 메시지부터 읽는다 전체 데이터 재처리가 필요한 경우


pick-me에서는 `auto-offset-reset: earliest`를 사용한다. 새로운 Consumer Group이 추가되거나, 서비스가 재배포될 때 기존에 처리하지 못한 메시지를 놓치지 않기 위함이다. 멱등성 필터(`IdempotencyFilter`)가 중복 처리를 방지하므로 이미 처리된 메시지를 다시 읽더라도 안전하다.

### Offset 커밋 전략

Consumer가 메시지를 어디까지 처리했는지 Kafka에 기록하는 방식이다.

 

전략 동작 장점 단점
자동 커밋 일정 시간마다 자동으로 오프셋을 커밋 간단하고 편리 메시지 손실 및 중복 처리 위험
수동 커밋 로직 처리 완료 후 명시적으로 커밋 데이터 안전성이 보장 구현 복잡도가 증가
배치 커밋 여러 메시지를 모아 한 번에 커밋 커밋 횟수 감소로 성능이 향상 일부 실패 시 재처리가 필요


pick-me에서는 수동 커밋을 사용한다.


Consumer가 비즈니스 로직을 완료한 뒤 명시적으로 `ack.acknowledge()`를 호출해야 오프셋이 커밋된다. 처리 중 예외가 발생하면 오프셋이 커밋되지 않아 메시지가 재전달되며, 이는 At-Least-Once 전달 보장의 기반이 된다.


6. 메시지 전달 보장 전략

분산 시스템에서 메시지가 정확히 처리되는 것을 보장하는 일은 중요하다. Kafka는 세 가지 전달 보장 수준을 제공한다.

### 중복 메시지 문제

먼저, 멱등성이 보장되지 않을 때 발생하는 문제를 살펴본다.


pick-me의 경우, 동일한 주문 메시지가 중복 처리되면 이중 결제, 재고 이중 차감, 중복 알림 발송 등 심각한 비즈니스 문제가 발생한다. 따라서 메시지 전달 보장 전략을 비즈니스 요구사항에 맞게 선택해야 한다.

### At Most Once (최대 한 번)


- Producer: `acks=0`으로 설정하여 응답 대기 없이 즉시 전송을 완료한다.
- Consumer: 메시지를 받자마자 오프셋을 커밋한 후 처리를 시작한다.
- 결과: 처리 중 장애가 발생하면 이미 커밋된 메시지는 재전달되지 않는다.

> 메시지 손실이 발생할 수 있지만, 중복 처리는 없다.

### At Least Once (최소 한 번)


- Producer: `acks=all`로 설정하여 모든 복제본에 저장된 것을 확인한 후 전송 완료 응답을 받는다.
- Consumer: 비즈니스 로직 처리가 완료된 후에 오프셋을 커밋한다.
- 결과: 처리 실패 시 오프셋이 커밋되지 않아 동일 메시지가 재전달된다.

> 메시지 손실은 없지만 중복 처리가 발생할 수 있다.

### Exactly Once (정확히 한 번)


- Producer: 멱등성을 활성화(`enable.idempotence=true`)하고 트랜잭션 단위로 메시지를 전송한다. Broker가 중복을 제거하고 순서를 보장한다.
- Consumer: 트랜잭션을 시작하고, 메시지 처리 + 데이터 저장 + 오프셋 커밋을 하나의 트랜잭션으로 묶는다.
- 결과: 처리 실패 시 트랜잭션 전체가 롤백되어 오프셋도 원복 된다.

> 메시지 손실도 없고 중복도 없는 안전성을 제공한다.

### 전략별 비교

전략 메시지 손실 중복 처리 성능 구현 복잡도
At Most Once 가능 없음 높음 낮음
At Least Once 없음 가능 중간 중간
Exactly Once 없음 없음 낮음 높음


### pick-me의 선택: At Least Once + 멱등성 = 실질적 Exactly Once

pick-me에서는 Kafka의 트랜잭션 기반 Exactly Once 대신, At Least Once 전달 + 애플리케이션 레벨 멱등성을 조합하여 실질적인 Exactly Once를 구현한다.

이 방식을 선택한 이유는 Kafka 트랜잭션의 성능 오버헤드를 피하면서도,비즈니스 로직에서 중복을 확실히 차단할 수 있기 때문이다.

 


모든 Consumer의 EventHandler는 이 패턴을 따른다:
1. 멱등성 확인 — `isDuplicate(eventId)` 체크
2. 비즈니스 로직 처리 — 도메인 모델 상태 변경
3. 처리 완료 기록 — `markProcessed(eventId, eventType)`
4. 수동 오프셋 커밋 — `ack.acknowledge()`

### Dead Letter Topic을 통한 실패 메시지 관리

처리가 반복적으로 실패하는 메시지는 Dead Letter Topic(`pickme.dead-letter`)으로 이동한다.


DLT에 적재된 이벤트는 Admin API(`POST /api/v1/admin/dlt/{eventId}/retry`)를 통해 수동으로 재처리할 수 있다. 이를 통해 일시적 장애로 인한 이벤트 유실 없이 완전한 데이터 정합성을 유지한다.


7. Kafka vs 다른 메시지 플랫폼

Kafka가 항상 최선의 선택은 아니다. 메시지 consume 순서는 단일 파티션이 아닌 경우 보장되지 않기 때문에, 대기열 개념의 순서 보장이 필요하다면 다른 플랫폼이 더 적합할 수 있다.

요구사항 Kafka RabbitMQ / SQS
높은 처리량 및 분산 처리 적합 -
메시지 영속성 - -
수평 확장(Scale-Out) 적합 -
메시지 순서 보장 - 적합
간단한 큐잉 처리 - 적합


하나의 파티션만 사용하면 순서 보장이 가능하지만, 분산 처리의 이점이 사라진다. 이 경우 Kafka를 사용하는 목적 자체가 희석되므로 다른 플랫폼을 고려해야 한다.

pick-me가 Kafka를 선택한 이유는 다음과 같다:
- 8개 도메인 서비스 간의 이벤트 라우팅이 필요하고, 하나의 이벤트를 여러 Consumer Group이 독립적으로 구독하는 멀티 컨슈머 패턴이 핵심이다.
- `OrderPlacedEvent` 하나로 결제, 재고, 통지 서비스가 각자 처리하는 구조는 Kafka의 토픽 기반 Pub-Sub이 가장 적합하다.
- RabbitMQ의 메시지 큐 방식은 하나의 메시지가 하나의 Consumer에만 전달되므로 이 패턴을 구현하려면 Exchange/Queue를 복잡하게 설정해야 한다.


8. 마무리

Apache Kafka는 대용량/분산 처리에 특화된 이벤트 스트리밍 플랫폼이다.

- EDA를 도입하면 서비스 간 결합도를 낮추고 장애를 격리할 수 있다. pick-me에서는 주문/결제/재고/통지/정산 서비스가 Kafka 이벤트를 통해 느슨하게 결합되어 있다.
- Transactional Outbox 패턴으로 비즈니스 트랜잭션과 이벤트 발행의 원자성을 보장하고, OutboxRelayScheduler가 500ms 주기로 Kafka에 릴레이 한다.
- Consumer Group 단위로 파티션을 나눠 병렬 처리하며, 7개의 Consumer Group이 9개 토픽을 독립적으로 구독한다.
- 수동 커밋 + 멱등성 필터를 조합하여 At-Least-Once 전달 위에서 실질적인 Exactly Once를 구현한다.
- Dead Letter Topic + Slack 알림 + Admin API로 실패 이벤트를 모니터링하고 수동 재처리할 수 있다.

Kafka가 모든 상황에 적합한 것은 아니다. 메시지 순서 보장이 필수적이거나 단순한 큐잉 처리가 목적이라면 RabbitMQ, SQS 등 다른 플랫폼을 함께 검토하는 것이 바람직하다.


출처

https://www.inflearn.com/course/%EC%B9%B4%EC%B9%B4%EC%98%A4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0/dashboard?cid=338641

 

카카오 면접관과 함께하는 워크플로우 기반의 대용량 트래픽 처리 기법| Hong - 인프런 강의

현재 평점 4.9점 수강생 493명인 강의를 만나보세요. 폭증하는 트래픽, 어떻게 견딜 것인가? Kafka, Spring, CDC, Temporal을 활용한 EDA(Event-Driven Architecture) 기반의 실전 설계 패턴을 통해, 장애에 강하고

www.inflearn.com

https://techblog.gccompany.co.kr/apache-kafka%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-eda-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-bf263c79efd0

 

Apache Kafka를 사용하여 EDA 적용하기

안녕하세요, 여기어때의 결제정산개발팀에서 예약 개발 업무를 맡고 있는 paori 입니다.

techblog.gccompany.co.kr