Infrastructure

Apache Kafka와 Kafka API 핵심 개념 마스터하기

Written by 개발자서동우 · 15 sec read >
Apache Kafka와 Kafka API 핵심 개념 마스터하기

안녕하세요 Devloo 입니다. 🙂 오늘은 데이터 인프라의 핵심 요소로 자리 잡은 카프카(Kafka)에 대해 깊이 있게 알아보려 합니다.

카프카(Kafka)는 처음에 LinkedIn에서 견고하고 확장 가능한 메시지 버스를 구축하기 위해 시작된 프로젝트입니다. 카프카는 LinkedIn의 데이터 인프라에서 중요한 역할을 했으며, 그 독특한 기능과 역량 덕분에 널리 채택되었습니다. LinkedIn은 카프카의 잠재력을 인식하고 2016년에 이를 Apache Software Foundation에 기부했습니다. 이후 Apache Kafka로 발전하여 원래의 기능을 유지하면서도 추가 기능과 개선을 이루었습니다.

카프카의 인기가 높아지고 코드베이스가 성숙해지면서 여러 벤더들이 자체 카프카 배포판을 제공하기 위해 포크하였습니다. RedpandaWarpstream과 같은 회사들은 Java가 아닌 다른 프로그래밍 언어로 전체 코드베이스를 다시 작성하기도 했습니다.

하지만 이러한 변화 속에서도 이들 회사는 한 가지 중요한 점을 유지하려고 노력했습니다. 바로 카프카 API입니다. 이 표준 인터페이스는 서로 다른 카프카 배포판 간의 호환성과 상호 운용성을 보장하는 데 중요한 역할을 합니다.

Apache Kafka와 Kafka API

Apache Kafka와 Kafka API는 서로 다른 두 개체이므로 그 차이를 이해하는 것이 중요합니다.

Apache Kafka는 분산형 추가 전용 커밋 로그입니다. 프로듀서로부터 메시지를 받아 이를 내결함성 방식으로 저장하여, 컨슈머가 이 메시지들을 수신된 순서대로 가져갈 수 있게 합니다. 이러한 설계 덕분에 카프카는 실시간 데이터 피드를 처리하는 데 있어 신뢰성과 높은 확장성을 갖춘 Pub/Sub 메시징 시스템으로 자리잡았습니다.

개발자는 Kafka API를 통해 카프카와 작업하게 됩니다. 개발자로서 Kafka API를 이용해 데이터를 읽고 쓰는 클라이언트 애플리케이션을 작성할 수 있습니다. 운영자로서도 Kafka API를 이용해 카프카에서 관리 작업을 수행할 수 있습니다. 오픈 소스든 상용 배포판이든 동일한 Kafka API를 사용해야 하므로, Kafka API를 이해하는 것은 매우 중요합니다.

Kafka와 Kafka API
브로커는 어떤 카프카 배포판이든 동일한 Kafka API를 유지합니다.

이 글에서는 특정 프로그래밍 언어나 벤더와 무관하게 모든 카프카 배포판을 사용할 때 겪게 되는 고수준 개념을 다루겠습니다. Kafka 프로토콜이나 API 메서드의 세부 사항에 대해서는 다루지 않을 것입니다.

브로커와 클러스터

카프카는 분산 시스템으로, 브로커는 이 시스템 내의 단일 노드를 나타냅니다.

브로커는 물리적 머신, 가상 머신, 컨테이너 등 다양한 형태를 취할 수 있습니다. 여러 브로커를 함께 구성하면 카프카 클러스터가 형성됩니다. 여러 브로커를 통해 로드 밸런싱과 내결함성이 보장됩니다.

클러스터 내의 각 브로커는 카프카 데몬을 실행하며 스토리지를 관리합니다.

클러스터에 여러 브로커가 있을 때 클라이언트 애플리케이션이 처음에 한 브로커에 연결하도록 구성하려면 어떻게 해야 할까요? 부트스트랩 서버 주소를 사용해야 합니다. 이는 애플리케이션이 클러스터 메타데이터를 획득하기 위해 처음 상호 작용하는 브로커입니다. 이 주제에 대해서는 나중에 자세히 다룰 것입니다.

클러스터 내 브로커의 수는 일반적으로 홀수로 설정하는데, 이는 쿼럼을 형성하는 데 도움이 되기 때문입니다.

메시지

카프카 세계에서는 메시지, 레코드, 이벤트라는 용어가 모두 동일한 개념을 의미합니다. 이는 애플리케이션과 브로커 간에 전송되는 데이터 단위를 가리킵니다.

메시지는 키와 값으로 구성됩니다. 값은 실제 데이터나 전송하려는 페이로드를 포함하고 있습니다. 키는 null을 포함해 어떤 값이라도 가질 수 있습니다. 일반적으로 값과 관련된 속성이 키로 지정됩니다. 예를 들어, 값이 주문 객체라면 키는 고객 ID가 될 수 있습니다. 키를 사용하는 목적은 메시지를 특정 토픽 내의 특정 파티션으로 라우팅하기 위함입니다. 파티션에 대해서는 나중에 더 자세히 다루겠습니다.

카프카 메세지, 레코드, 이벤트
카프카에서는 메시지, 레코드, 이벤트라는 용어가 모두 같은 의미로 사용됩니다.

키와 값 모두 가변 길이의 바이트 배열로 표현됩니다. 이를 통해 카프카는 일반 텍스트부터 직렬화된 객체까지 다양한 데이터 유형을 처리할 수 있습니다.

토픽

토픽은 메시지를 논리적으로 그룹화한 것입니다. 이는 관계형 데이터베이스에서 관련된 레코드를 함께 보관하는 테이블과 비슷한 개념입니다. 다양한 목적에 따라 서로 다른 토픽을 가질 수 있습니다.

토픽은 카프카 정보 계층의 최상위에 위치합니다. 개발자로서 여러분은 데이터를 생성하고 소비하는 클라이언트 애플리케이션을 작성하게 될 것입니다. 이러한 토픽은 메시지 브로커의 Pub/Sub 의미론을 차용합니다. 즉, 여러 프로듀서와 컨슈머가 동시에 데이터를 쓰고 읽을 수 있도록 지원합니다. 또한 하나의 토픽에 생성된 메시지를 여러 컨슈머가 소비할 수 있는 브로드캐스트/팬아웃 스타일의 메시징도 지원합니다.

토픽은 오직 추가 작업만 허용하며, 임의의 변형은 허용하지 않습니다. 즉, 한 번 토픽에 쓰여진 메시지는 수정할 수 없습니다. 또한 토픽에서 읽는 작업은 파괴적이지 않습니다. 동일한 컨슈머가 나중에 다시 와서 메시지를 재읽을 수 있습니다. 이는 컨슈머 오프셋에 대해 이야기할 때 더 자세히 다루겠습니다.

토픽은 클러스터 전반에 걸쳐 분산됩니다

파티션과 오프셋

토픽은 연속된 개념이 아니라, 파티션으로 구성됩니다.

토픽은 논리적 개념인 반면, 파티션은 보다 구체적인 실체입니다.

토픽 파티션은 하나의 토픽에 속하는 데이터의 하위 집합을 저장하는 추가 전용 정렬 로그 파일입니다. 하나의 토픽은 여러 개의 파티션을 가질 수 있으며, 이러한 파티션은 클러스터의 다양한 브로커에 분산되어 로드 밸런싱과 내결함성을 제공합니다.

왜 파티션이 필요할까요?

만약 파티션 개념이 없고, 카프카가 토픽의 데이터를 단일 블록으로 유지한다면 어떤 일이 벌어질까요? 첫째, 데이터가 늘어남에 따라 토픽의 크기가 커져 단일 머신의 저장 한도를 곧 초과하게 될 것입니다. 추가 스토리지를 연결하여 머신의 용량을 늘릴 수는 있지만, 이는 결국 한계에 도달할 것입니다.

둘째, 모든 컨슈머가 거대한 토픽을 보유한 브로커에서 데이터를 소비해야 합니다. 이는 해당 브로커의 부하를 증가시키게 되고, 컨슈머 로드 밸런싱이 불가능해집니다. 게다가 이렇게 거대한 토픽을 백업하는 데는 많은 시간이 소요되며, 이를 저장하는 브로커가 충돌하면 모든 데이터를 잃을 위험이 큽니다.

요약하자면, 카프카에서 토픽 파티션을 사용하는 것은 여러 브로커에 데이터를 분산시킬 수 있기 때문에 유익합니다. 이러한 분산은 로드 밸런싱과 내결함성을 개선합니다. 또한, 파티션을 통해 시스템을 더 확장 가능하게 만들 수 있습니다. 토픽의 데이터가 단일 머신의 저장 한도를 넘어서 성장할 수 있기 때문입니다. 추가로, 파티션을 통해 컨슈머 로드 밸런싱이 가능해지므로, 컨슈머가 서로 다른 브로커에서 데이터를 소비할 수 있습니다. 이는 시스템을 더욱 효율적이고 신뢰성 있게 만들며, 하나의 브로커가 충돌하더라도 모든 데이터를 잃을 위험을 줄여줍니다.

파티션 오프셋

파티션 내의 각 메시지는 고유한 오프셋을 가집니다. 오프셋은 파티션 로그 파일 내에서 메시지의 위치를 나타내는 단조 증가하는 정수입니다. 간단히 말해, 오프셋은 메시지가 로그 파일의 시작 지점에서 얼마나 떨어져 있는지를 나타냅니다.

메시지가 파티션에 쓰여질 때, 이는 로그의 끝에 추가되며 다음 순차적인 오프셋이 할당됩니다. 오프셋은 컨슈머가 파티션에서 소비한 메시지를 추적하는 데 특히 유용합니다.

카프카 :: 파티션 오프셋
카프카 :: 파티션 오프셋
메시지 순서와 파티션 라우팅

파티션에 쓰여진 메시지는 항상 도착 시간 순으로 정렬됩니다. 그러나 토픽 전체에 걸쳐 메시지 순서가 보장되지는 않습니다. 파티션 내에서 엄격한 순서를 유지하려면 파티션 키를 적절히 사용해야 합니다. 어떻게 할까요?

앞서 배운 바와 같이, 각 메시지에 키를 포함할 수 있습니다. 메시지를 수신하면 카프카는 키에 대해 해시 함수를 사용하여 메시지가 쓰여질 파티션을 결정합니다. 이는 동일한 키를 가진 모든 레코드가 전송된 순서대로 동일한 파티션에 도착하도록 보장합니다.

카프카 파티션 라우팅
카프카 파티션 라우팅

파티션 복제 — 리더와 팔로워

파티션은 두 개 이상의 복사본을 가질 수 있습니다. 이는 주로 내결함성과 고가용성을 위해 복제됩니다.

카프카는 여러 복사본을 유지함으로써 하나의 브로커가 실패하더라도 다른 브로커가 데이터를 제공할 수 있도록 합니다. 이 중복성은 시스템이 실패 상황에서도 계속 작동할 수 있도록 합니다. 또한 여러 브로커에 복제본을 분산하여 읽기 및 기 요청의 부하를 분산함으로써 시스템 성능을 향상시킵니다.

토픽을 생성할 때 파티션 수와 복제 계수를 선택할 수 있습니다. 예를 들어, 10개의 파티션을 가진 토픽이 있고 복제 계수가 3으로 설정되어 있다면, 클러스터 전체에 10×3=30개의 파티션이 저장됩니다.

파티션 리더와 팔로워

각 파티션 복제본은 리더 또는 팔로워 역할을 합니다. 리더 복제본은 해당 파티션에 대한 모든 읽기 및 쓰기 요청을 처리하며, 팔로워 복제본은 리더를 수동적으로 복제합니다. 만약 리더가 실패하면 팔로워 중 하나가 자동으로 새로운 리더가 됩니다.

카프카, 파티션 리더와 팔로워.webp

그렇다면 카프카는 어떻게 파티션 리더를 결정할까요? 카프카는 Apache Zookeeper와 같은 분산 합의 알고리즘 구현에 의존하여 파티션의 리더 선출을 처리합니다. 브로커가 실패하고 다시 온라인 상태가 되거나 새로운 브로커가 클러스터에 추가되면, Zookeeper는 각 파티션의 새로운 리더를 선출하는 데 도움을 줍니다. 이 선출 과정은 특정 시간에 하나의 브로커만이 특정 파티션의 리더로 활동하도록 보장합니다.

그러나 카프카의 Zookeeper 의존성은 점차 KRaft로 대체되고 있습니다. Redpanda와 같은 일부 브로커는 네이티브 Raft 구현을 브로커에 통합하고 있습니다.

세그먼트

파티션이 구체적인 실체라고 언급했지만, 사실 완전히 그렇지는 않습니다. 파티션은 다시 세그먼트로 나뉩니다.

세그먼트(Segment)는 카프카 스토리지에서 데이터를 보관하는 가장 작은 단위로, 본질적으로 파티션에 속하는 메시지의 하위 집합을 담고 있는 추가 전용 정렬 로그 파일입니다. 여러 세그먼트가 모여 하나의 파티션을 형성합니다.

각 파티션에는 항상 데이터를 받는 활성 세그먼트가 하나씩 있습니다. 활성 세그먼트에 충분한 메시지가 쌓이면 이 세그먼트는 닫히거나 다음 활성 세그먼트로 “롤오버”됩니다. 이 세그먼트의 크기는 구성 가능합니다.

닫힌 세그먼트의 메시지는 디스크 공간을 절약하기 위해 삭제되거나 압축될 수 있습니다. 이 또한 구성 가능합니다. 더 나아가 계층형 스토리지를 사용하면, 오래된 로그 세그먼트를 S3 버킷과 같은 비용 효율적인 스토리지로 아카이브하여 스토리지 비용을 줄일 수 있습니다.

파티션과 달리 세그먼트는 개발자에게 보이지 않으며 접근할 수도 없습니다. 주로 스토리지와 운영 측면에서 관리됩니다.

카프카 클라이언트

개발자로서, 해당하는 카프카 클라이언트 SDK가 있는 프로그래밍 언어로 카프카 클라이언트를 작성할 수 있습니다. 기본적으로 Java와 Scala가 있지만, Python, .NET, Go, Rust, C++ 등 다양한 언어에 대한 카프카 클라이언트 SDK도 제공됩니다.

프로듀서는 메시지를 생성하여 카프카 토픽에 전송하는 클라이언트 애플리케이션입니다. SDK는 send() 메서드를 통해 메시지 전송을 지원하며, 이 메서드는 토픽 이름, 키, 값, 파티션 ID를 매개변수로 받아들입니다. SDK는 메시지를 파티션별로 그룹화하고, 배치로 묶어 배치 크기가 일정 임계값에 도달하면 브로커에 배치를 전송합니다.

컨슈머는 카프카 토픽에서 메시지를 읽는 클라이언트 애플리케이션입니다. 카프카 클라이언트 SDK는 메시지를 하나씩 또는 배치로 소비할 수 있는 메서드를 제공합니다. 컨슈머는 하나 이상의 토픽을 구독하고 메시지를 작성된 순서대로 소비할 수 있습니다.

컨슈머 그룹은 여러 컨슈머가 레코드 처리를 분담할 수 있도록 하는 기능입니다. 여러 컨슈머가 동일한 토픽을 구독하고 동일한 컨슈머 그룹에 속할 때, 그룹 내 각 컨슈머는 레코드의 하위 집합을 받게 됩니다. 카프카는 메시지가 그룹 내 하나의 컨슈머에게만 소비되도록 보장하며, 장애 발생 시 컨슈머를 재조정하여 확장성과 내결함성을 제공합니다. 이는 시스템의 확장성과 신뢰성을 높이는 유용한 기능입니다.

마무리

결론적으로, 우리는 카프카와 카프카 API의 기본 개념과 구성 요소를 심도 있게 탐구했습니다. 카프카가 어떻게 분산형 추가 전용 커밋 로그로서 작동하며, 카프카 API가 이와 상호 작용하는 표준 인터페이스를 제공하는지 살펴보았습니다.

브로커와 클러스터, 메시지, 토픽, 파티션, 오프셋, 그리고 복제와 같은 핵심 개념들을 이해하는 것은 카프카를 제대로 활용하는 데 필수적입니다. 특히, 세그먼트의 역할과 카프카 클라이언트의 기능은 시스템의 효율성과 신뢰성을 높이는 데 중요한 요소입니다.

이 글을 통해 여러분이 카프카의 구조와 작동 원리에 대해 좀 더 명확하게 이해할 수 있었기를 바랍니다. 카프카를 처음 접하는 개발자든, 이미 사용하고 있는 전문가든, 이 내용을 바탕으로 더 나은 데이터 처리와 메시징 시스템을 구축할 수 있기를 응원해봅니다.

언제든지 궁금한 점이나 더 알고 싶은 부분이 있다면, 댓글로 남겨주세요. 끝까지 읽어주셔서 정말 감사합니다. 🙂

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *