오늘날의 IT 서비스는 실시간으로 발생하는 사용자 활동 로그, 시스템 이벤트 등을 안정적으로 처리하고 분석하는 것이 중요해졌습니다. 이러한 방대한 양의 대규모 데이터 스트림 처리를 위해 탄생한 플랫폼이 바로 Apache Kafka입니다.

카프카(Kafka)는 최초에 링크드인(Linkedin) 내부에서 대량의 데이터 스트림을 효율적으로 처리하기 위해 개발되었습니다. 당시 내부에는 수많은 어플리케이션이 직접 연결되어 있었고, 어플리케이션이 늘어날수록 데이터 파이프라인의 복잡도가 높아지고 관리가 어려워지는 문제가 발생했습니다. 이러한 문제를 해결하고 링크드인 사이트에서 발생하는 사용자 활동 로그 및 시스템 이벤트를 효과적으로 처리하는 카프카를 제작하였고, 현재는 Apache 재단에 기증되어 오픈소스로 공개되었습니다.

1. Cluster

카프카는 기본적으로 안정성과 확장성을 위해 클러스터(Cluster) 형태로 운영됩니다. 단일 서버로도 실행할 수는 있지만 대부분 테스트나 학습 목적이며, 실제 운영 환경에서는 여러 대의 서버로 구성된 클러스터 구조가 일반적입니다.

1.1 Broker

카프카 클러스터를 이루고 있는 각 서버에는 카프카 프로세스가 실행되고 있는데, 이러한 개별적인 인스턴스를 브로커(Broker)라고 합니다. 브로커는 메시지를 수신하고, 디스크에 저장 및 영구적으로 보관하며, 클라이언트의 요청에 따라 메시지를 제공하는 역할을 합니다.

1.2 Topic

토픽(Topic)은 메시지를 분류하여 저장하는 논리적인 채널입니다. 구매, 클릭 등 다양한 구분에 따라 토픽을 생성하고 관리합니다.

아래에서 더 설명하겠지만, Producer가 특정 토픽에 데이터를 발행하고, Consumer는 토픽을 구독하는 방식으로 데이터가 전달됩니다.

1.3 Partition

파티션(Partition)은 토픽을 여러 개로 나눈 물리적인 분산 처리 단위입니다. 메시지는 파티션 내에 순서대로 쌓이고, 생성된 순서에 따라 0부터 시작하는 순차적인 ID인 오프셋(Offset)을 가집니다.

최초 토픽 생성 시에 몇 개의 파티션을 가질지 지정해야 하며, 한 토픽을 여러 파티션으로 구성한다면 각 파티션은 여러 브로커에 걸쳐 나누어집니다.

또한 고가용성을 위해 파티션을 여러 브로커에 복제할 수 있습니다. 복제 계수(Replication Factor)가 3이면 3개의 브로커에 동일한 데이터가 저장되며, 이 중 하나가 리더(Leader) 역할을 하게 됩니다. Producer의 쓰기 요청과 Consumer의 읽기 요청은 모두 이 리더 파티션을 통해서만 처리됩니다. 그리고 나머지 복제본은 팔로워(Follower) 파티션으로 리더의 데이터를 복제하여 백업 역할을 수행합니다.

파티션을 늘리면 병렬 처리 능력과 확장성이 향상됩니다. 메시지 처리는 오프셋 순서로 처리되는데, 오프셋이 파티션 단위로 관리되기 때문에 병렬 처리 시에 동일한 파티션 내에서의 순서는 보장되지만 토픽 전체의 순서는 보장되지 않습니다.


2. Architecture

카프카의 기본 컨셉은 대규모 분산 환경에서 실시간으로 발생하는 데이터 스트림을 안정적으로 처리하고 영구적으로 저장하는 플랫폼입니다. 이를 위해 클러스터뿐 아니라 다른 클라이언트와 관리 시스템이 필요합니다.

2.1 Zookeeper to KRaft

Zookeeper는 카프카 클러스터의 모든 메타데이터(브로커 정보, 토픽 구성, 파티션 리더 선출 등)를 관리하고 조정하는 역할을 합니다. Kafka 2.8 버전 이후로는 Zookeeper 없이도 자체 내장된 KRaft를 사용하여 메타데이터를 관리할 수 있습니다.

KRaft는 Raft 알고리즘을 기반으로 하며, 기존에 Zookeeper가 담당하던 역할을 카프카 내부에 통합하였습니다. 가장 큰 효율성 향상은 메타데이터 전송 방식의 변화입니다. 기존에는 브로커가 Zookeeper에 메타데이터를 요청(Pull)하여 가져왔지만, KRaft에서는 전용 컨트롤러가 변경 사항을 브로커들에게 직접 전달(Push)해 주는 방식으로 변경되어 메타데이터 전파 속도와 클러스터 안정성이 크게 향상되었습니다.

2.2 Producer

Producer는 카프카 클러스터로 데이터를 전달하는 클라이언트입니다. 어플리케이션에서 생성된 데이터를 정해진 형식으로 가공하고 특정 토픽을 지정하여 브로커로 전송합니다.

데이터 전송 시에 메시지 키를 이용하여 파티션을 분배하거나 메시지 레코드에 대상 파티션을 명시적으로 지정할 수 있습니다. 메시지 키가 지정되지 않으면 기본적으로 라운드 로빈(Round Robin) 방식으로 파티션에 순차적으로 배분합니다.

2.3 Consumer

Consumer는 카프카 클러스터에 저장된 데이터를 가져와 처리하는 클라이언트입니다.

Consumer는 자신이 어디까지 메시지를 읽었는지를 기록하고 관리합니다. 메시지를 처리한 오프셋 정보를 카프카 내부 토픽(일반적으로 __consumer_offsets)에 기록하는데, 이 과정을 오프셋 커밋(Offset Commit)이라고 합니다. 커밋된 오프셋은 Consumer가 장애 등으로 재시작했을 때 이전에 처리했던 메시지를 처음부터 다시 읽지 않고 이어서 읽을 위치를 알려주는 체크포인트 역할을 합니다.

2.4 Data Flow

정리해 보면 아래와 같은 구조가 됩니다.

  1. Producer가 데이터를 토픽으로 전송합니다.
  2. 데이터는 여러 파티션으로 나뉘어 브로커에 분산 저장됩니다.
  3. 브로커는 메시지를 파티션에 순차적으로 기록하고 오프셋을 할당합니다.
  4. Consumer는 브로커에게 메시지를 요청하여 자신이 할당받은 파티션의 데이터를 읽습니다.


3. 메시징 시스템을 넘어선 이벤트 스트리밍 플랫폼

카프카를 단순히 메시지 큐 시스템이라고 하지 않고 이벤트 스트리밍 플랫폼이라는 용어를 사용하는 이유는 카프카의 기본적인 설계와 성능이 기존 시스템을 훨씬 뛰어넘었기 때문입니다. 카프카는 데이터를 단순히 저장하는 데 그치지 않고 영구 저장하는 데이터로 취급하여 이를 스트림처럼 지속적으로 처리하는 데 중점을 두고 있습니다.

Redis, RabbitMQ와 같은 인메모리 시스템에서는 Consumer가 데이터를 가져가면 해당 메시지를 메모리에서 지워버립니다. 하지만 카프카는 Consumer가 메시지를 읽은 후에도 데이터를 삭제하지 않고 설정된 보관 주기(Retention Policy) 동안 디스크에 영구적으로 보관합니다. 이러한 설계 덕분에 다양한 Consumer 그룹이 독립적으로 데이터를 처리할 수 있으며, 시스템 장애가 발생하더라도 과거의 특정 시점으로 돌아가 다시 처리가 가능합니다.

또한 카프카는 이벤트 스트림을 가공하고 분석하는 역할을 지원합니다. 자체적으로 Kafka Streams라는 라이브러리 또는 Kafka Connect 도구를 제공하는데, 이를 통해 데이터를 전송하는 중간 과정에서 실시간으로 집계, 변환, 조인 등의 복잡한 처리를 수행할 수 있습니다.


References