Kafka 기본 개념 - 토픽, 파티션, 컨슈머 그룹

study·18 min read·2026-08-28
목차

한 줄 요약

Kafka는 메시지를 “토픽”이라는 로그에 순서대로 쌓아두고, 여러 소비자가 각자의 속도로 읽어가게 해주는 분산 이벤트 스트리밍 플랫폼이다.

왜 (배경/문제 상황)

서비스가 커지면 한 서비스에서 일어난 이벤트를 여러 다른 서비스가 각자 비동기로 알아야 하는 경우가 흔해진다 — 주문이 생성되면 재고 차감, 알림 발송, 로그 적재가 동시에 필요한 식이다. 단순 메시지 큐는 메시지 하나를 누가 가져가면 사라지는 구조라, 같은 이벤트를 여러 소비자가 각자 처리하게 만들기 어렵고 장애 시 재처리도 까다롭다. Kafka는 메시지를 소비해도 바로 지우지 않는 “로그” 구조로 이 문제를 해결한다.

본문

핵심 개념 한눈에

개념설명
Topic메시지를 종류별로 구분하는 채널 (예: order-created)
Partition토픽을 나눈 물리적 로그 단위. 병렬 처리와 순서 보장의 단위
Producer토픽에 메시지를 쓰는 주체
Consumer토픽에서 메시지를 읽는 주체
Consumer Group여러 컨슈머가 파티션을 나눠 가지며 협업 소비하는 단위
Offset파티션 안에서 각 메시지의 위치(순번)
BrokerKafka 서버 인스턴스. 여러 개가 모여 클러스터를 이룸

토픽과 파티션

토픽은 “메시지 종류”를 구분하는 논리적 단위이고, 파티션은 그 토픽을 물리적으로 쪼갠 로그다.

Topic: order-created
├── Partition 0: [msg1, msg4, msg7, ...]
├── Partition 1: [msg2, msg5, msg8, ...]
└── Partition 2: [msg3, msg6, msg9, ...]

같은 파티션 안에서는 메시지 순서가 보장되지만, 파티션이 여러 개면 파티션 간 순서는 보장되지 않는다. 메시지를 어느 파티션에 보낼지는 보통 hash(key) % 파티션 수로 정하는데, 파티션 수와 파티셔너 설정이 그대로 유지되는 동안에는 같은 key가 항상 같은 파티션으로 가기 때문에 “그 key에 대해서는” 순서가 보장된다 (예: 같은 주문 ID의 이벤트들은 파티션 수가 안 바뀌는 한 항상 같은 파티션에 순서대로 쌓인다). 반대로 파티션 수를 늘리면 % 파티션 수 계산 결과가 달라져서, 같은 key라도 그 시점 이후의 메시지는 다른 파티션으로 갈 수 있다.

파티션 수를 늘리면 병렬로 처리할 수 있는 컨슈머 수도 늘어나지만, 한번 늘린 파티션 수는 줄이기 어렵고 key 기반 순서 보장 범위도 달라지므로 신중히 정해야 한다.

컨슈머 그룹 — 왜 이렇게 설계됐나

컨슈머 그룹은 “같은 그룹 안의 컨슈머들이 파티션을 나눠 갖는다”는 규칙 하나로 두 가지 문제를 동시에 푼다.

  • 병렬 처리: 파티션이 3개, 컨슈머가 3개면 각자 파티션 하나씩 맡아 동시에 처리한다.
  • 다중 소비자: 그룹을 다르게 두면, 같은 토픽을 완전히 독립적으로 여러 그룹이 각자 소비할 수 있다 (예: “재고 차감” 그룹과 “알림 발송” 그룹이 같은 order-created 토픽을 각자 소비). 다만 새 그룹이라고 무조건 맨 처음부터 읽는 건 아니다 — 커밋된 오프셋이 없을 때 어디서부터 읽을지는 auto.offset.reset 설정(earliest면 보존된 가장 오래된 메시지부터, latest면 그 시점 이후 새 메시지부터)에 따라 달라진다.

직접 살펴보기 — 컨슈머 수에 따라 파티션이 어떻게 배분되나

파티션 3개짜리 토픽에 같은 그룹의 컨슈머 수를 바꿔가며, 파티션이 어떻게 나뉘는지 비교한다. 아래 데모는 이해를 돕기 위해 라운드로빈(RoundRobinAssignor) 방식으로 단순화한 것이고, 실제 배분 결과는 partition.assignment.strategy 설정에 따라 달라진다 — 예를 들어 기본값인 RangeAssignor는 컨슈머가 2개일 때 라운드로빈과 다른 결과를 낼 수 있다.

컨슈머 수가 파티션 수보다 많아지면 남는 컨슈머는 놀게 된다 — 그래서 파티션 수가 “이 토픽을 최대 몇 개까지 병렬로 처리할 수 있는가”의 상한이 된다.

오프셋과 재처리

Kafka는 메시지를 소비해도 큐처럼 바로 지우지 않고, 설정된 보존 기간(retention) 동안 로그에 남겨둔다. 컨슈머는 “내가 어디까지 읽었는지”를 오프셋으로 따로 기록(commit)하기 때문에, 오프셋을 되돌리면 이미 처리한 메시지도 다시 읽을 수 있다 — 단, 그 메시지가 아직 로그에 남아있는 경우에만 가능하다. cleanup.policy=delete(기본값)에서는 retention.ms/retention.bytes 한도를 넘긴 오래된 레코드가 삭제되고, compact가 포함된 정책이면 같은 key의 이전 레코드가 정리될 수 있다. 재처리는 “오프셋을 되돌리면 항상 가능”한 게 아니라 “대상 레코드가 아직 보존되어 있어야” 가능한 것이다.

예제

주문 이벤트를 서로 다른 그룹의 컨슈머 두 개가 각자 소비하는 흐름을 pseudo-code로 보면 이렇다.

# Producer: 주문이 생성되면 이벤트 발행 (key로 파티션 결정)
producer.send('order-created', key=order.user_id, value=order.to_json())

# Consumer A (재고 서비스, group='inventory-service')
for message in consumer.poll('order-created', group='inventory-service'):
    reduce_stock(message.value)
    consumer.commit(message.offset + 1)  # 커밋 오프셋은 "다음에 읽을 위치" — 현재 레코드 offset을 그대로 쓰면 재시작 후 같은 메시지를 다시 읽는다

# Consumer B (알림 서비스, group='notification-service') — 별도 그룹이라 자기만의 오프셋으로 독립적으로 소비
for message in consumer.poll('order-created', group='notification-service'):
    send_notification(message.value)
    consumer.commit(message.offset + 1)

주의사항

  • 파티션 수는 나중에 늘릴 수는 있지만 줄이기는 사실상 어렵다(다시 만들어야 함). 처음부터 예상 트래픽과 컨슈머 확장 계획을 고려해서 정하는 편이 좋다.
  • 파티션 키를 잘못 고르면(예: 특정 key에 트래픽이 몰림) 특정 파티션에만 부하가 쏠리는 핫 파티션 문제가 생길 수 있다.
  • 오프셋을 언제 commit하느냐(메시지 처리 전/후)에 따라 “최소 한 번 처리(at-least-once)“와 “최대 한 번 처리(at-most-once)“가 갈린다. 처리 후 commit하면 중복 처리 가능성은 남아도 메시지 유실은 막을 수 있다.

참고자료