Kafka 기초 (1) - What is Kafka: Event Log와 구성요소
producer와 consumer가 정확히 무엇인지 주문 이벤트 하나의 여정으로 따라가고, 읽어도 지우지 않는 event log의 성질과 구성요소 일곱 개, 실사용 규칙 세 개를 그림으로 정리합니다.
Kafka 기초 시리즈의 1편입니다. 이번 편은 개념 정리이고 실습은 2편부터 시작합니다. 전체 목차는 0편에 있습니다.
Kafka가 푸는 문제
주문을 받는 서비스를 생각해 보겠습니다. 주문이 발생하면 할 일이 하나둘 늘어납니다. DB에 적재해야 하고, 통계도 집계해야 하고, 알림도 보내야 합니다. Kafka 없이 만들면 주문 API 서버가 그 셋을 전부 직접 호출하게 됩니다.
flowchart LR
A["주문 API 서버"] --> C["DB 적재"]
A --> D["통계 집계"]
A --> E["알림 발송"]
B["결제 서버"] --> C
B --> D
B --> E
이 구조는 만드는 쪽 N개와 쓰는 쪽 M개가 N×M으로 얽힙니다. 문제는 두 가지입니다.
- 추가할 때마다 기존 코드를 고칩니다. 쓰는 쪽을 하나 늘리려면 만드는 쪽 서버들의 코드를 전부 수정해야 합니다
- 한쪽의 사정이 다른 쪽에 번집니다. 알림 발송이 느려지면 그것을 호출해 주는 주문 API의 응답도 함께 느려지고, 알림 서버가 죽어 있는 동안의 주문은 알림이 유실됩니다
Kafka는 두 쪽 사이에 이벤트 log를 둡니다.
flowchart LR
A["주문 API 서버"] --> K
B["결제 서버"] --> K
K[("Kafka<br/>event log")] --> C["DB 적재"]
K --> D["통계 집계"]
K --> E["알림 발송"]
만드는 쪽은 Kafka에 쓰기만 하고, 쓰는 쪽은 Kafka에서 각자의 속도로 읽습니다. 연결이 N+M개로 줄고, 소비자를 추가해도 생산자 코드는 그대로이며, 알림이 느려져도 주문 API는 영향받지 않습니다.
Producer와 Consumer는 정확히 무엇인가
위 그림에서 만드는 쪽을 producer, 쓰는 쪽을 consumer라고 부릅니다. 여기서 정확히 잡아야 할 것이 있습니다. 둘 다 별도의 서버가 아니라, 여러분의 애플리케이션에 들어가는 client 코드입니다. 서버는 가운데의 Kafka(broker) 하나뿐입니다.
producer는 이벤트를 보내는 쪽 앱 안의 전송 코드입니다. 주문 API 서버라면, 주문을 처리하던 기존 코드 끝에 “주문이 발생했다”는 사실을 Kafka로 보내는 호출이 한 줄 추가되는 형태입니다.
1
2
3
4
5
6
7
# 주문 API 서버의 처리 로직 (producer는 이 안의 마지막 줄이다)
order = create_order(request) # 기존 주문 처리
producer.produce(
"orders", # topic 이름: 이벤트의 분류
key=order.id,
value='{"id": "order-42", "amount": 5000}',
)
consumer는 이벤트를 받아 처리하는 별도 프로세스입니다. 계속 떠서 루프를 돌며, 새 이벤트가 있으면 가져와서(poll) 자기 일을 합니다. DB 적재 서비스라면 INSERT가, 알림 서비스라면 push 발송이 “자기 일”입니다.
1
2
3
4
5
# DB 적재 서비스 (consumer 프로세스: 계속 돌고 있다)
while True:
event = consumer.poll() # 새 이벤트가 있으면 하나 가져온다
if event:
insert_into_db(event) # 이 서비스의 일
둘의 관계에서 중요한 성질이 나옵니다. producer는 누가 읽는지 모릅니다. orders라는 topic 이름에 넣을 뿐입니다. consumer도 누가 보냈는지 알 필요가 없습니다. orders topic을 구독할 뿐입니다. 두 쪽이 공유하는 것은 topic 이름 하나뿐이고, 이것이 앞에서 말한 “분리”의 실체입니다. 실제로 돌아가는 전체 코드는 5편에서 작성합니다.
이벤트 하나의 여정
5000원짜리 주문 한 건이 흘러가는 과정을 처음부터 끝까지 따라가면 나머지 구성요소가 전부 등장합니다.
- 주문 API 서버의 producer가 이벤트를 만듭니다 — key는 주문 ID
order-42, value는 주문 내용 JSON - producer가 orders topic으로 전송합니다. topic은 이벤트의 분류 단위로, 주문은 orders에, 클릭 로그는 click-events에 쌓는 식입니다
- broker는 topic을 이루는 partition 여러 개 중 key로 정해지는 하나를 골라, 그 끝에 이벤트를 추가하고 디스크에 저장합니다. partition 안에서의 위치 번호가 offset입니다
- DB 적재 서비스의 consumer가 poll로 이 이벤트를 받아 INSERT를 실행하고, “offset 7까지 읽었다”를 기록해 둡니다
- 알림 서비스의 consumer도 같은 이벤트를 독립적으로 받아 push를 보냅니다. 적재 서비스가 읽었다고 이벤트가 사라지지 않기 때문입니다
- 이벤트는 그 뒤로도 partition에 남아 있다가, 보존 기간(retention, 기본 7일)이 지나면 삭제됩니다
flowchart LR
subgraph APP["주문 API 서버"]
P["producer"]
end
subgraph BROKER["broker (Kafka 서버)"]
subgraph T["topic: orders"]
P0["partition 0"]
P1["partition 1 ...·6·7 ← 추가"]
P2["partition 2"]
end
end
subgraph S1["DB 적재 서비스"]
A["consumer → INSERT"]
end
subgraph S2["알림 서비스"]
B["consumer → push 발송"]
end
P -->|"key: order-42"| P1
P1 --> A
P1 --> B
읽어도 지우지 않는다: 메시지 큐와의 차이
위 여정의 5번이 Kafka를 일반적인 메시지 큐와 가르는 지점입니다. 큐는 메시지를 꺼내면 지웁니다. 소비자가 한 종류라는 전제가 구조에 박혀 있는 것입니다. Kafka는 이벤트를 추가 전용 log에 쌓고, 읽어도 지우지 않습니다. 지우는 기준은 “읽었는가”가 아니라 “오래됐는가”(retention)입니다.
그러면 어디까지 읽었는지는 누가 아는가 — broker가 아니라 소비자 각자가 자기 offset으로 관리합니다. partition 하나를 들여다보면 이런 상태가 됩니다.
1
2
3
4
5
6
partition 1 (orders topic)
offset: 0 1 2 3 4 5 6 7
[ev0] [ev1] [ev2] [ev3] [ev4] [ev5] [ev6] [ev7]
▲ ▲
알림 서비스는 여기까지 읽음 적재 서비스는 여기까지 읽음
같은 log를 두 서비스가 서로 다른 위치에서 읽고 있고, 서로 아무 영향을 주지 않습니다. 이 구조에서 세 가지가 공짜로 나옵니다.
- 소비자 장애가 유실이 아닙니다. 알림 서비스가 하루 죽어 있어도 이벤트는 log에 그대로 있고, 복구되면 offset 4부터 이어 읽으면 됩니다
- 새 소비자를 언제든 붙일 수 있습니다. 다음 달에 실시간 피처 계산 서비스를 추가하면 offset 0부터 과거 이벤트를 전부 다시 읽을 수 있습니다
- 재처리가 가능합니다. 적재 로직에 버그가 있었다면 offset을 되감아 다시 읽습니다 (4편에서 실습)
구성요소 일곱 개
여기까지 등장한 것을 표로 모으면 Kafka의 구성요소 전부입니다.
| 구성요소 | 역할 |
|---|---|
| broker | Kafka 서버 프로세스. 여러 대가 cluster를 이룬다 |
| topic | 이벤트의 분류 단위. orders, click-events처럼 이름을 붙인다 |
| partition | topic을 나눈 추가 전용 log. 병렬 처리와 순서 보장의 단위 |
| producer | topic에 이벤트를 쓰는 client. 보내는 쪽 앱 안의 전송 코드 |
| consumer | topic에서 이벤트를 읽는 client. 받아 처리하는 별도 프로세스 |
| consumer group | consumer의 묶음. group 안에서 partition을 나눠 맡는다 |
| offset | partition 안의 위치 번호. consumer가 어디까지 읽었는지의 기준 |
topic이 왜 통째로 하나의 log가 아니라 partition으로 쪼개져 있는지만 보충하면 됩니다. 답은 병렬화입니다. log가 하나면 읽는 쪽도 하나씩만 처리할 수 있지만, partition이 3개면 consumer 3개가 동시에 읽을 수 있고, partition을 서로 다른 broker에 나눠 둘 수 있어 쓰기 부하도 분산됩니다.
실사용을 지배하는 규칙 세 개
마지막 남은 구성요소가 consumer group입니다. 위에서 적재 서비스와 알림 서비스가 각자 이벤트를 전부 받았는데, 적재 서비스 혼자 처리가 밀리면 어떻게 할까요. 적재 consumer를 여러 개 띄우고 같은 group으로 묶으면, group 안에서 partition을 나눠 맡습니다.
flowchart LR
subgraph T["topic: orders (partition 3개)"]
P0["partition 0"]
P1["partition 1"]
P2["partition 2"]
end
subgraph GA["적재 group (consumer 2개)"]
A1["consumer A"]
A2["consumer B"]
end
subgraph GB["알림 group (consumer 1개)"]
B1["consumer C"]
end
P0 --> A1
P1 --> A1
P2 --> A2
P0 --> B1
P1 --> B1
P2 --> B1
적재 group 안에서는 consumer A가 partition 0과 1을, B가 2를 맡아 처리가 분산됩니다. group이 다른 알림 서비스는 여전히 전체를 혼자 다 읽습니다. 이 그림에서 규칙 세 개가 나오고, 실무에서 마주치는 문제 대부분이 이 셋으로 설명됩니다.
- 순서는 partition 안에서만 보장됩니다. topic 전체의 순서는 보장되지 않습니다. 순서가 필요한 이벤트(같은 주문, 같은 사용자)는 같은 key를 줘서 같은 partition으로 보냅니다 — 3편에서 직접 확인합니다
- 같은 group의 consumer들은 partition을 나눠 맡습니다. consumer를 늘리면 처리가 분산되고, 하나가 죽으면 그 partition을 다른 consumer가 이어받습니다. 병렬성의 상한이 partition 수라는 함정도 여기서 나옵니다 — 4편에서 확인합니다
- 다른 group은 각자 전체를 읽습니다. 적재용 group과 알림용 group이 같은 topic을 독립적으로 소비하는 위 그림의 구성이 이것으로 가능합니다
언제 쓰고 언제 안 쓰나
Kafka가 들어오는 기준은 “지금 일어난 이벤트에 초 단위로 반응해야 하는가”입니다. 로그·클릭스트림 수집, DB 변경 이력을 실어 나르는 CDC, 온라인 서빙의 실시간 피처 계산(머신러닝 실전 워크플로 9편)이 이 영역입니다.
반대로 하루 한 번 집계로 충분한 파이프라인이라면 Kafka 없이 Airflow 같은 배치 스케줄러가 단순합니다. broker라는 상시 서버를 하나 더 운영하는 비용은 공짜가 아니기 때문입니다.
다음 편에서 broker를 Docker로 직접 띄우고, 이번 편의 구성요소들을 터미널에서 눈으로 확인합니다.