The Kafka Newbie’s Migration
Kafka 뉴비의 마이그레이션, 산 넘고 물 건너 | 2024 당근 테크 밋업
Kafka 뉴비의 마이그레이션, 산 넘고 물 건너 | 2024 당근 테크 밋업
🧭 모두가 행복한 Kafka 마이그레이션 – 당근 공통서비스팀의 무중단 이행기
📌 개요: ‘서비스를 멈추지 않는’ 마이그레이션의 목표
Kafka는 비동기 이벤트 스트리밍의 핵심 인프라입니다. 그러나 조직이 커지고, 클러스터가 늘어나며, 토픽 수가 1000개를 넘어가면 운영 복잡도와 비용이 기하급수적으로 상승합니다.
당근 공통서비스팀은 이런 현실적 문제를 해결하기 위해 “심리스(Seamless) 마이그레이션”, 즉 클라이언트 수정 없이 Kafka 클러스터를 전환하는 프로젝트를 수행했습니다.
이번 글은 그 여정에서 마주친 난관과 이를 극복한 기술적 해법을 심층적으로 다룹니다.
🏗️ 1. 배경: 당근의 Kafka 운영 현황과 문제 인식
당근은 서비스 전반에서 비동기 이벤트를 처리하기 위해 Kafka를 사용합니다.
토픽 수: 약 1,100개
컨슈머 그룹 멤버 수: 2,000개
클러스터 수: 13개
일평균 메시지 트래픽: 수백만 건
이 중 **‘서비스 Kafka 클러스터’**는 전사 서비스가 공유하며, 변경이 빈번하고 의존성이 얽혀 있는 구조였습니다.
결과적으로 다음과 같은 문제가 누적되었습니다.
⚠️ 운영 문제 요약
스키마 무결성
메시지 스키마 변경 시 backward compatibility 깨짐 → 장애 발생
네이밍 혼란
토픽 이름 규칙 부재 → 출처 및 용도 추적 불가
권한 통제 부재
내부 Kafka 클라이언트면 모든 토픽 접근 가능 → 보안 취약
🎯 즉, 서비스별로 다른 클러스터로 프록시 라우팅이 가능해짐.
이를 통해 팀 단위·서비스 단위 점진적 전환이 가능해졌습니다.
🧮 7. 세 번째 난관: 메시지 및 오프셋 동기화
새로운 클러스터로 메시지를 옮길 때는 단순히 데이터를 복사하는 것만으로는 부족합니다.
Kafka는 오프셋(Offset) 기반으로 메시지 소비 상태를 추적하기 때문입니다.
📘 Offset: 파티션 내 메시지의 순서를 나타내는 정수 값. 컨슈머는 ‘마지막으로 읽은 메시지의 오프셋’을 커밋함으로써, 재시작 시 이어서 소비할 수 있음.
만약 오프셋이 동기화되지 않으면, 신규 클러스터에서 메시지를 중복 소비하거나 누락시킬 수 있습니다.
🧰 8. MirrorMaker 2: Kafka 간 동기화 솔루션 도입
직접 복제 로직을 구현하는 대신, Kafka에서 제공하는 **MirrorMaker 2 (MM2)**를 활용했습니다.
🔍 MirrorMaker 2란?
Apache Kafka의 공식 오픈소스 프레임워크로, 두 클러스터 간 메시지와 오프셋을 동기화하는 시스템.
내부적으로 Kafka Connect 프레임워크 위에서 동작합니다. 클러스터 간 데이터 복제를 지원합니다.
구성 요소:
SourceConnector: 메시지 복제 담당
CheckpointConnector: 메시지 오프셋 동기화 담당
🧱 9. Kubernetes 기반 운영: Strimzi Kafka Operator
당근의 인프라는 Kubernetes 기반으로 구성되어 있기에,
MM2는 Strimzi Kafka Operator를 통해 관리되었습니다.
📘 Strimzi: 쿠버네티스 환경에서 Kafka, Topic, MirrorMaker 등을 CRD(Custom Resource Definition)로 관리할 수 있게 해주는 CNCF 프로젝트.
이를 Helm Chart로 래핑하여, 다음처럼 단순한 매니페스트만으로 손쉽게 클러스터 간 복제를 구성했습니다:
🔁 10. Offset Mapping 문제 해결
MM2의 기본 문제 중 하나는 오프셋 맵핑입니다.
서로 다른 클러스터에서는 동일 메시지라도 오프셋 번호가 달라질 수 있습니다.
이를 위해 MM2는 offset-syncs-topic을 자동 생성하여,
소스–타겟 오프셋 매핑 테이블을 기록합니다.
예: legacy:offset(30) → new:offset(0)
이 정보를 CheckpointConnector가 참조해 컨슈머 오프셋을 이어서 커밋할 수 있게 됩니다.
🔤 11. 토픽 이름 정책 문제와 커스텀 확장
신규 클러스터에서는 “서비스 명 + 이벤트 명” 형식의 네이밍 컨벤션을 적용했습니다.
예: danggeun.user.create_user
그러나 기존 MM2는 토픽 이름 변경 동기화를 지원하지 않았습니다.
기본 정책(ReplicationPolicy)은 단지 접두사(prefix)만 붙이거나, 이름을 그대로 유지할 뿐입니다.
🧠 해결책: Custom ReplicationPolicy 구현
RenameTopicReplicationPolicy 클래스를 별도로 구현하여 다음을 지원했습니다:
이를 별도 이미지(danggeun-mirrormaker-extension)로 빌드하고, Strimzi에서 커스텀 이미지를 지정했습니다.
✅ 결과적으로 토픽명 변경 + 메시지/오프셋 동기화를 모두 보장하는 완전한 무중단 전환 구조가 완성되었습니다.
🚀 12. 마이그레이션 실행 절차
단계별 프로세스
대상 토픽 및 컨슈머 그룹 선정
중복 처리/순서 보장이 가능한 서비스만 우선.
MirrorMaker2 동기화 시작
Helm values로 소스/타겟/토픽 지정.
Kafka Wire Proxy 설정 갱신
Client ID 기준 라우팅 정책 추가.
컨슈머 전환 → 프로듀서 전환
중복 메시지는 오프셋 기반으로 최소화.
🧩 13. Zero Downtime을 위한 사전 조건
Kafka의 무중단 전환은 “이론적으로” 가능하지만, 실제로는 다음 조건이 필요합니다.
Idempotent Consumer (멱등 소비자)
중복 메시지를 처리해도 결과가 동일해야 함.
Ordering-Insensitive Producer
메시지 순서가 약간 달라도 비즈니스 로직이 깨지지 않아야 함.
당근의 주요 Kafka 클라이언트는 이 두 조건을 만족하도록 설계되어 있었기에, 진정한 제로 다운타임 이행이 가능했습니다.
🧰 14. 교훈과 인사이트
1️⃣ “장소 투명성”은 시스템 확장의 전제 조건이다
Kafka뿐 아니라 DB, Storage, MQ 등 모든 인프라 계층에서 “서비스 코드가 인프라 위치를 몰라도 된다”는 설계는 대규모 조직 운영의 핵심입니다.
2️⃣ 점진적 전환은 기술보다 정책의 문제다
기술적 복제보다, “어떤 서비스가 언제 옮겨질지”를 관리하는 정책이 더 중요합니다.
3️⃣ 오픈소스 확장의 힘
MirrorMaker2의 단점(이름 변경 불가)을 커스텀 코드로 해결함으로써,
당근은 내부 요구에 맞는 Kafka 이행 체계를 완성했습니다.
🔮 15. 앞으로의 계획
Kafka Wire Proxy의 표준화: 향후 신규 서비스 기본 연결 포인트로 지정 예정.
토픽 관리 자동화: Topic lifecycle 관리 및 네이밍 유효성 검증 자동화.
Managed Consumer Alerts: 메시지 소비 이상 탐지 자동 알림 시스템 개발 중.
[[Kafka 뉴비의 마이그레이션, 산 넘고 물 건너 | 2024 당근 테크 밋업](https://youtu.be/zheA2qdyCnM?si=6DlBXyKe3-Dpttr6)](https://youtu.be/zheA2qdyCnM?si=6DlBXyKe3-Dpttr6)
🧭 모두가 행복한 Kafka 마이그레이션 – 당근 공통서비스팀의 무중단 이행기
📌 개요: ‘서비스를 멈추지 않는’ 마이그레이션의 목표
Kafka는 비동기 이벤트 스트리밍의 핵심 인프라입니다. 그러나 조직이 커지고, 클러스터가 늘어나며, 토픽 수가 1000개를 넘어가면 운영 복잡도와 비용이 기하급수적으로 상승합니다.
당근 공통서비스팀은 이런 현실적 문제를 해결하기 위해 “심리스(Seamless) 마이그레이션”, 즉 클라이언트 수정 없이 Kafka 클러스터를 전환하는 프로젝트를 수행했습니다.
이번 글은 그 여정에서 마주친 난관과 이를 극복한 기술적 해법을 심층적으로 다룹니다.
🏗️ 1. 배경: 당근의 Kafka 운영 현황과 문제 인식
당근은 서비스 전반에서 비동기 이벤트를 처리하기 위해 Kafka를 사용합니다.
토픽 수: 약 1,100개
컨슈머 그룹 멤버 수: 2,000개
클러스터 수: 13개
일평균 메시지 트래픽: 수백만 건
이 중 **‘서비스 Kafka 클러스터’**는 전사 서비스가 공유하며, 변경이 빈번하고 의존성이 얽혀 있는 구조였습니다.
결과적으로 다음과 같은 문제가 누적되었습니다.
⚠️ 운영 문제 요약
스키마 무결성
메시지 스키마 변경 시 backward compatibility 깨짐 → 장애 발생
네이밍 혼란
토픽 이름 규칙 부재 → 출처 및 용도 추적 불가
권한 통제 부재
내부 Kafka 클라이언트면 모든 토픽 접근 가능 → 보안 취약
🧩 개선 방향
공통서비스팀은 **신규 Kafka 클러스터(신규 정책 반영)**를 구축하고,
기존 레거시 클러스터와 병행 운용하며 점진적 전환(Fade-out)을 시도했습니다.
그러나 1년이 지나도 신규 클러스터의 토픽 비율은 20% 미만, 대부분은 여전히 레거시에 남아 있었습니다.
이제 목표는 명확했습니다.
"서비스팀의 코드 수정 없이, 중앙 제어로 안전하게 Kafka 클러스터를 전환하자."
🧱 2. 첫 번째 난관: Static Broker Host 문제
Kafka 클라이언트는 브로커 연결을 위해 bootstrap.servers 프로퍼티를 사용합니다.
이는 “호스트:포트” 형태로 브로커 정보를 명시해야 하므로,
신규 클러스터로 전환할 경우 모든 서비스의 환경 변수와 배포 파이프라인을 수정해야 했습니다.
👉 수천 개 서비스의 수동 수정은 비현실적.
이 문제의 본질은 **“장소 투명성(Location Transparency)”**의 부재였습니다.
💡 접근 전략
모든 언어 SDK(Java, Go, Python 등)를 직접 수정하는 것은 불가능.
대신 Kafka 프로토콜 자체를 중계하는 프록시 서버를 구축한다면?
Kafka프로토콜?
바이너리 데이터로 이루어진 포맷으로 모든 클라이언트는 카프카 프로토콜의 포맷에 맞춰 개발해야함.
따라서 이를 활용하면 모든 클라이언트를 만족시키는 프록시를 만들수 있지 않을까?
이 아이디어가 바로 Kafka Wire Proxy의 출발점이었습니다.
🔌 3. Kafka Wire Proxy의 설계 배경
Kafka의 클라이언트–브로커 간 통신은 Kafka Protocol이라는 바이너리 포맷으로 이루어집니다.
이 포맷은 다음과 같이 구성됩니다:
모든 요청/응답(API Versions, Metadata, Produce, Fetch 등)은 이 구조를 따릅니다.
📘 Kafka Protocol: 클라이언트가 브로커에게 메타데이터를 질의하고, 메시지를 송수신하기 위한 바이너리 포맷 규약.
이를 중간에서 가로채고 해석할 수 있다면,
브로커 주소를 직접 변경하지 않고도 다른 클러스터로 라우팅이 가능합니다.
⚙️ 4. Kafka Wire Proxy 아키텍처
🔄 기존 구조 (As-Is)
클라이언트는
bootstrap.servers로 브로커 주소를 직접 참조.모든 서비스별 환경 변수 수정 필요.
🧠 개선 구조 (To-Be)
클라이언트 → Kafka Wire Proxy → 실제 브로커
Proxy가 초기 메타데이터 요청만 처리 후, 실제 브로커와 TCP 연결을 중계.
Proxy의 역할은 “초기 연결 관리자”이며, 이후 메시지 스트리밍은 클라이언트와 브로커가 직접 처리합니다.
⚡ 효과
클러스터 전환 시 Proxy 설정만 변경 → 서비스 수정 0건
모든 언어 SDK 통합 지원 → 위치 투명성 확보
클러스터 전환 중앙화 → 운영 효율화
🧭 5. 두 번째 난관: 점진적 전환(Rolling Migration)
Kafka의 특성상 하나의 토픽을 여러 팀이 구독할 수 있습니다.
즉, 특정 토픽을 새로운 클러스터로 옮기려면 모든 팀의 컨슈머 코드가 동시에 전환되어야 하는데, 이는 불가능합니다.
그래서 공통서비스팀은 “한 번에 전체를 옮기지 않고, 일부 클라이언트만 순차적으로 옮긴다”는 전략을 채택했습니다.
🧩 6. 헤더 기반 라우팅: Client ID를 이용한 세밀한 제어
Kafka 프로토콜의 헤더에는 client.id 필드가 포함됩니다.
이는 각 프로듀서/컨슈머를 식별하기 위한 메타데이터입니다.
Wire Proxy는 이 client.id를 기준으로 외부 설정 서버에서 라우팅 컨텍스트를 조회합니다.
user-service-*
legacy
ad-service-*
new
🎯 즉, 서비스별로 다른 클러스터로 프록시 라우팅이 가능해짐.
이를 통해 팀 단위·서비스 단위 점진적 전환이 가능해졌습니다.
🧮 7. 세 번째 난관: 메시지 및 오프셋 동기화
새로운 클러스터로 메시지를 옮길 때는 단순히 데이터를 복사하는 것만으로는 부족합니다.
Kafka는 오프셋(Offset) 기반으로 메시지 소비 상태를 추적하기 때문입니다.
📘 Offset: 파티션 내 메시지의 순서를 나타내는 정수 값. 컨슈머는 ‘마지막으로 읽은 메시지의 오프셋’을 커밋함으로써, 재시작 시 이어서 소비할 수 있음.
만약 오프셋이 동기화되지 않으면, 신규 클러스터에서 메시지를 중복 소비하거나 누락시킬 수 있습니다.
🧰 8. MirrorMaker 2: Kafka 간 동기화 솔루션 도입
직접 복제 로직을 구현하는 대신, Kafka에서 제공하는 **MirrorMaker 2 (MM2)**를 활용했습니다.
🔍 MirrorMaker 2란?
Apache Kafka의 공식 오픈소스 프레임워크로, 두 클러스터 간 메시지와 오프셋을 동기화하는 시스템.
내부적으로 Kafka Connect 프레임워크 위에서 동작합니다. 클러스터 간 데이터 복제를 지원합니다.
구성 요소:
SourceConnector: 메시지 복제 담당
CheckpointConnector: 메시지 오프셋 동기화 담당
🧱 9. Kubernetes 기반 운영: Strimzi Kafka Operator
당근의 인프라는 Kubernetes 기반으로 구성되어 있기에,
MM2는 Strimzi Kafka Operator를 통해 관리되었습니다.
📘 Strimzi: 쿠버네티스 환경에서 Kafka, Topic, MirrorMaker 등을 CRD(Custom Resource Definition)로 관리할 수 있게 해주는 CNCF 프로젝트.
이를 Helm Chart로 래핑하여, 다음처럼 단순한 매니페스트만으로 손쉽게 클러스터 간 복제를 구성했습니다:
🔁 10. Offset Mapping 문제 해결
MM2의 기본 문제 중 하나는 오프셋 맵핑입니다.
서로 다른 클러스터에서는 동일 메시지라도 오프셋 번호가 달라질 수 있습니다.
이를 위해 MM2는 offset-syncs-topic을 자동 생성하여,
소스–타겟 오프셋 매핑 테이블을 기록합니다.
예: legacy:offset(30) → new:offset(0)
이 정보를 CheckpointConnector가 참조해 컨슈머 오프셋을 이어서 커밋할 수 있게 됩니다.
🔤 11. 토픽 이름 정책 문제와 커스텀 확장
신규 클러스터에서는 “서비스 명 + 이벤트 명” 형식의 네이밍 컨벤션을 적용했습니다.
예: danggeun.user.create_user
그러나 기존 MM2는 토픽 이름 변경 동기화를 지원하지 않았습니다.
기본 정책(ReplicationPolicy)은 단지 접두사(prefix)만 붙이거나, 이름을 그대로 유지할 뿐입니다.
🧠 해결책: Custom ReplicationPolicy 구현
RenameTopicReplicationPolicy 클래스를 별도로 구현하여 다음을 지원했습니다:
이를 별도 이미지(danggeun-mirrormaker-extension)로 빌드하고, Strimzi에서 커스텀 이미지를 지정했습니다.
✅ 결과적으로 토픽명 변경 + 메시지/오프셋 동기화를 모두 보장하는 완전한 무중단 전환 구조가 완성되었습니다.
🚀 12. 마이그레이션 실행 절차
단계별 프로세스
대상 토픽 및 컨슈머 그룹 선정
중복 처리/순서 보장이 가능한 서비스만 우선.
MirrorMaker2 동기화 시작
Helm values로 소스/타겟/토픽 지정.
Kafka Wire Proxy 설정 갱신
Client ID 기준 라우팅 정책 추가.
컨슈머 전환 → 프로듀서 전환
중복 메시지는 오프셋 기반으로 최소화.
🧩 13. Zero Downtime을 위한 사전 조건
Kafka의 무중단 전환은 “이론적으로” 가능하지만, 실제로는 다음 조건이 필요합니다.
Idempotent Consumer (멱등 소비자)
중복 메시지를 처리해도 결과가 동일해야 함.
Ordering-Insensitive Producer
메시지 순서가 약간 달라도 비즈니스 로직이 깨지지 않아야 함.
당근의 주요 Kafka 클라이언트는 이 두 조건을 만족하도록 설계되어 있었기에, 진정한 제로 다운타임 이행이 가능했습니다.
🧰 14. 교훈과 인사이트
1️⃣ “장소 투명성”은 시스템 확장의 전제 조건이다
Kafka뿐 아니라 DB, Storage, MQ 등 모든 인프라 계층에서 “서비스 코드가 인프라 위치를 몰라도 된다”는 설계는 대규모 조직 운영의 핵심입니다.
2️⃣ 점진적 전환은 기술보다 정책의 문제다
기술적 복제보다, “어떤 서비스가 언제 옮겨질지”를 관리하는 정책이 더 중요합니다.
3️⃣ 오픈소스 확장의 힘
MirrorMaker2의 단점(이름 변경 불가)을 커스텀 코드로 해결함으로써,
당근은 내부 요구에 맞는 Kafka 이행 체계를 완성했습니다.
🔮 15. 앞으로의 계획
Kafka Wire Proxy의 표준화: 향후 신규 서비스 기본 연결 포인트로 지정 예정.
토픽 관리 자동화: Topic lifecycle 관리 및 네이밍 유효성 검증 자동화.
Managed Consumer Alerts: 메시지 소비 이상 탐지 자동 알림 시스템 개발 중.
느낀점
이 발표를 보면서 가장 인상 깊었던 점은 “모든 걸 한 번에 바꾸지 않고, 현실적인 제약 속에서 점진적으로 개선해 나가는 자세”였던것 같습니다. 단순히 기술 스택을 교체하는 이야기가 아니라, 수많은 팀과 서비스가 얽혀 있는 환경에서 어떻게 안정성과 생산성을 동시에 챙길 수 있는지를 볼 수 있었습니다.
특히 Wire Proxy로 연결을 추상화하고, client.id로 라우팅을 제어하며, MirrorMaker 2로 메시지와 오프셋까지 동기화하는 과정은 “문제를 한 겹씩 벗겨가며 해결하는 기술적 사고”가 느껴졌고, 무언가를 완전히 갈아엎는 대신, 조금씩 더 나은 구조로 이끌어가는 점이 인상깊었습니다.
Last updated