포스트

Kafka 기초 (1) - What is Kafka: Event Log와 구성요소

producer와 consumer가 정확히 무엇인지 주문 이벤트 하나의 여정으로 따라가고, 읽어도 지우지 않는 event log의 성질과 구성요소 일곱 개, 실사용 규칙 세 개를 그림으로 정리합니다.

Kafka 기초 (1) - What is Kafka: 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원짜리 주문 한 건이 흘러가는 과정을 처음부터 끝까지 따라가면 나머지 구성요소가 전부 등장합니다.

  1. 주문 API 서버의 producer가 이벤트를 만듭니다 — key는 주문 ID order-42, value는 주문 내용 JSON
  2. producer가 orders topic으로 전송합니다. topic은 이벤트의 분류 단위로, 주문은 orders에, 클릭 로그는 click-events에 쌓는 식입니다
  3. broker는 topic을 이루는 partition 여러 개 중 key로 정해지는 하나를 골라, 그 끝에 이벤트를 추가하고 디스크에 저장합니다. partition 안에서의 위치 번호가 offset입니다
  4. DB 적재 서비스의 consumer가 poll로 이 이벤트를 받아 INSERT를 실행하고, “offset 7까지 읽었다”를 기록해 둡니다
  5. 알림 서비스의 consumer도 같은 이벤트를 독립적으로 받아 push를 보냅니다. 적재 서비스가 읽었다고 이벤트가 사라지지 않기 때문입니다
  6. 이벤트는 그 뒤로도 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의 구성요소 전부입니다.

구성요소역할
brokerKafka 서버 프로세스. 여러 대가 cluster를 이룬다
topic이벤트의 분류 단위. orders, click-events처럼 이름을 붙인다
partitiontopic을 나눈 추가 전용 log. 병렬 처리와 순서 보장의 단위
producertopic에 이벤트를 쓰는 client. 보내는 쪽 앱 안의 전송 코드
consumertopic에서 이벤트를 읽는 client. 받아 처리하는 별도 프로세스
consumer groupconsumer의 묶음. group 안에서 partition을 나눠 맡는다
offsetpartition 안의 위치 번호. 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이 다른 알림 서비스는 여전히 전체를 혼자 다 읽습니다. 이 그림에서 규칙 세 개가 나오고, 실무에서 마주치는 문제 대부분이 이 셋으로 설명됩니다.

  1. 순서는 partition 안에서만 보장됩니다. topic 전체의 순서는 보장되지 않습니다. 순서가 필요한 이벤트(같은 주문, 같은 사용자)는 같은 key를 줘서 같은 partition으로 보냅니다 — 3편에서 직접 확인합니다
  2. 같은 group의 consumer들은 partition을 나눠 맡습니다. consumer를 늘리면 처리가 분산되고, 하나가 죽으면 그 partition을 다른 consumer가 이어받습니다. 병렬성의 상한이 partition 수라는 함정도 여기서 나옵니다 — 4편에서 확인합니다
  3. 다른 group은 각자 전체를 읽습니다. 적재용 group과 알림용 group이 같은 topic을 독립적으로 소비하는 위 그림의 구성이 이것으로 가능합니다

언제 쓰고 언제 안 쓰나

Kafka가 들어오는 기준은 “지금 일어난 이벤트에 초 단위로 반응해야 하는가”입니다. 로그·클릭스트림 수집, DB 변경 이력을 실어 나르는 CDC, 온라인 서빙의 실시간 피처 계산(머신러닝 실전 워크플로 9편)이 이 영역입니다.

반대로 하루 한 번 집계로 충분한 파이프라인이라면 Kafka 없이 Airflow 같은 배치 스케줄러가 단순합니다. broker라는 상시 서버를 하나 더 운영하는 비용은 공짜가 아니기 때문입니다.

다음 편에서 broker를 Docker로 직접 띄우고, 이번 편의 구성요소들을 터미널에서 눈으로 확인합니다.

다음 글: Kafka 기초 (2) - Installation and First Run

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.