데이터 엔지니어를 위한 Apache Kafka: 스트리밍, 파티션, 면접 질문

데이터 엔지니어를 위한 Apache Kafka 심층 분석. Kafka 4.x와 KRaft를 활용한 스트리밍 아키텍처, 파티션 전략, 컨슈머 그룹, 기술 면접 빈출 질문을 실전 코드 예제와 함께 설명합니다.

Apache Kafka 스트리밍 아키텍처와 파티션 데이터 흐름 다이어그램

Apache Kafka는 현대 데이터 엔지니어링 스택의 핵심에 자리하며, 모든 규모의 조직에서 매일 수조 건의 이벤트를 처리하고 있습니다. Kafka 4.x부터는 KRaft 기반의 완전 자율 운영이 구현되어 ZooKeeper 의존성이 완전히 제거되었습니다. 리얼타임 데이터 파이프라인의 업계 표준으로서 동일한 안정성을 유지하면서도, 운영 복잡성이 크게 감소했습니다.

Kafka 4.x: KRaft 전용 모드

Apache Kafka 4.0부터 ZooKeeper가 더 이상 필요하지 않습니다. KRaft(Kafka Raft)가 모든 메타데이터 관리를 네이티브로 처리하여 운영 복잡성을 줄이고, 프로덕션 배포에서 인프라 구성 요소를 하나 완전히 제거했습니다.

데이터 파이프라인을 위한 Kafka 스트리밍 아키텍처

Kafka는 분산 커밋 로그로 동작합니다. 프로듀서가 토픽 파티션의 끝에 레코드를 추가하고, 컨슈머가 해당 레코드를 순서대로 읽습니다. 이 추가 전용(append-only) 설계 덕분에 리얼타임 스트리밍과 임의 오프셋에서의 배치 리플레이가 모두 가능하며, 두 가지 패턴을 모두 필요로 하는 데이터 엔지니어링 워크로드에 최적의 기반을 제공합니다.

Kafka 클러스터는 브로커(서버), 토픽(논리적 채널), 파티션(토픽의 물리적 샤드)으로 구성됩니다. 각 파티션은 순서가 지정된 불변의 레코드 시퀀스입니다. 하나의 브로커가 파티션 리더 역할을 하며 모든 읽기와 쓰기를 처리합니다. 팔로워 레플리카는 내결함성을 위해 사본을 유지합니다.

핵심 아키텍처 속성은 다음과 같습니다: 순서 보장은 파티션 내에서만 유효하며, 파티션 간에는 보장되지 않습니다. 이 단일 제약 조건이 데이터 파이프라인에서 대부분의 Kafka 설계 결정을 좌우합니다.

yaml
# docker-compose.yml — Kafka 4.x KRaft cluster (no ZooKeeper)
services:
  kafka-1:
    image: apache/kafka:4.2.0
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LOG_DIRS: /var/lib/kafka/data
      KAFKA_NUM_PARTITIONS: 6
      KAFKA_DEFAULT_REPLICATION_FACTOR: 3
      KAFKA_MIN_INSYNC_REPLICAS: 2
    ports:
      - "9092:9092"

이 구성은 컨트롤러 역할도 겸하는 KRaft 모드 브로커를 시작합니다. KAFKA_CONTROLLER_QUORUM_VOTERS 설정은 이전에 ZooKeeper가 관리하던 리더 선출, 토픽 메타데이터, 클러스터 멤버십 역할을 대체합니다.

파이프라인 성능을 결정하는 파티션 전략

파티션은 병렬 처리 수준, 순서 보장, 처리량을 결정합니다. 적절한 파티션 수와 키 전략의 선택이 파이프라인의 안정성과 컨슈머 확장성에 직접적인 영향을 미칩니다.

파티션 수 가이드라인:

  • 토픽을 동시에 처리할 것으로 예상되는 컨슈머 수로 시작합니다
  • 각 파티션은 최신 하드웨어에서 약 10MB/s의 쓰기 처리량을 유지할 수 있습니다
  • 파티션 수가 많아지면 엔드투엔드 지연 시간이 약간 증가합니다(리더 선출과 파일 핸들 증가)
  • Kafka 4.x는 KRaft의 메타데이터 개선 덕분에 브로커당 수천 개의 파티션을 효율적으로 처리합니다

파티션 키 선택에 따라 어떤 레코드가 어떤 파티션에 배치될지 결정됩니다. 같은 키를 가진 레코드는 항상 같은 파티션으로 전송되어, 해당 키에 대한 순서가 보존됩니다.

python
# partition_strategy.py — Choosing the right partition key
from confluent_kafka import Producer
import json

def create_producer():
    return Producer({
        'bootstrap.servers': 'kafka-1:9092',
        'acks': 'all',
        'enable.idempotence': True,
        'max.in.flight.requests.per.connection': 5
    })

def publish_order_event(producer, event):
    # Key by customer_id: all events for one customer stay ordered
    key = str(event['customer_id']).encode('utf-8')
    value = json.dumps(event).encode('utf-8')
    producer.produce(
        topic='order-events',
        key=key,
        value=value,
        headers=[('source', b'order-service'), ('version', b'2')]
    )
    producer.flush()

def publish_clickstream(producer, event):
    # Key by session_id: ordering within a session matters
    # NOT by user_id — one user may have multiple sessions
    key = str(event['session_id']).encode('utf-8')
    value = json.dumps(event).encode('utf-8')
    producer.produce(
        topic='clickstream',
        key=key,
        value=value
    )

customer_idsession_id 중 무엇을 파티션 키로 사용할지는 서로 다른 순서 요구사항을 반영합니다. 주문 이벤트는 트랜잭션 일관성을 유지하기 위해 고객 단위의 순서가 필요합니다. 클릭스트림 이벤트는 사용자 여정을 정확하게 재구성하기 위해 세션 단위의 순서가 필요합니다.

핫 파티션 위험

하나의 키가 다른 키보다 압도적으로 많은 레코드를 생성하는 경우(예: 단일 엔터프라이즈 고객이 이벤트의 80%를 생성하는 상황), 해당 파티션이 병목 지점이 됩니다. 파티션 랙 메트릭을 모니터링하고, 편향된 워크로드에는 복합 키나 커스텀 파티셔너 적용을 검토해야 합니다.

컨슈머 그룹과 오프셋 관리

컨슈머 그룹을 통해 토픽의 병렬 처리가 가능해집니다. Kafka는 그룹 내 각 파티션을 정확히 하나의 컨슈머에 할당하므로, 최대 병렬 처리 수준은 파티션 수와 동일합니다.

오프셋 관리는 Exactly-once(정확히 한 번)와 At-least-once(최소 한 번) 시맨틱을 제어합니다. Kafka는 커밋된 오프셋을 내부 __consumer_offsets 토픽에 저장합니다. 처리 대비 오프셋 커밋의 타이밍이 전달 보장 수준을 결정합니다.

python
# consumer_pipeline.py — Consumer group with manual offset management
from confluent_kafka import Consumer, KafkaError
import json

def create_consumer(group_id: str):
    return Consumer({
        'bootstrap.servers': 'kafka-1:9092',
        'group.id': group_id,
        'auto.offset.reset': 'earliest',
        'enable.auto.commit': False,  # Manual commit for at-least-once
        'max.poll.interval.ms': 300000,
        'session.timeout.ms': 45000,
        'isolation.level': 'read_committed'  # Only read committed transactions
    })

def process_batch(consumer: Consumer, batch_size: int = 500):
    """Process records in micro-batches for throughput."""
    consumer.subscribe(['order-events'])
    buffer = []

    while True:
        msg = consumer.poll(timeout=1.0)
        if msg is None:
            continue
        if msg.error():
            if msg.error().code() == KafkaError._PARTITION_EOF:
                continue
            raise Exception(msg.error())

        record = json.loads(msg.value().decode('utf-8'))
        buffer.append(record)

        if len(buffer) >= batch_size:
            # Process the full batch (write to warehouse, etc.)
            write_to_warehouse(buffer)
            # Commit AFTER successful processing
            consumer.commit(asynchronous=False)
            buffer.clear()

def write_to_warehouse(records: list):
    # Insert batch into data warehouse (BigQuery, Snowflake, etc.)
    pass

enable.auto.commitFalse로 설정하고 write_to_warehouse 성공 후에만 커밋함으로써 At-least-once 전달이 보장됩니다. 컨슈머가 처리 후 커밋 전에 크래시되면, 재시작 시 레코드가 재전달됩니다. 웨어하우스에 데이터를 공급하는 파이프라인에서는 이것이 일반적으로 올바른 트레이드오프입니다. 웨어하우스에 대한 멱등 쓰기가 중복을 처리합니다.

데이터 파이프라인 통합을 위한 Kafka Connect

Kafka Connect는 커스텀 프로듀서/컨슈머 코드를 작성하지 않고도 Kafka와 외부 시스템 간의 데이터 이동을 가능하게 하는 확장 가능한 프레임워크입니다. 소스 커넥터가 Kafka로 데이터를 수집하고, 싱크 커넥터가 데이터를 외부로 전송합니다.

json
{
  "name": "postgres-cdc-source",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres-primary",
    "database.port": "5432",
    "database.user": "replication_user",
    "database.dbname": "production",
    "topic.prefix": "cdc",
    "table.include.list": "public.orders,public.customers,public.products",
    "slot.name": "debezium_slot",
    "plugin.name": "pgoutput",
    "publication.name": "dbz_publication",
    "snapshot.mode": "initial",
    "transforms": "route",
    "transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter",
    "transforms.route.regex": "cdc\\.public\\.(.*)",
    "transforms.route.replacement": "warehouse.$1"
  }
}

이 Debezium CDC 커넥터는 PostgreSQL 테이블에서 발생하는 모든 INSERT, UPDATE, DELETE를 캡처하여 Kafka 이벤트로 발행합니다. RegexRouter 트랜스폼이 토픽 이름을 cdc.public.orders에서 warehouse.orders로 변경하여 파이프라인 네임스페이스를 정리합니다. BigQuery나 Snowflake의 싱크 커넥터와 결합하면, 커스텀 코드 없이 완전 자동화된 CDC 파이프라인을 구축할 수 있습니다.

Data Engineering 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

Kafka 파이프라인의 Exactly-Once 시맨틱

Kafka는 멱등 프로듀서와 트랜잭셔널 API를 통해 Exactly-once 시맨틱(EOS)을 지원합니다. 데이터 엔지니어링 파이프라인에서 EOS는 중복 제거 로직 없이도 다운스트림 시스템의 중복 레코드를 방지합니다.

EOS를 구현하는 3가지 구성 요소:

  1. 멱등 프로듀서: 각 프로듀서는 고유한 Producer ID를 부여받습니다. 브로커는 시퀀스 번호를 사용하여 재시도의 중복을 제거하므로, 네트워크 재시도가 중복을 생성하지 않습니다.
  2. 트랜잭션: 프로듀서는 여러 파티션에 대한 쓰기와 오프셋 커밋을 단일 트랜잭션으로 원자적으로 수행할 수 있습니다. 모든 쓰기가 성공하거나 모두 실패합니다.
  3. read_committed 격리 수준: isolation.level=read_committed로 설정된 컨슈머는 커밋된 트랜잭션의 레코드만 읽습니다.
python
# exactly_once_pipeline.py — Transactional consume-transform-produce
from confluent_kafka import Consumer, Producer
import json

def transactional_pipeline():
    consumer = Consumer({
        'bootstrap.servers': 'kafka-1:9092',
        'group.id': 'etl-transformer',
        'enable.auto.commit': False,
        'isolation.level': 'read_committed'
    })

    producer = Producer({
        'bootstrap.servers': 'kafka-1:9092',
        'transactional.id': 'etl-transformer-001',
        'acks': 'all',
        'enable.idempotence': True
    })

    producer.init_transactions()
    consumer.subscribe(['raw-events'])

    while True:
        msg = consumer.poll(1.0)
        if msg is None or msg.error():
            continue

        # Begin atomic transaction
        producer.begin_transaction()
        try:
            raw = json.loads(msg.value())
            enriched = transform(raw)

            # Write transformed record to output topic
            producer.produce(
                'enriched-events',
                key=msg.key(),
                value=json.dumps(enriched).encode('utf-8')
            )

            # Commit consumer offset within the same transaction
            producer.send_offsets_to_transaction(
                consumer.position(consumer.assignment()),
                consumer.consumer_group_metadata()
            )
            producer.commit_transaction()
        except Exception:
            producer.abort_transaction()

def transform(record: dict) -> dict:
    # Apply business logic transformations
    record['processed_at'] = '2026-04-20T00:00:00Z'
    record['pipeline_version'] = '2.1'
    return record

transactional.id 설정을 통해 브로커는 좀비 프로듀서를 차단할 수 있습니다. 컨슈머가 크래시되고 동일한 transactional.id로 새 인스턴스가 시작되면, 브로커는 이전 인스턴스의 보류 중인 트랜잭션을 중단하여 출력 중복을 방지합니다.

Share Groups: Kafka 4.2의 큐 시맨틱

Kafka 4.2에서는 프로덕션 수준의 Share Groups가 도입되어, Kafka에 전통적인 메시지 큐 시맨틱이 추가되었습니다. 각 파티션이 그룹 내 정확히 하나의 컨슈머에만 할당되는 컨슈머 그룹과 달리, Share Groups에서는 여러 컨슈머가 동일한 파티션의 레코드를 독립적으로 처리할 수 있습니다.

| 특성 | 컨슈머 그룹 | Share Groups | |---|---|---| | 파티션 할당 | 배타적 (파티션당 컨슈머 1개) | 공유 (파티션당 복수 컨슈머) | | 순서 보장 | 파티션 내 순서 보존 | 순서 보장 없음 | | 사용 사례 | 순서 보장 이벤트 스트림 | 작업 분배, 작업 큐 | | 확인 응답 | 오프셋 기반 (위치 커밋) | 레코드 단위 (개별 확인 응답) | | 최대 병렬 처리 | 파티션 수로 제한 | 컨슈머 수로 제한 |

Share Groups는 오랫동안 존재하던 한계, 즉 파티션 수를 초과하는 컨슈머 확장 문제를 해결합니다. 알림 전송이나 배치 작업 분배와 같이 순서가 필요 없는 워크로드에서는 Kafka 외에 별도의 외부 큐 시스템이 필요하지 않게 됩니다.

Share Groups 활용 시점

Share Groups는 작업 분배형 워크로드에 적합합니다: 이메일 발송, 업로드 처리, ML 추론 실행 등. 이벤트 소싱, CDC, 또는 순서 보장 처리가 필요한 파이프라인에서는 컨슈머 그룹이 여전히 올바른 선택입니다.

데이터 엔지니어를 위한 Kafka 면접 질문

데이터 엔지니어링 직무의 기술 면접에서는 Kafka 관련 지식이 자주 평가됩니다. 다음 질문들은 가장 일반적으로 평가되는 개념을 다루고 있습니다.

Q: Kafka는 메시지 순서를 어떻게 보장합니까? 순서는 단일 파티션 내에서만 보장됩니다. 동일한 파티션 키를 가진 모든 레코드는 같은 파티션으로 전송되어 순서대로 추가됩니다. 파티션 간에는 순서가 존재하지 않습니다. 적절한 파티션 키 선택이 Kafka 파이프라인에서 순서를 제어하는 주요 메커니즘입니다.

Q: 그룹 내 컨슈머에 장애가 발생하면 어떻게 됩니까? 그룹 코디네이터가 하트비트 누락을 통해 장애를 감지합니다(session.timeout.ms로 제어). 리밸런스가 트리거되어, 장애가 발생한 컨슈머의 파티션이 나머지 그룹 멤버에게 재할당됩니다. 리밸런스 중에는 소비가 일시적으로 중단됩니다. Kafka 4.x에서 기본값인 협력적 스티키 리밸런싱은 영향받는 파티션만 재할당하여 중단을 최소화합니다.

Q: acks=1acks=all의 차이를 설명해 주십시오. acks=1에서는 리더 레플리카가 레코드를 영구 저장한 후 브로커가 쓰기를 확인 응답합니다. acks=all에서는 모든 In-Sync 레플리카(ISR)가 쓰기를 확인할 때까지 대기합니다. acks=allmin.insync.replicas=2와 결합하면, 단일 브로커 장애 시에도 데이터 손실이 없음을 보장합니다. 지연 시간은 약간 증가합니다.

Q: Kafka를 사용한 CDC 파이프라인을 어떻게 설계합니까? Debezium(PostgreSQL, MySQL용) 또는 네이티브 CDC 커넥터를 사용하여 소스 데이터베이스의 변경 사항을 캡처합니다. 변경 이벤트를 기본 키로 파티셔닝된 Kafka 토픽에 발행합니다. 싱크 커넥터(BigQuery Sink, S3 Sink)를 사용하여 변경 사항을 분석용 웨어하우스에 로드합니다. 컨슈머에 isolation.level=read_committed를 설정하여 커밋되지 않은 데이터베이스 트랜잭션의 읽기를 방지합니다. 변경 이벤트의 Avro 또는 Protobuf 스키마 관리에는 스키마 레지스트리를 사용합니다.

Q: ISR이란 무엇이며 왜 중요합니까? ISR(In-Sync Replicas)은 파티션 리더와 완전히 동기화된 레플리카의 집합입니다. 리더 선출 대상은 ISR 멤버만 해당됩니다. min.insync.replicas 설정은 쓰기가 커밋된 것으로 간주되기 위해 확인 응답이 필요한 레플리카 수를 정의합니다. ISR이 min.insync.replicas 미만으로 줄어들면, 브로커는 데이터 손실 위험을 피하기 위해 쓰기를 거부합니다.

추가적인 데이터 엔지니어링 면접 준비를 위해, 데이터 엔지니어링 면접 질문 모음에서 ETL/ELT 파이프라인 패턴데이터 모델링 등 더 넓은 주제를 다루고 있습니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

결론

  • Kafka 4.x의 KRaft로 ZooKeeper가 완전히 제거되어, 데이터 엔지니어링 팀의 운영 오버헤드가 감소합니다
  • 파티션 키 선택이 순서 보장을 결정합니다. 편의가 아닌 다운스트림 처리 요구사항에 따라 키를 선택해야 합니다
  • enable.auto.commit=False를 통한 수동 오프셋 커밋으로 At-least-once 전달이 가능하며, 트랜잭셔널 API는 consume-transform-produce 파이프라인에 Exactly-once 시맨틱을 제공합니다
  • Kafka Connect와 Debezium을 활용하면 커스텀 컨슈머 코드 없이 프로덕션 수준의 CDC를 구현할 수 있습니다
  • Share Groups(Kafka 4.2)는 순서가 필요 없는 작업 분배 워크로드에 큐 시맨틱을 추가합니다
  • 컨슈머 그룹 확장은 파티션 수에 의해 제한되므로, 피크 시 컨슈머 병렬 처리 요구에 기반하여 파티션 수를 계획해야 합니다

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#kafka
#streaming
#data-engineering
#partitions
#consumer-groups
#kraft
#event-driven

공유

관련 기사