본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 4. 9. · 0 Views
Kafka 설치 및 환경 설정 완벽 가이드
Apache Kafka의 설치부터 환경 설정까지 단계별로 안내합니다. JDK 설치, ZooKeeper 구성, Kafka 브로커 설정, Docker Compose 활용, KRaft 모드까지 실무에 필요한 모든 환경 구축 방법을 다룹니다.
목차
- JDK_설치_및_환경_변수_설정
- ZooKeeper_설치_및_설정
- Kafka_브로커_설치
- server.properties_핵심_설정
- Kafka_서비스_시작_및_종료
- Docker_Compose로_카프카_실행하기
- KRaft_모드로_ZooKeeper_없이_실행
1. JDK 설치 및 환경 변수 설정
김개발 씨는 어제 팀 미팅에서 "다음 프로젝트부터 Kafka를 도입하자"는 결정을 들었습니다. 당장 자신의 개발 환경에 Kafka를 설치해야 한다는 사실을 깨달았지만, 막상 어디서부터 시작해야 할지 막막했습니다.
선배 박시니어 씨에게 조심스럽게 물어보았습니다. "선배님, Kafka 설치하려면 뭘 먼저 해야 하나요?"
Kafka는 Java로 작성된 플랫폼이므로, 가장 먼저 **JDK(Java Development Kit)**를 설치해야 합니다. 마치 한글 소설을 읽으려면 한글을 먼저 알아야 하는 것과 같습니다.
JDK는 Kafka가 정상적으로 동작하는 데 필수적인 런타임 환경을 제공합니다.
다음 코드를 살펴봅시다.
# Ubuntu/Debian에서 OpenJDK 17 설치
sudo apt update
sudo apt install -y openjdk-17-jdk
# 설치 확인
java -version
# openjdk version "17.0.x" 출력
# 환경 변수 설정 (~/.bashrc 또는 ~/.zshrc에 추가)
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
# 설정 적용
source ~/.bashrc
# JAVA_HOME 확인
echo $JAVA_HOME
# /usr/lib/jvm/java-17-openjdk-amd64 출력
"Apache Kafka 완전 정복" 코스의 세 번째 시간입니다. 지난 시간에는 Kafka가 어떤 도구이고, 왜 기존 메시지 브로커보다 뛰어난 성능을 보여주는지 알아보았습니다.
이제 직접 Kafka를 설치하고 실행해볼 차례입니다. 그 첫걸음이 바로 JDK 설치입니다.
김개발 씨는 회사 노트북에 이미 몇 가지 프로그래밍 언어가 설치되어 있었습니다. 하지만 자바는 처음이라 낯설게 느껴졌습니다.
"선배님, 저 자바 안 해봤는데 괜찮나요?" 박시니어 씨는 빙그레 웃으며 대답했습니다. "걱정 마요.
Kafka를 쓰기 위해 자바 코드를 직접 작성할 필요는 없어요. JDK는 그저 Kafka가 돌아가는 바닥이 될 뿐이니까요." 그렇다면 왜 하필 JDK일까요?
쉽게 비유하자면, JDK는 Kafka라는 자동차가 달릴 수 있는 도로를 까는 작업과 같습니다. 아무리 좋은 자동차가 있어도 도로가 없으면 출발할 수 없습니다.
Kafka의 핵심 서버인 브로커는 Scala와 Java로 작성되어 있고, JVM(Java Virtual Machine) 위에서 실행됩니다. 따라서 반드시 Java 런타임이 필요합니다.
어떤 버전을 설치해야 할까요? Kafka 3.x 버전부터는 JDK 11 이상을 요구합니다.
하지만 최신 LTS(Long Term Support) 버전인 JDK 17을 설치하는 것을 권장합니다. LTS 버전은 장기간 지원이 보장되므로, 프로덕션 환경에서도 안심하고 사용할 수 있습니다.
김개발 씨의 팀에서도 JDK 17을 표준으로 사용하고 있었습니다. 환경 변수 설정은 왜 필요할까요?
JAVA_HOME은 시스템 전체에서 Java 설치 경로를 가리키는 표준 변수입니다. Kafka의 시작 스크립트는 이 변수를 참조해서 올바른 JVM을 찾습니다.
만약 JAVA_HOME이 설정되어 있지 않다면, Kafka는 "java를 찾을 수 없다"는 에러를 내며 실행되지 않습니다. 마치 GPS가 목적지 주소를 모르면 길을 안내할 수 없는 것과 같습니다.
PATH에도 $JAVA_HOME/bin을 추가해야 합니다. 이렇게 하면 터미널 어디에서든 java와 javac 명령어를 사용할 수 있습니다.
김개발 씨는 환경 변수를 설정한 뒤 source ~/.bashrc로 즉시 반영하는 것을 잊지 않았습니다. 초보자들이 흔히 하는 실수 중 하나가, 설정 파일을 수정하고 터미널을 재시작하지 않는 것입니다.
설치가 잘 되었는지 확인하는 방법은 간단합니다. java -version을 입력하면 설치된 JDK 버전이 출력됩니다.
만약 "command not found"라는 메시지가 나온다면 PATH 설정이 잘못된 것입니다. 이때는 which java 명령어로 Java가 실제로 어디에 설치되어 있는지 확인하고, JAVA_HOME을 그 경로에 맞게 수정하면 됩니다.
macOS를 사용하는 개발자라면 Homebrew로 설치할 수도 있습니다. brew install openjdk@17 명령어 한 줄이면 끝입니다.
다만 Homebrew로 설치한 후에는 /usr/libexec/java_home -v 17 명령어로 정확한 경로를 확인하고, 그 경로를 JAVA_HOME에 설정해야 합니다. JDK 설치가 완료되면 드디어 Kafka를 다운로드할 준비가 된 것입니다.
박시니어 씨가 말했습니다. "JDK만 설치하면 반이에요.
다음은 Kafka가 동작하는 데 필요한 조력자를 만나볼 차례죠."
실전 팁
💡 - Kafka 3.x 이상을 사용한다면 JDK 17(LTS)을 설치하는 것을 강력히 권장합니다
- JAVA_HOME 설정 후에는 반드시
source ~/.bashrc로 설정을 즉시 반영하세요 - 여러 버전의 JDK가 설치되어 있다면
update-alternatives --config java로 기본 버전을 선택할 수 있습니다
2. ZooKeeper 설치 및 설정
JDK 설치를 마친 김개발 씨는 Kafka 다운로드 페이지에서 이상한 것을 발견했습니다. Kafka와 함께 ZooKeeper라는 프로그램도 함께 다운로드하라는 안내가 있었습니다.
"ZooKeeper가 뭔가요? 동물원 관리 프로그램인가요?" 김개발 씨의 혼잣말에 박시니어 씨가 웃으며 설명을 시작했습니다.
ZooKeeper는 분산 시스템에서 코디네이션(조율) 역할을 담당하는 서비스입니다. 마치 대형 콘서트장의 진행자가 출연자들의 순서와 위치를 관리하듯, Kafka 브로커들의 상태를 추적하고 관리합니다.
ZooKeeper 없이는 여러 대의 Kafka 브로커가 서로를 인식하고 협력할 수 없습니다.
다음 코드를 살펴봅시다.
# Kafka 다운로드 (ZooKeeper 포함)
wget https://archive.apache.org/dist/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# 압축 해제
tar -xzf kafka_2.13-3.6.1.tgz
cd kafka_2.13-3.6.1
# ZooKeeper 설정 파일 확인
# config/zookeeper.properties
# ZooKeeper 데이터 저장 디렉토리 생성
mkdir -p /tmp/zookeeper
# ZooKeeper 시작
bin/zookeeper-server-start.sh config/zookeeper.properties
# ZooKeeper가 정상적으로 실행 중인지 확인
# "binding to port 0.0.0.0/0.0.0.0:2181" 로그 확인
JDK 설치가 끝난 김개발 씨는 드디어 Kafka를 설치하려던 참이었습니다. 그런데 Kafka 다운로드 페이지에서 ZooKeeper라는 낯선 이름을 마주했습니다.
왜 Kafka만 설치하면 안 될까요? 이유를 이해하려면 분산 시스템의 본질을 알아야 합니다.
Kafka는 보통 한 대의 서버가 아니라 여러 대의 브로커로 구성된 클러스터로 운영됩니다. 브로커가 한 대뿐이라면 서버가 고장 나면 서비스가 중단되겠죠.
그래서 여러 대를 두고, 한 대가 죽어도 나머지가 대신 처리할 수 있게 만듭니다. 이를 **고가용성(High Availability)**이라고 합니다.
문제는, 브로커가 여러 대가 되면 누군가 이들을 조율해야 한다는 점입니다. 어떤 브로커가 살아있고, 어떤 브로커가 리더인지, 새로운 브로커가 추가되면 어떻게 처리할지.
바로 이 역할을 ZooKeeper가 담당합니다. 쉽게 비유하자면, ZooKeeper는 대형 회사의 인사부와 같습니다.
직원(브로커)이 누가 입사했고, 누가 퇴사했고, 누가 팀장인지를 관리합니다. 직원들끼리 서로 협력하려면 인사부가 관리하는 명단이 필요하듯, Kafka 브로커들도 ZooKeeper가 관리하는 정보가 필요합니다.
ZooKeeper 설정 파일은 config/zookeeper.properties에 있습니다. 가장 중요한 설정은 dataDir입니다.
이것은 ZooKeeper가 메타데이터를 저장하는 디렉토리 경로입니다. 기본값은 /tmp/zookeeper로 설정되어 있습니다.
프로덕션 환경에서는 /var/lib/zookeeper 같은 전용 디렉토리를 사용하는 것이 좋습니다. /tmp는 시스템 재시작 시 내용이 삭제될 수 있기 때문입니다.
또한 clientPort 설정도 중요합니다. 기본값은 2181번 포트입니다.
이 포트를 통해 Kafka 브로커가 ZooKeeper에 연결합니다. 방화벽 설정 시 이 포트를 열어주어야 합니다.
ZooKeeper를 시작하는 순서를 정확히 기억해두세요. 항상 ZooKeeper를 먼저 시작하고, 그 다음 Kafka 브로커를 시작해야 합니다.
순서가 바뀌면 Kafka는 "ZooKeeper에 연결할 수 없다"며 시작에 실패합니다. 마치 식당에서 주방장(ZooKeeper)이 먼저 준비를 마쳐야 웨이터(Kafka)가 주문을 받을 수 있는 것과 같습니다.
시작 명령어는 간단합니다. bin/zookeeper-server-start.sh config/zookeeper.properties를 입력하면 됩니다.
정상적으로 시작되면 콘솔에 "binding to port 0.0.0.0/0.0.0.0:2181" 같은 로그가 출력됩니다. 이 로그를 확인하면 ZooKeeper가 2181번 포트에서 대기 중이라는 뜻입니다.
그런데 잠깐, ZooKeeper는 Kafka 전용 도구일까요? 아닙니다.
ZooKeeper는 Apache 재단의 독립 프로젝트로, Hadoop, HBase, Dubbo 등 다양한 분산 시스템에서 사용됩니다. Kafka뿐만 아니라 분산 시스템 전반에서 필요한 코디네이션 기능을 제공하는 범용 도구입니다.
다만 최근에는 Kafka 자체적으로 ZooKeeper의 역할을 대체하는 KRaft 모드가 도입되었습니다. 이에 대해서는 뒤에서 자세히 다루겠습니다.
김개발 씨는 ZooKeeper가 성공적으로 실행된 것을 확인하고 안도의 한숨을 쉬었습니다. "드디어 Kafka를 설치할 수 있겠네요!" 박시니어 씨가 고개를 끄덕였습니다.
"맞아요. ZooKeeper가 준비됐으니 이제 본 주인공을 만나볼 시간이에요."
실전 팁
💡 - ZooKeeper는 반드시 Kafka 브로커보다 먼저 시작해야 합니다
- 프로덕션 환경에서는 dataDir을 /tmp가 아닌 전용 디렉토리로 변경하세요
- ZooKeeper도 클러스터 구성이 가능합니다. 홀수 개(3, 5, 7)의 노드로 구성하는 것이 일반적입니다
3. Kafka 브로커 설치
ZooKeeper가 정상적으로 실행되고 있는 것을 확인한 김개발 씨는 드디어 Kafka 브로커를 설치할 준비가 되었습니다. 사실 앞서 Kafka를 다운로드할 때 ZooKeeper도 함께 포함되어 있었기에, 별도의 추가 설치는 필요하지 않았습니다.
"선배님, Kafka는 이미 다운로드한 거 맞나요?" "맞아요. Kafka 패키지 안에 브로커 실행 파일도 모두 포함되어 있어요."
Kafka 브로커는 메시지를 수신, 저장, 전달하는 핵심 서버입니다. 마치 우체국이 편지를 받아 분류하고 배달하는 것처럼, 브로커는 프로듀서가 보낸 메시지를 토픽에 저장하고 컨슈머가 가져갈 수 있게 대기시킵니다.
브로커는 Kafka 시스템의 실질적인 일꾼입니다.
다음 코드를 살펴봅시다.
# Kafka 홈 디렉토리 이동
cd /home/dev-user/kafka_2.13-3.6.1
# Kafka 설정 파일 확인
# config/server.properties
# Kafka 로그 저장 디렉토리 생성
mkdir -p /tmp/kafka-logs
# Kafka 브로커 시작
bin/kafka-server-start.sh config/server.properties
# 정상 실행 확인
# "Kafka Server started" 로그 확인
# 다른 터미널에서 Kafka가 실행 중인지 포트 확인
netstat -tlnp | grep 9092
# tcp 0 0.0.0.0:9092 LISTEN 출력
이제 본격적으로 Kafka 브로커를 실행해봅시다. 앞서 다운로드한 Kafka 패키지에는 브로커 실행에 필요한 모든 파일이 포함되어 있습니다.
별도로 추가로 다운로드할 것은 없습니다. 다만 올바르게 설정하고 실행하는 과정을 이해하는 것이 중요합니다.
Kafka 브로커의 핵심 역할을 다시 한번 정리해보겠습니다. 프로듀서가 메시지를 보내면 브로커가 이를 받아 토픽에 저장합니다.
그리고 컨슈머가 메시지를 요청하면 브로커가 저장된 메시지를 전달합니다. 이 과정에서 브로커는 메시지를 디스크에 영속적으로 저장하므로, 시스템이 재시작되어도 메시지가 사라지지 않습니다.
이것이 카프카가 기존 메시지 큐와 차별화되는 중요한 특징 중 하나입니다. 브로커를 시작하기 전에 server.properties 설정 파일을 살펴봅시다.
이 파일은 Kafka 브로커의 동작을 결정하는 핵심 설정 파일입니다. 주요 설정 항목은 다음 카드에서 자세히 다루겠지만, 기본적으로 broker.id, log.dirs, zookeeper.connect 정도만 이해하면 브로커를 실행할 수 있습니다.
broker.id는 클러스터 내에서 이 브로커를 식별하는 고유 번호입니다. 기본값은 0입니다.
여러 대의 브로커를 실행할 때는 각각 다른 ID를 부여해야 합니다. 마치 학교에서 학생 번호가 각자 다르듯, 브로커도 고유한 식별자가 필요합니다.
log.dirs는 메시지를 저장할 디렉토리 경로입니다. 기본값은 /tmp/kafka-logs입니다.
이 디렉토리가 없으면 브로커가 시작되지 않으므로, 미리 생성해두어야 합니다. 프로덕션 환경에서는 성능을 위해 전용 디스크를 할당하는 것이 좋습니다.
SSD를 사용하면 메시지 읽기/쓰기 성능이 크게 향상됩니다. zookeeper.connect는 ZooKeeper의 주소입니다.
기본값은 localhost:2181입니다. 브로커는 시작될 때 이 주소로 ZooKeeper에 접속하여 자신을 등록합니다.
만약 ZooKeeper가 실행 중이 아니라면, 브로커는 "Connection refused" 에러와 함께 시작에 실패합니다. 시작 명령어는 bin/kafka-server-start.sh config/server.properties입니다.
정상적으로 실행되면 콘솔에 여러 로그가 쏟아지고, 마지막에 "[KafkaServer id=0] started" 같은 메시지가 나타납니다. 기본적으로 브로커는 9092번 포트에서 클라이언트 연결을 대기합니다.
netstat이나 ss 명령어로 이 포트가 LISTEN 상태인지 확인할 수 있습니다. 브로커를 종료하려면 다른 터미널에서 bin/kafka-server-stop.sh를 실행합니다.
또는 브로커가 실행 중인 터미널에서 Ctrl+C를 눌러도 됩니다. 하지만 프로덕션 환경에서는 강제 종료 대신 정상 종료 스크립트를 사용하는 것이 안전합니다.
정상 종료 시 브로커는 진행 중인 작업을 마무리하고 저장소를 깔끔하게 정리합니다. 김개발 씨는 첫 번째 Kafka 브로커를 성공적으로 실행했습니다.
콘솔에 쏟아지는 로그를 보며 감회가 새로웠습니다. "드디어 내 컴퓨터에서 Kafka가 돌아가는군요!" 하지만 박시니어 씨는 "지금은 기본 설정으로 실행한 것뿐이에요.
실무에서는 설정 파일을 꼼꼼하게 다뤄야 해요"라고 덧붙였습니다.
실전 팁
💡 - 브로커를 시작하기 전에 반드시 ZooKeeper가 실행 중인지 확인하세요
- 여러 브로커를 실행할 때는 broker.id를 각각 다르게 설정해야 합니다
- 프로덕션 환경에서는 log.dirs를 SSD 전용 디스크로 설정하면 성능이 크게 향상됩니다
4. server.properties 핵심 설정
김개발 씨는 Kafka 브로커를 성공적으로 실행했지만, 설정 파일의 대부분이 주석 처리되어 있어 어떤 항목을 수정해야 할지 몰랐습니다. 박시니어 씨가 config/server.properties 파일을 열며 말했습니다.
"이 파일이 Kafka의 두뇌예요. 핵심 설정 몇 가지만 이해하면 나머지는 문서를 참고하면 돼요."
server.properties는 Kafka 브로커의 동작을 제어하는 핵심 설정 파일입니다. 브로커 ID, 포트, 로그 저장 경로, ZooKeeper 연결 정보, 복제 수, 보존 정책 등 모든 운영 파라미터를 이 파일에서 관리합니다.
마치 자동차의 설정 패널처럼, 이 파일 하나로 브로커의 모든 동작을 결정할 수 있습니다.
다음 코드를 살펴봅시다.
# config/server.properties - 핵심 설정 예시
# 브로커 고유 ID (클러스터 내에서 유일해야 함)
broker.id=0
# 클라이언트 연결을 받을 포트
listeners=PLAINTEXT://:9092
# ZooKeeper 연결 정보
zookeeper.connect=localhost:2181
# 메시지 로그 저장 경로
log.dirs=/var/lib/kafka/logs
# 토픽의 기본 복제 수 (프로덕션: 3 권장)
default.replication.factor=1
# 메시지 보존 시간 (168시간 = 7일)
log.retention.hours=168
# 로그 세그먼트 최대 크기 (1GB)
log.segment.bytes=1073741824
설정 파일을 열어보면 수많은 항목이 주석 처리되어 있습니다. 처음에는 압도당할 수 있지만, 핵심 설정 몇 가지만 이해하면 나머지는 필요할 때 찾아보면 됩니다.
박시니어 씨가 하나씩 설명해주기로 했습니다. 먼저 broker.id입니다.
이것은 클러스터 내에서 이 브로커를 식별하는 고유 번호입니다. 정수 값을 가지며, 클러스터 내에서 중복되면 안 됩니다.
브로커가 여러 대라면 0, 1, 2처럼 각각 다른 번호를 부여합니다. 마치 주민등록번호가 사람을 고유하게 식별하듯, broker.id도 브로커를 고유하게 식별합니다.
이 ID는 ZooKeeper에 등록되고, 브로커가 재시작되어도 동일한 ID를 유지해야 클러스터 구성이 깨지지 않습니다. 다음으로 listeners 설정입니다.
이것은 브로커가 클라이언트 연결을 받을 프로토콜과 포트를 지정합니다. 기본값인 PLAINTEXT://:9092는 암호화되지 않은 일반 연결을 9092번 포트에서 받겠다는 뜻입니다.
프로덕션 환경에서는 보안을 위해 SASL_SSL://:9092로 설정하여 인증과 암호화를 적용하는 것이 좋습니다. 외부에서 접근해야 한다면 advertised.listeners 설정도 함께 변경해야 합니다.
zookeeper.connect는 이미 앞서 설명한 ZooKeeper 주소입니다. 기본값인 localhost:2181은 로컬 개발 환경에서 적합합니다.
하지만 ZooKeeper가 다른 서버에 있다면 zk1.example.com:2181,zk2.example.com:2181,zk3.example.com:2181처럼 쉼표로 구분하여 여러 주소를 지정할 수 있습니다. 이렇게 하면 ZooKeeper 클러스터에 연결되어, ZooKeeper 노드 중 일부가 다운되어도 서비스가 유지됩니다.
log.dirs는 메시지가 실제로 저장되는 디스크 경로입니다. 주의할 점은, 여기서 말하는 "log"는 애플리케이션 로그가 아니라 메시지 데이터 로그를 의미합니다.
Kafka는 수신한 메시지를 파일 형태로 디스크에 저장하는데, 이 저장 위치가 바로 log.dirs입니다. 여러 디렉토리를 쉼표로 구분하여 지정할 수도 있는데, 이 경우 Kafka가 디렉토리 간에 데이터를 균형 있게 분산시킵니다.
예를 들어 log.dirs=/disk1/kafka,/disk2/kafka처럼 여러 디스크를 지정하면 I/O 성능이 향상됩니다. default.replication.factor는 토픽 생성 시 기본 복제 수를 결정합니다.
기본값은 1이지만, 프로덕션 환경에서는 반드시 3 이상으로 설정해야 합니다. 복제 수가 3이면 메시지가 3개의 브로커에 각각 복사됩니다.
그중 한 대가 고장 나도 나머지 두 대가 메시지를 안전하게 보관하고 있으므로 데이터 손실이 발생하지 않습니다. 이를 **내결함성(Fault Tolerance)**이라고 합니다.
log.retention.hours는 메시지 보존 기간을 지정합니다. 기본값은 168시간, 즉 7일입니다.
이 기간이 지난 메시지는 자동으로 삭제됩니다. 쇼핑몰의 주문 이벤트처럼 7일만 보관하면 되는 데이터에는 적합하지만, 금융 거래 내역처럼 장기 보관이 필요한 데이터에는 더 긴 기간을 설정해야 합니다.
log.retention.bytes로 크기 기준으로도 삭제 시점을 지정할 수 있습니다. log.segment.bytes는 로그 세그먼트의 최대 크기를 결정합니다.
Kafka는 메시지를 하나의 거대한 파일에 저장하는 것이 아니라, 정해진 크기의 세그먼트 단위로 나누어 저장합니다. 기본값은 1GB입니다.
세그먼트가 꽉 차면 새 세그먼트가 생성되고, 오래된 세그먼트는 보존 기간이 지나면 삭제됩니다. 이 설계 덕분에 Kafka는 대용량 데이터도 효율적으로 관리할 수 있습니다.
김개발 씨는 설정 파일의 각 항목이 어떤 역할을 하는지 이해하고 나니, 이전의 두려움이 사라졌습니다. "핵심만 알면 그렇게 복잡하지 않네요." 박시니어 씨가 만족스러운 표정으로 말했습니다.
"맞아요. 기본에 충실하면 응용은 자연스럽게 따라와요."
실전 팁
💡 - 프로덕션 환경에서는 default.replication.factor를 3 이상으로 설정하세요
- 로그 보존 정책은 비즈니스 요구사항에 맞게 log.retention.hours와 log.retention.bytes를 함께 조정하세요
- 보안 설정은 listeners를 PLAINTEXT 대신 SASL_SSL로 변경하는 것부터 시작하세요
5. Kafka 서비스 시작 및 종료
설정 파일까지 마무리한 김개발 씨는 이제 Kafka를 직접 실행해보려고 합니다. 하지만 서버를 시작하고 종료하는 방법도 제대로 알아야겠죠.
"그냥 명령어 치면 되는 거 아닌가요?" 김개발 씨의 질문에 박시니어 씨는 진지한 표정으로 대답했습니다. "시작하는 것보다 종료하는 것이 더 중요해요.
데이터 손실을 막으려면 정상 종료 방법을 반드시 알아야 합니다."
Kafka 서비스는 시작 순서와 종료 방법이 매우 중요합니다. ZooKeeper를 먼저 시작하고 Kafka 브로커를 나중에 시작해야 하며, 종료할 때는 브로커를 먼저 종료하고 ZooKeeper를 나중에 종료해야 합니다.
이 순서를 지키지 않으면 데이터 일관성 문제가 발생할 수 있습니다.
다음 코드를 살펴봅시다.
# 1. ZooKeeper 시작 (데몬 모드)
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
# 2. Kafka 브로커 시작 (데몬 모드)
bin/kafka-server-start.sh -daemon config/server.properties
# 3. 실행 상태 확인
jps
# 출력 예시:
# 12345 QuorumPeerMain (ZooKeeper)
# 12346 Kafka (Broker)
# 4. 토픽 생성 테스트
bin/kafka-topics.sh --create \
--topic test-topic \
--bootstrap-server localhost:9092 \
--partitions 1 --replication-factor 1
# 5. 브로커 정상 종료
bin/kafka-server-stop.sh
# 6. ZooKeeper 정상 종료
bin/zookeeper-server-stop.sh
지금까지 JDK를 설치하고, ZooKeeper를 설정하고, Kafka 브로커를 구성하는 방법을 알아보았습니다. 이제 실제로 서비스를 시작하고 종료하는 전체 흐름을 정리해봅시다.
가장 먼저 기억해야 할 것은 시작 순서입니다. 항상 ZooKeeper 먼저, Kafka 브로커 나중입니다.
이 순서를 거꾸로 하면 브로커는 ZooKeeper에 연결할 수 없어 시작에 실패합니다. 쉽게 비유하자면, 백화점을 열 때 먼저 보안팀(ZooKeeper)이 자리를 잡고, 그다음 점포(Kafka 브로커)가 문을 여는 것과 같습니다.
보안팀이 없으면 점포의 동선을 관리할 수 없겠죠. 앞서 ZooKeeper와 브로커를 각각 bin/zookeeper-server-start.sh와 bin/kafka-server-start.sh로 실행해보았습니다.
하지만 이 명령어는 터미널을 점유합니다. 터미널을 닫으면 서비스도 함께 종료됩니다.
실무에서는 데몬 모드로 실행하는 것이 일반적입니다. 데몬 모드는 서비스를 백그라운드에서 실행하는 방식입니다.
명령어에 -daemon 옵션을 추가하면 됩니다. bin/zookeeper-server-start.sh -daemon config/zookeeper.properties처럼 실행하면, ZooKeeper가 백그라운드에서 실행되고 터미널은 다시 사용할 수 있습니다.
서비스가 잘 실행되었는지 어떻게 확인할까요? 가장 간단한 방법은 jps 명령어를 사용하는 것입니다.
jps는 실행 중인 Java 프로세스를 보여주는 도구입니다. ZooKeeper가 실행 중이면 QuorumPeerMain이라는 이름으로, Kafka 브로커가 실행 중이면 Kafka라는 이름으로 표시됩니다.
두 프로세스가 모두 보이면 정상적으로 실행된 것입니다. 더 확실한 확인 방법은 토픽을 생성해보는 것입니다.
bin/kafka-topics.sh --create --topic test-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 명령어로 간단한 토픽을 만들어봅시다. "Created topic test-topic."이라는 메시지가 나오면 브로커가 정상적으로 동작 중인 것입니다.
토픽 생성에 실패한다면 브로커가 제대로 시작되지 않은 것이니 로그를 확인해야 합니다. 이제 종료 방법을 알아봅시다.
종료 순서는 시작 순서의 정반대입니다. 먼저 Kafka 브로커를 종료하고, 그다음 ZooKeeper를 종료합니다.
브로커가 ZooKeeper보다 먼저 종료되어야 브로커가 ZooKeeper와 정상적으로 연결을 끊고 자신의 상태를 정리할 수 있습니다. 만약 ZooKeeper가 먼저 종료되면 브로커는 코디네이터를 잃어버리게 되고, 정상적인 종료가 어려워집니다.
정상 종료 스크립트는 각각 bin/kafka-server-stop.sh와 bin/zookeeper-server-stop.sh입니다. 이 스크립트를 실행하면 서비스가 graceful shutdown을 수행합니다.
진행 중인 메시지 처리를 완료하고, 저장소를 플러시하고, 클러스터에서 자신을 정상적으로 탈퇴시킵니다. 반면 kill -9로 강제 종료하면 어떻게 될까요?
진행 중인 작업이 중간에 끊기고, 디스크에 쓰이지 않은 데이터가 손실될 수 있습니다. 또한 ZooKeeper에 브로커의 상태가 "정상 종료"가 아닌 "비정상 종료"로 기록됩니다.
클러스터의 다른 브로커들은 이 브로커가 언제 다시 돌아올지 모르기 때문에 불필요한 리밸런싱을 수행할 수 있습니다. 따라서 프로덕션 환경에서는 절대 kill -9를 사용하면 안 됩니다.
김개발 씨는 시작과 종료의 순서를 노트에 꼼꼼히 적어두었습니다. "시작은 ZK 먼저, 종료는 Broker 먼저." 박시니어 씨가 고개를 끄덕이며 덧붙였습니다.
"이 원칙만 기억하면 큰 실수는 하지 않을 거예요. 하지만 매번 이렇게 수동으로 실행하는 건 귀찮죠.
다음에는 Docker로 훨씬 쉽게 해보겠습니다."
실전 팁
💡 - 데몬 모드(-daemon)로 실행하면 백그라운드에서 서비스가 유지되어 터미널을 자유롭게 사용할 수 있습니다
- jps 명령어로 ZooKeeper(QuorumPeerMain)와 Kafka 브로커(Kafka)가 실행 중인지 항상 확인하세요
- 프로덕션 환경에서는 절대 kill -9로 강제 종료하지 마세요. 반드시 stop 스크립트를 사용하세요
6. Docker Compose로 카프카 실행하기
김개발 씨는 수동으로 ZooKeeper와 Kafka를 각각 실행하고 종료하는 과정이 번거롭게 느껴졌습니다. "매번 이렇게 하나씩 실행해야 하나요?" 박시니어 씨가 미소를 지으며 대답했습니다.
"요즘은 다들 Docker를 쓰죠. docker-compose 명령어 하나면 ZooKeeper와 Kafka가 동시에 실행돼요."
Docker Compose를 사용하면 ZooKeeper와 Kafka 브로커를 하나의 명령어로 함께 실행할 수 있습니다. 마치 레고 블록으로 조립하듯, 설정 파일에 필요한 서비스들을 정의해두면 Docker가 알아서 컨테이너를 생성하고 실행합니다.
개발 환경 구축의 복잡도를 크게 줄여주는 현대적인 방식입니다.
다음 코드를 살펴봅시다.
# docker-compose.yml
version: '3.8'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
kafka:
image: confluentinc/cp-kafka:7.5.0
container_name: kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
# 실행 명령어
# docker-compose up -d
# 종료 명령어
# docker-compose down
Docker를 사용하면 운영체제에 JDK를 직접 설치할 필요가 없습니다. 컨테이너 안에 모든 것이 포함되어 있기 때문입니다.
김개발 씨는 Docker의 이런 편리함에 크게 감탄했습니다. Docker Compose의 핵심은 YAML 파일에 서비스를 정의하는 것입니다.
먼저 version은 Compose 파일의 버전을 지정합니다. '3.8'은 널리 사용되는 안정적인 버전입니다.
services 아래에 실행할 컨테이너들을 나열합니다. ZooKeeper 서비스를 살펴봅시다.
image는 사용할 Docker 이미지를 지정합니다. 여기서는 Confluent에서 제공하는 공식 이미지를 사용합니다.
Confluent는 Kafka의 창시자들이 설립한 회사로, Kafka 관련 도구의 신뢰할 수 있는 이미지를 제공합니다. container_name은 컨테이너의 이름을 지정하여 관리를 쉽게 만듭니다.
environment 섹션에서 ZooKeeper의 설정을 환경 변수로 전달합니다. ZOOKEEPER_CLIENT_PORT: 2181은 클라이언트 연결 포트를 지정합니다.
ZOOKEEPER_TICK_TIME: 2000은 ZooKeeper가 하트비트를 확인하는 간격(밀리초)입니다. 틱 타임이 짧으면 장애 감지가 빨라지지만, 네트워크 지연으로 인해 오인식할 가능성도 높아집니다.
Kafka 서비스가 더 흥미롭습니다. depends_on: zookeeper는 시작 순서를 보장합니다.
이 설정이 있어야 Docker가 ZooKeeper 컨테이너를 먼저 시작하고, ZooKeeper가 준비된 후에 Kafka 컨테이너를 시작합니다. 앞서 수동으로 실행할 때 지켰던 순서를 Docker가 알아서 처리해주는 것입니다.
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181에서 주목할 점은 호스트명이 localhost가 아니라 zookeeper라는 것입니다. Docker Compose는 자체 네트워크를 생성하므로, 컨테이너 간에는 컨테이너 이름으로 통신합니다.
즉, Kafka 컨테이너에서 ZooKeeper에 접속할 때 localhost:2181이 아니라 zookeeper:2181로 접속해야 합니다. KAFKA_ADVERTISED_LISTENERS는 외부 클라이언트가 Kafka에 접속할 때 사용할 주소입니다.
PLAINTEXT://localhost:9092로 설정하면 호스트 머신에서 localhost:9092로 Kafka에 접속할 수 있습니다. 만약 이 설정이 잘못되면 클라이언트가 "Connection refused" 에러를 만나게 됩니다.
Docker 환경에서 가장 흔히 겪는 문제 중 하나입니다. KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1은 컨슈머 오프셋을 저장하는 내부 토픽의 복제 수를 1로 설정합니다.
기본값은 3이지만, 브로커가 한 대뿐인 개발 환경에서는 1로 설정해야 합니다. 그렇지 않으면 Kafka가 시작될 때 "Replication factor: 3 larger than available brokers: 1" 에러를 내며 시작에 실패합니다.
이제 실행해봅시다. docker-compose up -d 명령어 하나면 끝입니다.
-d 옵션은 백그라운드 실행을 의미합니다. Docker가 ZooKeeper 컨테이너를 먼저 시작하고, 준비되면 Kafka 컨테이너를 시작합니다.
docker-compose ps로 두 컨테이너가 모두 "Up" 상태인지 확인할 수 있습니다. 서비스를 중지할 때는 docker-compose down을 사용합니다.
모든 컨테이너가 정상적으로 종료됩니다. docker-compose down -v를 사용하면 컨테이너와 함께 **볼륨(데이터)**도 함께 삭제됩니다.
깔끔하게 초기화하고 싶을 때 유용하지만, 저장된 메시지도 모두 사라지므로 주의해야 합니다. Docker Compose는 실무에서도 널리 사용됩니다.
개발 환경뿐만 아니라, CI/CD 파이프라인의 테스트 환경에서도 Docker Compose로 Kafka를 실행합니다. 개발자 간에 docker-compose.yml 파일만 공유하면, "내 컴퓨터에서는 되는데"라는 문제를 크게 줄일 수 있습니다.
환경 불일치로 인한 디버깅 시간을 없애주는 강력한 도구입니다. 김개발 씨는 docker-compose 명령어 하나로 모든 것이 해결되는 것을 보며 감탄했습니다.
"이렇게 간단하다니, 진작 Docker를 쓸 걸 그랬네요." 하지만 박시니어 씨가 한 가지 더 소개할 것이 있다고 했습니다. "Docker 방식도 좋지만, 최신 Kafka에서는 ZooKeeper 자체를 없애는 방법도 있어요."
실전 팁
💡 - KAFKA_ADVERTISED_LISTENERS 설정이 틀리면 외부에서 Kafka에 접속할 수 없습니다. Docker 환경에서는 반드시 확인하세요
- 브로커가 한 대뿐일 때는 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR를 1로 설정해야 에러가 발생하지 않습니다
- docker-compose down -v는 데이터까지 모두 삭제하므로, 메시지를 보존하려면 docker-compose stop을 사용하세요
7. KRaft 모드로 ZooKeeper 없이 실행
"ZooKeeper 없이도 Kafka를 실행할 수 있다고요?" 김개발 씨의 눈이 커졌습니다. 지금까지 ZooKeeper가 Kafka의 필수 조력자라고 배웠는데, 이를 없앨 수 있다니 놀라웠습니다.
박시니어 씨가 설명하기 시작했습니다. "Kafka 2.8부터 도입된 KRaft 모드가 그걸 가능하게 해요.
ZooKeeper를 대체하는 Kafka 내장 코디네이션 기능이에요."
KRaft(Kafka Raft) 모드는 Kafka 내부에 코디네이션 기능을 통합하여 ZooKeeper 없이도 클러스터를 운영할 수 있게 하는 새로운 아키텍처입니다. 마치 스마트폰이 카메라, MP3 플레이어, GPS 등 여러 기기를 하나로 통합한 것처럼, Kafka가 브로커 기능과 코디네이션 기능을 모두 담당하게 됩니다.
운영 복잡도를 크게 줄여주는 Kafka의 미래입니다.
다음 코드를 살펴봅시다.
# KRaft 모드용 설정 파일 생성
# config/kraft/server.properties
# 노드 ID (클러스터 내에서 유일)
node.id=1
# 클러스터 ID 생성 (최초 1회)
# KAFKA_CLUSTER_ID=$(bin/kafka-storage.sh random-uuid)
# 컨트롤러 리스너
controller.quorum.voters=1@localhost:9093
# 브로커 리스너
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
inter.broker.listener.name=PLAINTEXT
controller.listener.names=CONTROLLER
# 로그 디렉토리
log.dirs=/tmp/kraft-combined-logs
# KRaft 모드로 스토리지 포맷
# bin/kafka-storage.sh format -t $KAFKA_CLUSTER_ID \
# -c config/kraft/server.properties
# KRaft 모드로 Kafka 시작
# bin/kafka-server-start.sh config/kraft/server.properties
ZooKeeper는 오랫동안 Kafka의 필수 동반자였습니다. 하지만 이 조합에는 고유의 문제가 있었습니다.
박시니어 씨가 그 배경을 설명해주었습니다. 가장 큰 문제는 운영 복잡도입니다.
ZooKeeper와 Kafka를 각각 설치, 설정, 모니터링, 업그레이드해야 합니다. 두 시스템의 버전 호환성도 맞춰야 합니다.
장애가 발생했을 때 어느 쪽의 문제인지 파악해야 하고, 복구도 각각 수행해야 합니다. 시스템이 두 개면 관리 포인트도 두 배가 되는 셈입니다.
또한 메타데이터 동기화 지연도 문제였습니다. Kafka의 토픽, 파티션, 리더 정보 같은 메타데이터는 ZooKeeper에 저장됩니다.
브로커는 이 정보를 ZooKeeper에서 읽어와 자신의 메모리에 캐시합니다. 하지만 클러스터가 커지면 ZooKeeper가 병목 지점이 될 수 있습니다.
수만 개의 파티션을 관리할 때 ZooKeeper의 응답 지연이 증가하고, 이는 결국 Kafka의 확장성 한계로 이어집니다. 이런 문제를 해결하기 위해 Kafka 커뮤니티는 KRaft 모드를 개발했습니다.
KRaft는 Apache Raft 합의 알고리즘을 기반으로 합니다. Raft는 분산 시스템에서 노드 간에 데이터를 일관되게 유지하기 위한 알고리즘입니다.
쉽게 말해, 여러 대의 서버가 "누가 리더인지"와 "어떤 데이터가 최신인지"에 대해 합의하는 방법입니다. KRaft 모드에서는 Kafka 브로커 중 일부가 컨트롤러 역할을 겸합니다.
컨트롤러는 이전에 ZooKeeper가 담당하던 코디네이션 작업을 수행합니다. 토픽 생성, 파티션 할당, 리더 선출 등의 메타데이터 관리를 Kafka 내부에서 처리합니다.
브로커와 컨트롤러가 동일한 프로세스에서 실행되므로, 별도의 코디네이션 서비스가 필요하지 않습니다. KRaft 모드의 설정을 살펴봅시다.
가장 먼저 node.id를 설정합니다. 이것은 이전의 broker.id와 비슷한 역할이지만, KRaft 모드에서는 브로커와 컨트롤러를 구분하지 않고 모두 "노드"로 취급합니다.
controller.quorum.voters는 컨트롤러 투표에 참여하는 노드를 지정합니다. 1@localhost:9093은 노드 ID가 1인 컨트롤러가 localhost:9093 포트에서 통신한다는 뜻입니다.
리스너 설정도 중요합니다. KRaft 모드에서는 두 가지 리스너가 필요합니다.
PLAINTEXT://:9092는 클라이언트 연결을 받는 일반 리스너이고, CONTROLLER://:9093은 컨트롤러 간 통신을 위한 리스너입니다. 컨트롤러 리스너는 클라이언트가 직접 접근하지 않으므로, 내부 통신 전용으로 분리하는 것이 좋습니다.
KRaft 모드로 시작하기 전에 한 가지 필수 단계가 있습니다. kafka-storage.sh format 명령어로 스토리지를 포맷해야 합니다.
이 과정에서 클러스터 ID가 생성되고, 로그 디렉토리에 메타데이터 파일이 초기화됩니다. 클러스터 ID는 kafka-storage.sh random-uuid로 생성할 수 있습니다.
이 ID는 클러스터에 참여하는 모든 노드가 동일해야 합니다. KRaft 모드의 도입 현황은 어떨까요?
Kafka 2.8에서 프리뷰로 처음 도입되었고, Kafka 3.3에서 프로덕션 준비 완료(Production Ready) 상태가 되었습니다. 그리고 Kafka 4.0에서는 ZooKeeper 모드가 완전히 제거될 예정입니다.
즉, ZooKeeper 모드는 점진적으로 사라지고 KRaft가 표준이 됩니다. 그렇다면 지금 당장 KRaft로 전환해야 할까요?
새로 시작하는 프로젝트라면 KRaft 모드로 시작하는 것을 권장합니다. 운영 복잡도가 크게 줄어들고, 장기적으로 ZooKeeper 유지보수 부담도 없어집니다.
하지만 기존에 ZooKeeper 모드로 운영 중인 클러스터라면, 신중하게 마이그레이션 계획을 수립해야 합니다. Kafka 3.x에서는 두 모드 간의 마이그레이션 툴이 제공됩니다.
김개발 씨는 오늘 배운 내용을 정리하며 감탄했습니다. "JDK 설치부터 KRaft 모드까지, 생각보다 할 게 많네요." 박시니어 씨가 마지막 조언을 덧붙였습니다.
"환경 설정은 한 번 제대로 하면 반복할 필요가 없어요. 다음에는 이 Kafka에 데이터를 넣고 빼는 방법을 배워봅시다.
그러려면 먼저 토픽과 파티션의 개념을 이해해야 해요."
실전 팁
💡 - 새 프로젝트를 시작한다면 KRaft 모드를 기본으로 사용하세요. ZooKeeper 없이 더 간단하게 운영할 수 있습니다
- KRaft 모드로 시작하기 전에 반드시 kafka-storage.sh format으로 스토리지를 포맷하세요
- 기존 ZooKeeper 클러스터를 KRaft로 마이그레이션하려면 Kafka 3.x의 마이그레이션 툴을 사용하고 충분한 테스트를 거치세요
- 다음 카드뉴스에서는 Kafka의 핵심 개념인 "토픽과 파티션의 이해"를 다룹니다. 메시지가 Kafka 내부에서 어떻게 분산 저장되고 관리되는지 알아보세요
- 이 카드뉴스는 "Apache Kafka 완전 정복" 코스의 3/15편입니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
토픽과 파티션의 이해 완벽 가이드
Kafka의 핵심 데이터 구조인 토픽과 파티션의 개념부터 오프셋, 메시지 순서 보장, 삭제 정책까지 실무에 필요한 모든 내용을 다룹니다. 카프카를 제대로 이해하기 위해 반드시 알아야 할 기초를 탄탄하게 다집니다.
Apache Kafka 소개 및 특징
Apache Kafka의 탄생 배경부터 핵심 설계 철학, 다른 메시지 브로커와의 비교, 높은 처리량의 원리, 생태계 구성 요소, 실제 사용 사례까지 종합적으로 다룹니다. 이벤트 스트리밍 플랫폼으로서 카프카가 왜 업계 표준이 되었는지 알아봅니다.
메시지 큐와 이벤트 스트리밍 개념 완벽 가이드
메시지 큐의 기본 개념부터 이벤트 기반 아키텍처, 스트리밍 플랫폼의 필요성까지 술술 읽히는 이북 스타일로 설명합니다. Apache Kafka를 배우기 전 반드시 알아야 할 핵심 개념들을 다룹니다.
MCP 어노테이션 기반 개발 완벽 가이드
Spring AI와 MCP(Model Context Protocol)를 활용한 어노테이션 기반 도구 개발 방법을 알아봅니다. 선언적 프로그래밍으로 AI 에이전트용 도구를 쉽게 만드는 방법을 초급자도 이해할 수 있게 설명합니다.
MCP 클라이언트 구현 완벽 가이드
Model Context Protocol 클라이언트를 Java/Spring 환경에서 구현하는 방법을 다룹니다. 서버 디스커버리부터 멀티 서버 관리까지 실무에서 바로 사용할 수 있는 패턴을 학습합니다.