[Kafka] 카프카의 심장: 토픽, 파티션, 프로듀서, 컨슈머 완벽 해부

1 min read

Apache Kafka가 어떻게 대용량 데이터를 실시간으로, 그리고 안정적으로 처리할 수 있는지 궁금하신가요? 그 비밀은 Kafka를 구성하는 핵심 요소들의 유기적인 협력에 있습니다. Kafka는 마치 잘 짜여진 오케스트라와 같습니다. 각 파트가 고유한 역할을 완벽히 수행하며 조화를 이룰 때 비로소 웅장한 교향곡이 완성되는 것처럼, Kafka의 각 구성 요소는 데이터의 흐름을 효율적으로 지휘합니다.

이 글에서는 Kafka 아키텍처의 심장과도 같은 5가지 핵심 구성 요소인 브로커, 토픽, 파티션, 프로듀서, 컨슈머를 깊이 있게 분석합니다. 이 요소들의 역할과 상호작용을 완벽하게 이해한다면, Kafka의 강력한 성능을 제대로 활용하고 견고한 데이터 파이프라인을 설계하는 핵심 역량을 갖추게 될 것입니다.

데이터의 여정: 한 눈에 보는 카프카 데이터 흐름

Kafka에서 데이터는 끊임없이 흐르는 강물과 같습니다. 데이터는 프로듀서(Producer)에서 시작하여, 특정 주제인 토픽(Topic)으로 발행됩니다. 토픽 내에서는 데이터가 파티션(Partition)이라는 단위로 나뉘어 브로커(Broker) 서버에 순서대로 기록됩니다. 마지막으로, 데이터를 필요로 하는 컨슈머(Consumer)가 이 파티션에 접근하여 데이터를 가져가 소비합니다.

이 흐름을 간단한 도식으로 표현하면 다음과 같습니다.

[프로듀서][토픽][파티션][브로커에 저장][파티션][토픽][컨슈머]

이제 각 여정을 책임지는 주역들을 하나씩 살펴보겠습니다.

카프카의 5가지 핵심 요소: 역할과 책임

1. 브로커 (Broker): 데이터 저장소이자 클러스터의 중심
  • 역할: 브로커는 카프카 클러스터를 구성하는 개별 서버입니다. 프로듀서로부터 받은 메시지(데이터)를 토픽의 파티션 단위로 디스크에 저장하고, 컨슈머의 요청에 따라 데이터를 제공하는 물리적인 심장 역할을 합니다. 여러 브로커가 모여 하나의 카프카 클러스터를 형성하며, 이들은 서로 협력하여 데이터를 안전하게 분산 저장하고 관리합니다.
  • 주요 특징:
    • 데이터 저장 및 관리: 토픽의 파티션들을 분산하여 저장합니다.
    • 고가용성: 일부 브로커에 장애가 발생해도 다른 브로커가 작업을 이어받아 서비스 중단을 방지합니다. (데이터 복제 기능)
    • 확장성: 데이터 처리량이 늘어나면 클러스터에 브로커를 추가하는 것만으로 간단하게 수평 확장이 가능합니다.
2. 토픽 (Topic): 데이터 분류를 위한 논리적 채널
  • 역할: 토픽은 데이터가 오고 가는 논리적인 채널이자, 메시지를 분류하는 기준입니다. 마치 데이터베이스의 테이블이나 파일 시스템의 폴더처럼, 관련된 성격의 메시지들을 하나의 토픽으로 묶습니다. 예를 들어, ‘order'(주문), ‘payment'(결제), ‘delivery'(배송)와 같이 비즈니스 도메인별로 토픽을 생성하여 데이터를 체계적으로 관리할 수 있습니다.
  • 주요 특징:
    • 논리적 데이터 그룹: 프로듀서와 컨슈머는 토픽을 기준으로 데이터를 주고받습니다.
    • 파티션 구성: 하나의 토픽은 하나 이상의 파티션으로 구성되어 병렬 처리를 지원합니다.
3. 파티션 (Partition): 병렬 처리와 성능의 핵심
  • 역할: 파티션은 하나의 토픽을 여러 개로 나누어 분산 저장하는 단위입니다. 토픽에 데이터가 쌓이면, 이 데이터는 파티션 내부에 순서대로(Append-only) 기록됩니다. 각 파티션은 독립적으로 동작하므로, 여러 컨슈머가 각기 다른 파티션에 동시에 접근하여 데이터를 병렬로 처리할 수 있게 해줍니다. 이것이 Kafka가 높은 처리량을 달성하는 핵심 원리입니다.
  • 주요 특징:
    • 병렬 처리: 파티션의 개수만큼 컨슈머가 동시에 데이터를 처리할 수 있어 처리량을 극대화합니다.
    • 순서 보장: 하나의 파티션 내에서는 데이터가 들어온 순서가 절대적으로 보장됩니다.
    • 확장성의 기반: 토픽의 파티션 수를 늘리면 시스템 전체의 처리 능력을 향상시킬 수 있습니다.
4. 프로듀서 (Producer): 데이터 발행자
  • 역할: 프로듀서는 데이터를 생성하여 카프카 토픽으로 전송(발행, Publish)하는 클라이언트 애플리케이션입니다. 웹 서버의 로그, IoT 기기의 센서 데이터, 애플리케이션의 이벤트 등 모든 데이터의 시작점에 위치합니다.
  • 주요 특징:
    • 데이터 생성 및 전송: 특정 토픽을 지정하여 메시지를 보냅니다.
    • 파티셔닝: 메시지의 키(Key) 값에 따라 데이터를 보낼 파티션을 지정하거나, 라운드 로빈 방식으로 분배할 수 있습니다.
    • 전송 보장: 메시지 전송 성공 여부를 확인하는 다양한 옵션(ack)을 제공합니다.
5. 컨슈머 (Consumer): 데이터 구독자
  • 역할: 컨슈머는 특정 토픽을 구독(Subscribe)하고, 해당 토픽의 파티션으로부터 데이터를 가져와서 소비(Consume)하는 클라이언트 애플리케이션입니다. 가져온 데이터는 데이터베이스에 저장되거나, 추가 분석을 위해 다른 시스템으로 전달되는 등 다양한 비즈니스 로직을 수행하는 데 사용됩니다.
  • 주요 특징:
    • 데이터 소비: 토픽을 구독하여 저장된 메시지를 순차적으로 읽어옵니다.
    • 컨슈머 그룹: 여러 컨슈머를 하나의 그룹으로 묶어 각 파티션의 데이터를 병렬로 처리할 수 있습니다.
    • 오프셋 관리: 각 컨슈머는 파티션별로 어디까지 데이터를 읽었는지를 나타내는 오프셋(Offset)을 관리하여 데이터 처리 위치를 추적합니다.

프로듀서와 컨슈머: Java 코드 예시로 이해하기

개념을 실제 코드로 확인하면 이해가 더욱 명확해집니다.

프로듀서 (Producer) 코드 예시

프로듀서는 KafkaProducer 객체를 생성하고, ProducerRecord에 보낼 메시지를 담아 send() 메소드로 전송합니다.

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;

public class SimpleProducer {
    public static void main(String[] args) {
        // 1. 카프카 프로듀서 설정
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); // 카프카 브로커 주소
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 메시지 키 직렬화
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 메시지 값 직렬화

        // 2. 프로듀서 인스턴스 생성
        Producer<String, String> producer = new KafkaProducer<>(props);
        String topicName = "my-topic";

        // 3. 메시지 생성 및 전송
        for (int i = 0; i < 5; i++) {
            String messageKey = "key-" + i;
            String messageValue = "Hello Kafka! Message-" + i;
            ProducerRecord<String, String> record = new ProducerRecord<>(topicName, messageKey, messageValue);

            // 비동기 전송 및 콜백 처리
            producer.send(record, (metadata, exception) -> {
                if (exception == null) {
                    System.out.printf("성공! Topic: %s, Partition: %d, Offset: %d\n",
                            metadata.topic(), metadata.partition(), metadata.offset());
                } else {
                    System.err.println("전송 실패: " + exception.getMessage());
                }
            });
        }

        // 4. 프로듀서 종료 (버퍼에 남은 메시지 모두 전송)
        producer.flush();
        producer.close();
    }
}
컨슈머 (Consumer) 코드 예시

컨슈머는 구독할 토픽을 지정하고 poll() 메소드를 통해 주기적으로 데이터를 가져옵니다.

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

public class SimpleConsumer {
    public static void main(String[] args) {
        // 1. 카프카 컨슈머 설정
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); // 카프카 브로커 주소
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-group"); // 컨슈머 그룹 ID
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); // 키 역직렬화
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); // 값 역직렬화
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); // 읽을 오프셋 위치

        // 2. 컨슈머 인스턴스 생성
        Consumer<String, String> consumer = new KafkaConsumer<>(props);
        String topicName = "my-topic";

        // 3. 토픽 구독
        consumer.subscribe(Collections.singletonList(topicName));

        // 4. 메시지 무한 루프 소비
        try {
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
                for (ConsumerRecord<String, String> record : records) {
                    System.out.printf("메시지 수신: Key: %s, Value: %s, Partition: %d, Offset: %d\n",
                            record.key(), record.value(), record.partition(), record.offset());
                }
            }
        } finally {
            consumer.close();
        }
    }
}

더 깊이 알아보기: 카프카의 고급 개념들

  • 컨슈머 그룹과 파티션 재조정(Rebalancing): 여러 컨슈머가 하나의 group.id를 공유하면 컨슈머 그룹이 됩니다. Kafka는 토픽의 파티션들을 그룹 내 컨슈머들에게 공평하게 분배하여 병렬 처리를 극대화합니다. 그룹에 새로운 컨슈머가 추가되거나 기존 컨슈머가 이탈하면, 파티션 소유권이 재조정되는 ‘리밸런싱’이 일어납니다.
  • 오프셋(Offset) 관리의 중요성: 오프셋은 컨슈머가 특정 파티션에서 어디까지 메시지를 읽었는지 기록하는 숫자입니다. 이 오프셋을 어떻게 관리하느냐에 따라 메시지 처리 보장 수준(At-least-once, At-most-once, Exactly-once)이 달라집니다. 오프셋을 잘못 관리하면 데이터가 유실되거나 중복 처리될 수 있어 매우 중요합니다.
  • 데이터 복제(Replication)와 고가용성: Kafka는 데이터 안정성을 위해 파티션의 복제본을 여러 브로커에 저장합니다. 이때 원본을 리더(Leader), 복제본을 팔로워(Follower)라고 부릅니다. 리더 브로커에 장애가 발생하면 팔로워 중 하나가 새로운 리더로 선출되어 데이터 유실 없이 서비스를 지속합니다.

정리하며

지금까지 카프카를 구성하는 핵심 요소들과 그 작동 원리를 살펴보았습니다.

  • 프로듀서가 데이터를 만들어 토픽에 보내면,
  • 토픽은 여러 파티션으로 나뉘어 브로커에 분산 저장되고,
  • 컨슈머는 이 파티션들로부터 데이터를 병렬로 가져와 처리합니다.

이 견고하고 확장 가능한 아키텍처 덕분에 Kafka는 대규모 데이터 스트리밍 플랫폼의 표준으로 자리 잡을 수 있었습니다. 각 요소의 역할을 명확히 이해하는 것이야말로 Kafka 기반의 안정적인 시스템을 구축하는 첫걸음입니다.

쿠버네티스 시크릿 관리, 어떤 방법이 최선일까? 4가지 방식 장단점…

쿠버네티스에서 애플리케이션을 운영할 때, DB 접속 정보나 API 키 같은 민감한 정보, 즉 ‘시크릿(Secret)’을 어떻게 관리해야 할지는 모두의 공통된 고민입니다. 관리 방식은 보안, 운영...
eve
13 sec read

루아 Lua 프로그래밍 : 모듈과 패키지 가이드

지금까지 우리는 함수로 코드를 묶고, 테이블로 데이터를 구조화하는 방법을 익혔습니다. 하지만 프로젝트의 규모가 커지기 시작하면, 모든 코드를 단 하나의 파일에 담는 것은 금세 한계에...
eve
53 sec read

루아 (Lua) 프로그래밍: 테이블과 메타테이블의 모든 것

Lua 프로그래밍의 여정에서 가장 중요하고 흥미로운 지점에 도달했습니다. 바로 Lua 언어의 심장이자 가장 중심적인 기능인 테이블(Table)입니다. Lua에는 배열, 딕셔너리, 리스트, 객체 등을 위한 별도의...
eve
1 min read