가장 큰 그림부터 보겠습니다. 카프카는 단일 서버로 동작하기보다는, 여러 대의 서버(컴퓨터)를 하나로 묶어 거대한 단일 시스템처럼 동작시키는 것을 전제로 설계되었습니다.
여러 대의 브로커(서버)를 클러스터로 묶어 하나의 시스템처럼 동작하게 합니다.
만약 브로커 1에 장애가 발생해도, 브로커 2, 3이 계속 서비스를 이어가 고가용성(High Availability)을 확보합니다. 보통 3대 이상의 홀수로 구성하는데, 이는 클러스터 내에서 의사결정을 할 때 과반수 투표와 같은 합의 알고리즘을 안정적으로 사용하기 위함입니다.
여러 브로커가 한 팀처럼 움직이려면, 누군가는 "어떤 브로커가 살아있는지", "각 토픽의 리더는 누구인지"와 같은 중요한 정보(메타데이터)를 총괄 관리하고 조정하는 '반장' 역할을 해야 합니다. 이 역할을 하는 것이 바로 '코디네이터'이며, 과거에는 '주키퍼'가, 현재는 'KRaft'가 그 역할을 수행합니다.
과거 카프카는 클러스터의 메타데이터 관리와 리더 선출 등 핵심적인 조정 작업을 위해 '주키퍼'라는 별도의 분산 코디네이션 시스템에 의존했습니다. 주키퍼가 수행하는 주요 역할은 다음과 같습니다.
단점 (Pain Point): 카프카를 운영하려면 주키퍼 클러스터도 따로 설치하고 관리해야 합니다. 관리 포인트가 두 배가 되는 셈이라 아키텍처가 복잡하고 운영 부담이 컸습니다.
최신 카프카는 Raft 합의 프로토콜을 내장하여, 주키퍼 없이 자체적으로 클러스터를 관리할 수 있는 KRaft 모드를 지원합니다. 이 모드에서는 브로커 중 일부가 '컨트롤러' 역할을 맡아 주키퍼가 하던 모든 일을 직접 처리합니다.
결론: KRaft 모드는 운영 복잡성을 줄이고 성능을 향상시키므로, 현재 새로 카프카를 도입한다면 KRaft 모드를 사용하는 것이 표준으로 자리 잡고 있습니다.
그렇다면 개발자, 즉 애플리케이션의 입장에서 이 복잡한 클러스터는 어떻게 보일까요? 다행히도 우리는 클러스터 내부의 모든 브로커를 일일이 알 필요 없이, '대표 연락처'에 해당하는 bootstrap.servers 목록만으로 전체 클러스터와 통신을 시작할 수 있습니다.
bootstrap.servers="b1,b2,b3"
bootstrap.servers 목록에 있는 브로커 중 하나에 접속하여 클러스터 전체의 메타데이터(e.g., 'order' 토픽의 파티션 리더는 'Broker 2'라는 정보)를 요청합니다.카프카 클러스터라는 거대한 시스템 위에서, 우리가 다루는 데이터는 그냥 뒤죽박죽 저장되는 것이 아니라, 명확한 계층 구조를 가지고 체계적으로 관리됩니다. 이 구조를 이해하는 것이 카프카를 제대로 사용하는 첫걸음입니다.
order 토픽에, '사용자 활동' 데이터는 user-activity 토픽에 저장하는 식입니다. 데이터베이스의 '테이블'과 유사한 개념으로 볼 수 있습니다.여러 독자(컨슈머)가 동시에 다른 권(파티션)을 읽을 수 있으므로, 전집을 훨씬 빨리 읽을 수 있습니다. 이것이 카프카의 병렬 처리 원리입니다.
프로듀서 코드에서 send() 메소드를 호출하는 단 한 줄의 코드 이면에서는, 데이터의 안정성과 효율성을 보장하기 위한 정교한 통신 과정이 일어납니다. 이 흐름을 이해하면 카프카의 동작 방식을 깊이 있게 파악할 수 있습니다.
이러한 복제 과정 덕분에 리더 브로커에 장애가 발생해도 팔로워에 데이터가 남아있어 데이터 유실을 방지할 수 있습니다.
💡 모범 답안 예시
"토픽의 파티션 개수는 예상 처리량(Throughput)과 컨슈머의 병렬 처리 수준을 핵심적으로 고려하여 결정해야 합니다.
먼저, 파티션은 한번 늘리면 메시지 키의 순서 보장 문제 때문에 줄일 수 없으므로, 처음에는 보수적으로 접근하는 것이 안전합니다. 예를 들어, 클러스터의 브로커 수와 비슷하게 설정하거나, 예상되는 최대 컨슈머 수에 맞춰 설정하고, 이후 모니터링을 통해 처리량이 부족하다고 판단될 때 점진적으로 늘려나가는 것이 좋습니다.
또한, 하나의 파티션은 한 컨슈머 그룹 내에서 단 하나의 컨슈머만 소비할 수 있으므로, 파티션의 개수가 곧 해당 토픽을 처리할 수 있는 최대 병렬 처리의 한계가 됩니다. 따라서 미래에 컨슈머를 스케일 아웃할 가능성까지 고려하여 초기 파티션 수를 설계해야 합니다."
위와 같이 대답하려면 아래의 몇 가지 핵심 원리를 이해해야 합니다.
이는 하나의 토픽에 대한 파티션을 의미합니다. 예를 들어, 3대의 브로커로 구성된 클러스터에 'order' 토픽을 생성할 때 파티션을 3개로 만들면, 카프카는 각 브로커에 파티션을 하나씩 분산시켜(e.g., P0→B1, P1→B2, P2→B3) 부하를 균등하게 만듭니다. 이것이 '브로커당 파티션 1개' 상태입니다. "작게 시작하라"는 것은 처음부터 불필요하게 많은 파티션을 만들지 말라는 의미입니다.
결론부터 말하면 메시지 키(Key)의 순서 보장 때문입니다. 카프카는 동일한 키를 가진 메시지가 항상 동일한 파티션에 저장되도록 보장합니다. 이때 사용하는 공식이 hash(key) % numPartitions 입니다.
hash("userA") % 4 = 1. userA의 메시지는 1번 파티션으로 갑니다.hash("userA") % 2 = 1 (우연히 같을 수도 있지만) 또는 hash("userB") % 4 = 3 이었는데 hash("userB") % 2 = 1 로 바뀌는 등 목적지 파티션이 달라집니다.이렇게 되면 동일한 키의 메시지들이 여러 파티션에 흩어져 순서가 완전히 꼬이게 됩니다. 이 데이터 정합성 문제를 막기 위해 카프카는 파티션 수를 줄이는 것을 원천적으로 금지합니다.
하나의 파티션은 컨슈머 그룹 내에서 오직 하나의 컨슈머만 점유할 수 있습니다. 만약 파티션이 3개인데 컨슈머를 5개 띄우면, 3개의 컨슈머만 파티션에 할당되고 나머지 2개는 아무 일도 하지 않고 놀게 됩니다. 따라서 최대 병렬 처리량은 파티션 개수에 의해 제한됩니다.
'Dead Letter'나 'Read Repair'와 같은 개념은 심화 과정에서 자세히 다룰 내용입니다. 하지만 현재 단계에서 카프카가 이러한 문제들을 어떻게 해결하는지 미리 알아두면 전체적인 그림을 이해하는 데 큰 도움이 됩니다.
컨슈머가 특정 메시지를 계속해서 처리하지 못하고 재시도만 반복하는 상황이 발생하면, 전체 시스템의 지연을 유발할 수 있습니다. 이러한 '독이 든 메시지(Poison Pill)'를 처리하기 위한 패턴이 바로 Dead Letter Topic (DLT)입니다.
※ 이 내용은 [4. 컨슈머 심화], [6. 운영과 트러블슈팅]에서 실제로 구현하는 방법을 자세히 다룰 예정입니다.
여러 서버에 데이터를 복제할 때 발생할 수 있는 데이터 불일치 문제를 해결하기 위해 'Read Repair'와 같은 방식이 사용되지만, 카프카는 더 효율적인 접근 방식을 사용합니다.
결론적으로, 컨슈머는 항상 일관성이 보장된 리더로부터 데이터를 읽기 때문에, 읽기 시점에 데이터를 비교하고 수정할 필요가 없습니다.
※ 이 내용은 [5. 브로커와 복제]에서 ISR과 리더 선출을 학습하면 완벽하게 이해할 수 있습니다.