
개요
카프카에 대해 제대로 공부해보고자 스터디를 시작했습니다. 이번 포스팅 시리즈에서는 서적 "카프카 핵심 가이드" 를 완독하고 카프카의 구조 및 생태계에 대한 전반적인 학습 및 각 기능의 POC를 실제로 수행하는 것을 목표로 합니다.
책의 서문에서, 유명한 과학자인 닐 디그래스 타이슨의 말을 인용합니다.
"과학자들이 서로 동의하지 않는 상황이 벌어진다면, 그것은 데이터가 불충분하기 때문이다. 그리고 어떠한 데이터를 얻을 것인지를 합의하고 데이터를 얻을 수 있다면 문제는 해결된다. 내가 올든 상대방이 옳든, 아니면 둘 다 틀리든 말이다. 그제서야 우리는 그 다음으로 진행해 갈 수 있다." - 닐 디그래스 타이슨
모든 기업은 데이터로 움직이며, 우리는 이런 데이터를 얻고, 분석하고, 가공하여 단순한 데이터 이상의 것을 결과물로 산출해야만 합니다.
데이터의 모든 부분은 의미가 있으며 중요한 정보를 담고 있습니다.
이런 중요한 정보를 자세히 알기 위해서는 데이터를 생성한 곳에서 분석할 수 있는 곳으로 옮겨야 합니다.
이러한 작업을 더 빠르게 해낼수록 조직은 더 유연해지고 더 민첩해질 수 있습니다. 우리가 데이터를 이동시키는 작업에 더 적은 시간과 노력을 들일수록 핵심 비즈니스에 더욱 집중할 수 있습니다.
데이터 중심 기업에서 파이프라인이 핵심적인 요소가 되는 이유가 바로 이것 때문입니다.
아파치 카프카에 대해 알아보기 전에, 발행/구독 메시지 전달(이하 pub/sub messaging)의 개념과 데이터 주도 애플리케이션에서의 중요성을 이해할 필요가 있습니다.
pub/sub messaging 에서 전송자(pub)는 수신자(sub)로 직접 메시지를 보내지 않습니다.
대신 전송자는 메시지를 분류하여 "브로커" 라 불리는 중간 지점에 전달하고, 수신자는 그렇게 분류된 메시지를 구독합니다.
2대의 서버가 지표를 생성하고, 이 지표를 모니터링하기 위한 지표 서버에 지표를 전송한다고 가정해봅시다.

그림 1. 발행자와 구독자가 직접 연결
서버1과 서버2는 각각 지표를 생성하여, 대시보드에 지표를 정리해서 노출해주는 지표 서버로 직접 전달 합니다.
이때, 지표 서버 뿐만 아니라, 각 서버의 활동상태를 확인하기 위한 활동 모니터링 서버, 지표를 직접 분석해서 결과물을 산출하는 분석 서버 가 새로 추가되었다고 가정해보겠습니다.

그림 2. 발행자와 여러 구독자가 직접 연결
심지어 이제는, 서버1, 서버2 뿐만이 아니라, 서버 3,4,5가 추가로 지표를 생성하며, 수신 서버들에서는 모든 서버의 지표를 다 받는 것이 아닌 일부 서버의 지표만 필요한 상황이 되었다고 가정해봅시다.
아래와 같은 그림이 될 것입니다.

그림 3. 여러 발행자와 여러 구독자가 직접 연결
연결을 추적하는 것이 굉장히 어려워졌습니다. 이러한 방식에서 발생하는 기술 부채는 명백하기 떄문에 개선이 필요합니다.
각 지표값을 받고 받은 지표들을 질의하면 답변해주는 어플리케이션을 중간 계층에 추가하면 됩니다.

그림 4. 메시지 발생/구독 시스템
바로 이것이 메시지 발행/구독 시스템입니다.
앞에서 지표를 다루던 것처럼, 로드 메시지에 대해서도 비슷한 작업을 해줘야합니다. 앞서서는 간단하게 지표 서버, 활동 모니터링 서버, 분석 서버 3대로 수신자를 제한했지만, 사용자 활동을 추적해서 머신러닝 개발자에게 제공하거나 관리자용 보고서를 생성하는 데 사용해야할 수도 있습니다.
이러한 경우에도 비슷한 시스템을 구성함으로써 정보의 발행자와 구독자를 분리할 수 있습니다.

그림 5. 다수의 발행/구독 시스템
그림 3에서 본 point-to-point 연결 방식보다 이쪽이 더 바람직하긴 하지만, 이렇게 되면 버그도 한계도 제각각인 다수의 데이터 큐 시스템을 유지 관리해야하는 오버헤드가 생깁니다.
여기에 메시지 교환을 필요로 하는 사례가 추가 된다면 어떨까요?
비즈니스가 확장됨에 따라 함께 확장되는, 일반화된 유형의 데이터를 발행하고 구독할 수 있는 중앙 집중화된 시스템이 필요할 것 같습니다.

아파치 카프카는 위에서 설명한 것과 같은 문제를 해결하기 위해 고안된 메시지 발행/구독 시스템입니다.
'분산 커밋 로그' 혹은 '분산 스트리밍 플랫폼' 이라고 불리기도 합니다.
파일 시스템이나 데이터베이스 커밋 로그는 모든 트랜잭션 기록을 지속성있게 보존함으로써 시스템의 상태를 일관성있게 복구할 수 있도록 고안되었습니다.
이러한 파일 시스템, 데이터베이스와 유사하게, 카프카에 저장된 데이터는 순서를 유지한채로 지속성있게 보관되며 결정적으로 읽을 수 있습니다.
또한 확장시 성능을 향상시키고 실패가 발생하더라도 데이터 사용에는 문제가 없도록 시스템 안에서 데이터를 분산시켜 저장할 수 있습니다.
카프카에서 데이터의 기본 단위는 메시지 입니다. 메시지에는 특정한 형식이나 의미가 없으며, 키라고 불리는 메타데이터를 포함할 수도 있습니다.
키는 저장할 파티션을 결정하는 역할을 하며, 가장 간단한 파티션 결정 방법은 키값에서 일정한 해시값을 생성한 뒤 이 값을 토픽의 파티션 수도 나눴을 때 나오는 나머지 값에 해당하는 파티션에 메시지를 저장하는 것입니다.
이렇게 하면 같은 키 값을 가진 메시지는 항상 같은 파티션에 저장되어 같은 키에 한해서는 항상 순서를 보장할 수 있습니다.
카프카는 효율성을 위해 메시지를 배치 단위로 저장합니다. 배치는 그저 같은 토픽의 파티션에 쓰여지는 메시지들의 집합일 뿐이며, 메시지를 쓸 때마다 네트워크 상에서 신호가 오가는 것은 막대한 오버헤드를 발생시키므로, 배치 단위로 모아서 쓰면 이 오버헤드를 줄일 수 있습니다.
물론 이것은 지연(latency)와 처리량(throughput)사이에 트레이드 오프를 발생시키므로, 서비스의 성격에 맞춰 튜닝하는 것이 중요합니다.
카프카 입장에서 메시지는 단순한 바이트 배열일 뿐이지만, 내용을 이해하기 쉽도록 일정한 구조(Schema)를 부여하는 것이 권장됩니다.
이러한 메시지 스키마에는 여러가지가 있는데, 가장 간단한 방법으로는 쓰기 쉽고 사람이 알아볼 수 있는 JSON, XML 과 같은 것들이 있습니다.
하지만 이 방식들은 타입 처리 기능이나 스키마 버전 간의 호환성 유지 기능이 떨어지므로, 많은 카프카 개발자들은 아파치 에이브로(Avro)를 선호합니다.
Avro는 조밀한 직렬화 형식을 제공하는 데다 메시지 본체와 스키마를 분리하기 때문에 스키마가 변경되더라도 코드를 생성할 필요가 없습니다. 보통은 스키마 레지스트리와 함께 사용하며, 이에 관해서는 추후 기회가 있다면 따로 다뤄보도록 하겠습니다.
카프카에 저장되는 메시지는 토픽 단위로 분류됩니다. 토픽에 대한 개념을 이해하기 쉽게 설명하자면, RDBMS의 테이블을 생각하면 된다고 말하고 싶습니다.
토픽은 다시 여러개의 파티션으로 나뉘어집니다.
파티션에 메시지가 쓰여질 때는, 추가만 가능한 형태로 쓰여지며(append-only), 읽을때는 맨 앞부터 제일 끝까지의 순서로 읽힙니다.
대개 토픽에 여러개의 파티션이 있는 만큼 토픽 안의 메시지 전체에 대한 순서는 보장되지 않으며, 단일 파티션 안에서만 순서가 보장됩니다.
이에 대한 이해를 돕기 위해, 다음 그림을 참조해 주세요.

0,1,2,... 숫자를 메시지로 취급한다고 가정해보겠습니다. 아직 오프셋에 대해서는 다루지 않았으나, 여기서의 숫자는 오프셋과는 다릅니다.
파티션1(0)을 파티션 1번의 0번 메시지라고 부르겠습니다.
하나의 파티션 내의 0,1,... 의 숫자는 순서가 보장이 되지만, 다른 파티션간의 순서에서는 순서 보장이 되지 않는다는 의미입니다.
🌱 위와 같은 파티션의 순서보장 규칙 때문에, 순서가 중요한 데이터들은 키를 지정하여, 항상 같은 파티션에 배정되도록 하는 것이 바람직합니다.
카프카와 같은 시스템을 이야기할 때면 스트림 이라는 용어가 자주 사용됩니다. 대부분의 경우 스트림은 "하나의 토픽에 저장된 데이터"로 간주되며, 프로듀서로부터 컨슈머로의 하나의 데이터 흐름을 나타냅니다.
메시지의 집합을 스트림이라는 용어로 부르는 것은 "카프카 스트림", "아파치 삼자", "아파치 스톰" 과 같은 프레임워크에서 메시지를 실시간으로 처리하는 것처럼 스트림 처리에 대한 논의를 진행할 때 가장 일반적인 관례입니다.
이러한 방식의 스트림 처리는 데이터를 시간이 흐른 뒤 한꺼번에 대량으로 처리하는 하둡(Hadoop)과 같은 오프라인 프레임워크와 대비됩니다.
카프카 클라이언트는 이 시스템의 사용자이며, 기본적으로 프로듀서와 컨슈머의 두 종류가 있습니다.
더 고급 클라이언트 API도 있는데, 데이터 통합에 사용되는 카프카 커넥트 API와 스트림 처리에 사용되는 카프카 스트림즈가 바로 그것입니다.
이 고급 클라이언트들은 프로듀서와 컨슈머를 기본적인 요소로서 사용하며, 좀 더 고차원적인 기능을 제공합니다.
이에 관해 대략적인 카프카 생태계를 그림으로 표현해보자면 다음과 같습니다.

위 그림과 설명을 잘 읽어보면, 카프카 생태계 전반이 어떻게 이루어지는지 감을 잡을 수 있을겁니다.
추가로, 컨슈머는 컨슈머 그룹의 일원으로서 작동합니다. 컨슈머 그룹은 토픽에 저장된 데이터를 읽어오기 위해 협업하는 하나 이상의 컨슈머로, 컨슈머 그룹은 각 타피션이 하나의 컨슈머에 의해서만 읽히도록 조정합니다. 아래 그림은 하나의 컨슈머 그룹에 속한 3개의 컨슈머가 하나의 토픽에서 데이터를 읽어오는 모습을 표현한 것입니다.

하나의 컨슈머 그룹이 모든 파티션에서 데이터를 읽되, 각 컨슈머 그룹에 속한 컨슈머들끼리 파티션에 대한 소유권을 가지고 파티션을 전담하는 것을 확인할 수 있습니다.
이러한 컨슈머 그룹의 개념을 활용하여, 컨슈머를 수평 확장하여 파티션에서 데이터를 읽어오는 성능을 극대화하는 것이 가능합니다.
하나의 카프카 서버를 브로커 라고 부릅니다. 브로커는 프로듀서로부터 메시지를 전달받아 오프셋을 할당한 뒤 디스크 저장소에 씁니다. 브로커는 컨슈머의 파티션 읽기 요청 역시 처리하고 발행된 메시지를 보내줍니다.
💡시스템 하드웨어의 성능에 따라 다르겠지만, 하나의 브로커는 초당 수천개의 파티션과 수백만개의 메시지를 쉽게 처리할 수 있습니다.
카프카 브로커는 클러스터의 일부로서 작동하도록 설계되었습니다.
하나의 클러스터 안에 여러개의 브로커가 포함될 수 있으며, 그 중 하나의 브로커가 클러스터 컨트롤러의 역할을 하게 됩니다.
컨트롤러의 역할을 정리하면 다음과 같습니다:
컨트롤러가 어떤 프로커가 어떤 파티션을 맡을지를 결정해주는데, 이때 파티션을 맡은 브로커를 파티션 리더라고 부릅니다. 파티션은 보통 복제본(replica)가 존재하므로, 이 복제본을 할당받는 브로커는 파티션 팔로워라고 부릅니다.
이러한 복제본은 파티션 리더가 모종의 이유로 죽었을때 파티션 팔로워가 리더로 승격되어 리더 역할을 이어받을 수 있도록 하여 가용성을 보장하기 위해 존재합니다.
모든 프로듀서는 리더 브로커에 메시지를 발행해야 하지만, 컨슈머는 리더나 팔로워 중 하나로부터만 데이터를 읽어올 수 있습니다. 이와 관련하여 ack라는 개념이 있는데, 이는 추후에 다뤄보겠습니다.
🌱 클러스터 컨트롤러를 선출하는건 Raft 합의 알고리즘을 사용합니다.
과거 카프카 버전에서는 Zookeeper를 통해 클러스터 컨트롤러를 선정했는데요, 카프카 생태계의 완전한 독립을 실현하고자 이제는 Zookeeper가 아닌 KRaft라는 별도의 모듈을 만들기로 KIP-500에서 논의되었습니다.
KRaft에서는 단 하나의 컨트롤러만 가지던 Zookeeper 기반 방식과 다르게, 컨트롤러 클러스터를 따로 가질 수 있습니다. 혹은 각 브로커가 컨트롤러 역할을 동시에 수행할 수도 있습니다.
🌱 KIP는 카프카에 관한 주요 의논사항을 논의하고 결정하는 문서 체계입니다.
아파치 카프카의 핵심 기능 중에 일정 기간 동안 메시지를 지속성 있게 보관하는 보존 기능이 존재합니다.
카프카 브로커는 토픽에 대해 기본적인 보존 설정이 되어 있는데, 특정 기간동안 메시지를 보존하거나 파티션의 크기가 특정 사이즈에 도달할 때까지 보존합니다.
이러한 한도값에 도달하면 메시지는 만료되어 삭제되게 됩니다.
설치된 카프카가 확장되어감에 따라 하나의 클러스터에 여러 토픽을 넣는 것보다, 다수의 클러스터를 운용하는 것이 더 나은 경우도 존재합니다.
이 방식에는 다음과 같은 장점이 있습니다:
특히 카프카가 다수의 데이터센터에서 운용될 때는 데이터센터 간에 메시지를 복제해 줄 필요가 있는 경우가 많습니다.
이렇게 하면 온라인 애플리케이션은 양쪽 데이터센터 모두에서 사용자 활동 정보를 사용할 수 있습니다.
예를 들어서, 사용자가 자신의 프로필에 있는 공개 정보를 수정한다면 이 변경사항은 검색 결과가 보여지는 데이터센터가 어디냐에 관계 없이 보여야 합니다.
카프카 클러스터의 복제 메커니즘은 다중 클러스터 사이에서가 아닌 하나의 클러스터 안에서만 작동하도록 설계되었습니다.
그래서 클러스터간 복제에는 미러메이커라는 툴을 사용하게 되는데요, 이에 관해서는 오프셋 Sync와 같은 고급 개념이 얽혀있어, 책의 후반부에서 다시 살펴보겠습니다.
발행/구독 메시지 전달 시스템에는 여러가지가 있습니다. 그렇다면 카프카를 선택해야하는 상황에는 어떤 경우가 있을까요?
카프카는 자연스럽게 여러 프로듀서를 처리할 수 있습니다. 프로듀서 클라이언트가 여러 토픽을 사용하든 하나의 토픽을 사용하든 관계 없이, 데이터를 수집하고 일관성을 유지하는데 적합합니다.
예를 들어서 다수의 마이크로 서비스를 통해 사용자에게 컨텐츠를 서비스하는 사이트에서는 모든 서비스가 공통의 형식으로 쓸 수 있는 페이지 뷰용 토픽을 가질 수 있습니다.
앞서 살펴보았듯, 카프카는 컨슈머 그룹을 지원합니다. 카프카는 많은 컨슈머가 상호 간섭 없이 어떠한 메시지 스트림도 읽을 수 있도록 설계되었습니다.
이것은 하나의 메시지를 하나의 클라이언트에서만 소비할 수 있도록 되어있는 많은 큐 시스템과의 결정적인 차이점이기도 합니다.
카프카는 다중 컨슈머를 처리할 수 있을 뿐만 아니라 메시지를 지속성 있게 저장할 수도 있습니다.
이는 컨슈머들이 항상 실시간으로 데이터를 읽어올 필요가 없다는 의미입니다. 메시지는 정해진 보존 규칙과 함께 저장되어, 컨슈머가 필요로 할때 소비할 수 있습니다.
카프카는 유연한 확장성을 가지고 있기 때문에 어떠한 크기의 데이터셋도 쉽게 처리할 수 있습니다. 클러스터 전체의 가용성에 영향을 주지 않으면서 확장이 가능합니다.
카프카가 고부하 아래에서도 높은 성능을 보여주는 발행/구독 메시지 전달 시스템이 될 수 있었던 이유는 위 모든 특징들 덕분입니다. 발행된 메시지가 컨슈머에게 전달될 때까지 1초 미만의 시간이 걸리면서도 규모가 매우 큰 메시지 스트림을 쉽게 다룰 수 있고 수평 확장이 가능합니다.
카프카 생태계는 카프카 커텍트, 카프카 스트림즈처럼 여러 상황에 걸맞은 다양한 API 및 라이브러리를 지원합니다. 이는 개발 속도를 향상시키고 개발자의 직접 구현으로 인한 버그 발생 가능성을 줄이는데 기여할 수 있습니다.