Blog
kafkadisaster-recoverymirrormaker2replicationoperations

[Kafka DR ②] MirrorMaker 2 실전 구축 — 단방향 복제부터

Kafka MirrorMaker 2의 아키텍처(Connect 기반 3개 커넥터), 복제 정책과 토픽 네이밍, mm2.properties 실전 설정, 실행과 복제 검증까지 단방향 DR 복제를 처음부터 구축하는 핸즈온 가이드입니다.

Data Dynamics2026년 6월 14일20 min read

재해 복구(DR)를 위해 두 번째로 마련한 Kafka 클러스터는, 데이터가 흘러 들어오기 전까지는 빈 껍데기일 뿐입니다. 1편에서 우리는 "왜 DR이 필요한가"와 RPO/RTO 같은 목표를 세웠습니다. 이제 본론입니다. 주(primary) 클러스터의 메시지를 어떻게 DR 클러스터로 실시간 복제할 것인가? 그 답이 바로 Apache Kafka가 공식으로 제공하는 MirrorMaker 2(MM2) 입니다. 이번 편에서는 가장 기본이자 가장 중요한 단방향 복제를 처음부터 손으로 구축해 봅니다.

이 글에서 배우는 것

  • MM2가 Kafka Connect 위에서 동작하는 방식과 3개의 핵심 커넥터
  • 복제 정책(Replication Policy)과 토픽 네이밍 규칙, 그리고 복제 루프를 막는 원리
  • 실전에서 바로 쓸 수 있는 mm2.properties 설정 한 줄 한 줄
  • connect-mirror-maker.sh로 MM2를 띄우고 복제를 검증하는 방법
  • 데이터 복제만으로는 왜 컨슈머 페일오버가 불가능한지 (3편 예고)

1. MirrorMaker 2는 Kafka Connect 위에서 동작한다

왜 MM2인가

초기 MirrorMaker(이른바 MM1)는 단순히 컨슈머 + 프로듀서를 묶은 도구였습니다. 토픽 설정도, 컨슈머 오프셋도 복제하지 못했고, 운영 중 토픽이 늘어나면 수동으로 따라가야 했습니다. KIP-382로 도입된 MirrorMaker 2는 이 한계를 Kafka Connect 프레임워크 위에 재구축하면서 해결했습니다.

Connect 위에 올라갔다는 사실이 중요합니다. 덕분에 MM2는 분산 실행, 자동 리밸런싱, 오프셋 추적, REST 기반 관리 같은 Connect의 모든 인프라를 그대로 물려받습니다. MM2를 운영한다는 것은 사실상 세 종류의 Connect 커넥터를 운영하는 것과 같습니다.

3개의 커넥터

MM2는 복제 방향(source→target) 하나당 다음 세 커넥터를 띄웁니다.

커넥터역할결과물
MirrorSourceConnector소스 토픽의 레코드 데이터를 타깃으로 복제primary.orders 같은 미러 토픽
MirrorCheckpointConnector컨슈머 그룹 오프셋을 체크포인트로 변환·복제<source>.checkpoints.internal
MirrorHeartbeatConnector주기적 하트비트 발행으로 연결성·지연 측정heartbeats 토픽

핵심은 세 커넥터의 책임이 명확히 분리되어 있다는 점입니다.

  • MirrorSourceConnector가 실제 메시지(payload)를 복제합니다. 토픽 목록을 주기적으로 갱신하고, 새 파티션이 생기면 따라갑니다. 토픽 설정(config)과 ACL도 옵션에 따라 함께 동기화합니다.
  • MirrorCheckpointConnector는 메시지가 아니라 컨슈머 그룹의 오프셋 위치를 복제합니다. 단, 소스 클러스터의 오프셋 번호를 그대로 쓸 수는 없습니다(미러 토픽은 오프셋이 다르게 매겨지므로). 그래서 소스 오프셋 → 타깃 오프셋 매핑(offset-syncs)을 기록해 두고, 이를 바탕으로 "체크포인트"를 만들어 둡니다. 이 체크포인트가 3편의 페일오버에서 결정적인 역할을 합니다.
  • MirrorHeartbeatConnector는 소스 클러스터에 일정 간격으로 하트비트 레코드를 찍습니다. 이 레코드가 타깃까지 복제되는 시간을 보면 엔드투엔드 복제 지연(replication lag) 을 측정할 수 있습니다.
Loading diagram…

2. 복제 정책과 토픽 네이밍

DefaultReplicationPolicy — 접두사로 출처를 새긴다

MM2가 토픽을 복제할 때 가장 먼저 결정해야 하는 것은 타깃에서 토픽 이름을 무엇으로 할 것인가입니다. 기본값인 DefaultReplicationPolicy소스 클러스터 별칭(alias)을 접두사로 붙입니다.

소스(primary)의 토픽:   orders
타깃(dr)의 미러 토픽:   primary.orders

이 접두사 규칙은 두 가지 문제를 동시에 해결합니다.

  1. 복제 루프 방지: 양방향(active-active) 구성에서 primary.orders가 다시 primary로 역복제되면 dr.primary.orders가 되어야 하는데, MM2는 이미 자기 별칭이 접두사에 들어간 토픽은 다시 복제하지 않습니다. 즉 접두사가 루프를 끊는 안전장치입니다.
  2. 출처 명확화: DR 클러스터에서 primary.orders를 보면 "이건 primary에서 흘러온 미러"라는 사실이 이름만으로 드러납니다.

IdentityReplicationPolicy — 이름을 그대로

조직에 따라 접두사가 거슬릴 수 있습니다(기존 컨슈머가 orders를 구독하는데 DR에서는 primary.orders를 구독해야 하므로). 이때 IdentityReplicationPolicy를 쓰면 이름을 바꾸지 않고 ordersorders로 복제합니다.

정책타깃 토픽 이름장점위험
DefaultReplicationPolicyprimary.orders루프 방지 자동, 출처 명확컨슈머가 접두사 인지 필요
IdentityReplicationPolicyorders컨슈머 변경 불필요, 페일오버 단순루프 방지 안 됨 → 단방향에서만, 또는 명시적 토픽 필터 필수

주의: IdentityReplicationPolicy는 접두사로 루프를 끊지 못하므로, 양방향 구성에서 그대로 쓰면 무한 복제가 발생할 수 있습니다. 단방향 DR이라면 비교적 안전하지만, topics / topics.exclude 필터를 명시적으로 잡아 미러 토픽이 되돌아가지 않도록 직접 막아야 합니다. 본 글의 단방향 예제는 안전한 기본값인 DefaultReplicationPolicy를 사용합니다.

내부 토픽들

MM2는 동작에 필요한 메타데이터를 클러스터의 내부 토픽에 저장합니다. 이름만 봐도 역할을 짐작할 수 있게 설계되어 있습니다.

토픽위치역할
heartbeats소스하트비트 원본 (타깃에는 <source>.heartbeats로 미러)
<source>.checkpoints.internal타깃컨슈머 그룹 오프셋 체크포인트
mm2-offset-syncs.<target>.internal소스(또는 타깃)소스 오프셋 ↔ 타깃 오프셋 매핑

mm2-offset-syncs가 바로 "소스의 100번 오프셋이 타깃에서는 몇 번인가"를 기록하는 표입니다. 이 표가 있어야 체크포인트가 의미를 가지며, 페일오버 시 컨슈머를 올바른 위치에 앉힐 수 있습니다.


3. mm2.properties 설정 한 줄씩

이제 실제 설정 파일을 작성합니다. MM2는 전용 설정 파일 하나로 클러스터 정의·복제 방향·복제 대상을 모두 기술합니다. 아래는 primary → dr 단방향 DR 복제를 위한 실전 예시입니다.

# ── 클러스터 별칭 정의 ───────────────────────────────
# 여기서 정한 별칭(primary, dr)이 토픽 접두사와 흐름 정의에 그대로 쓰인다.
clusters = primary, dr
 
# ── 각 클러스터의 부트스트랩 서버 ────────────────────
primary.bootstrap.servers = primary-broker1:9092,primary-broker2:9092,primary-broker3:9092
dr.bootstrap.servers      = dr-broker1:9092,dr-broker2:9092,dr-broker3:9092
 
# (필요 시) 보안 설정도 클러스터별 접두사로 지정
# primary.security.protocol = SASL_SSL
# primary.sasl.mechanism    = SCRAM-SHA-512
# primary.sasl.jaas.config  = org.apache.kafka.common.security.scram.ScramLoginModule required username="mm2" password="***";
 
# ── 복제 흐름(flow) 활성화 ───────────────────────────
# primary에서 dr 방향으로만 복제(단방향). 역방향은 명시하지 않으므로 비활성.
primary->dr.enabled = true
dr->primary.enabled = false
 
# ── 무엇을 복제할 것인가 ─────────────────────────────
# 모든 사용자 토픽/그룹 복제. 운영에서는 정규식 화이트리스트 권장.
primary->dr.topics = .*
primary->dr.groups = .*
# 복제 제외(내부/임시 토픽 등). MM2 기본 제외 패턴 외 추가가 필요하면 여기에.
primary->dr.topics.exclude = .*[\-\.]internal, .*\.replica, __.*
 
# ── 미러 토픽의 복제 계수 ────────────────────────────
# DR 클러스터 브로커 수에 맞춰 설정(예: 3노드면 3).
replication.factor = 3
 
# ── MM2 내부 토픽의 복제 계수 ────────────────────────
checkpoints.topic.replication.factor       = 3
heartbeats.topic.replication.factor        = 3
offset-syncs.topic.replication.factor      = 3
offset.storage.replication.factor          = 3
status.storage.replication.factor          = 3
config.storage.replication.factor          = 3
 
# ── 토픽 설정/ACL 동기화 ─────────────────────────────
# 소스 토픽의 설정(파티션 수, retention 등)을 타깃에 맞춰 유지
sync.topic.configs.enabled = true
# 소스 토픽의 ACL을 타깃에 동기화(보안 클러스터라면 권장)
sync.topic.acls.enabled    = true
 
# ── 토픽/그룹 변화 추적 주기 ─────────────────────────
# 새 토픽·파티션·그룹이 생겼을 때 얼마나 빨리 따라잡을지(초)
refresh.topics.interval.seconds = 30
refresh.groups.interval.seconds = 30
 
# ── 체크포인트/하트비트 발행 ─────────────────────────
emit.checkpoints.enabled         = true
emit.checkpoints.interval.seconds = 30
emit.heartbeats.enabled          = true
emit.heartbeats.interval.seconds = 5
 
# ── 복제 정책 (기본값 명시) ──────────────────────────
replication.policy.class = org.apache.kafka.connect.mirror.DefaultReplicationPolicy
 
# ── 커넥터 태스크 병렬도 ─────────────────────────────
# 처리량이 큰 토픽이 많으면 늘려 파티션을 더 잘게 나눠 처리.
tasks.max = 4

꼭 짚고 갈 옵션

  • clusters / *.bootstrap.servers: 모든 것의 출발점. 별칭(primary, dr)이 곧 토픽 접두사가 되므로 의미 있는 이름을 쓰세요.
  • primary->dr.enabled = true: 복제는 "흐름(flow)" 단위로 켜고 끕니다. 단방향이라면 역방향(dr->primary.enabled)은 켜지 않습니다.
  • topics = .*: 처음에는 좁게(화이트리스트) 시작하길 권합니다. .*는 편하지만 컴팩션 토픽·임시 토픽까지 따라가 디스크와 네트워크를 잠식할 수 있습니다.
  • sync.topic.configs.enabled: 이걸 켜야 소스에서 파티션을 늘리거나 retention을 바꿨을 때 타깃이 따라옵니다. DR 일관성의 핵심입니다.
  • emit.checkpoints.enabled: 페일오버를 하려면 반드시 true. 컨슈머 오프셋을 옮길 근거가 여기서 만들어집니다(3편에서 본격적으로 활용).
  • refresh.topics.interval.seconds: 새 토픽이 DR에 나타나기까지의 최대 지연. 너무 짧으면 메타데이터 부하, 너무 길면 신규 토픽 누락 시간이 길어집니다. 30초 전후가 무난합니다.

4. MM2 실행하기

전용 모드: connect-mirror-maker.sh

가장 간단한 실행 방법은 Kafka 배포본에 포함된 전용 스크립트입니다. 위에서 만든 설정 파일을 인자로 넘기면, MM2가 필요한 Connect 워커와 세 커넥터를 한 번에 띄웁니다.

# Kafka 설치 디렉터리에서
$ bin/connect-mirror-maker.sh config/mm2.properties
 
# 여러 노드로 분산 실행하려면, 같은 설정 파일로 각 노드에서 동일 명령을 실행한다.
# (같은 그룹으로 묶여 Connect가 자동으로 태스크를 리밸런싱한다.)
$ bin/connect-mirror-maker.sh config/mm2.properties

이 "전용 모드(dedicated mode)"는 MM2가 내부적으로 Connect 워커를 직접 구동하므로, 별도의 Connect 클러스터를 미리 띄울 필요가 없습니다. DR 구축 초기엔 이 방식이 가장 빠릅니다.

기존 Connect 클러스터에 커넥터로 올리기

이미 운영 중인 Kafka Connect 클러스터가 있다면, MM2의 세 커넥터를 일반 커넥터처럼 REST로 등록할 수도 있습니다. 이 경우 Connect의 모니터링·배포 파이프라인을 그대로 재사용할 수 있어 운영 표준화에 유리합니다.

# Connect REST로 MirrorSourceConnector 등록 (예시)
$ curl -X PUT http://connect:8083/connectors/primary-to-dr-source/config \
  -H "Content-Type: application/json" \
  -d '{
    "connector.class": "org.apache.kafka.connect.mirror.MirrorSourceConnector",
    "source.cluster.alias": "primary",
    "target.cluster.alias": "dr",
    "source.cluster.bootstrap.servers": "primary-broker1:9092",
    "target.cluster.bootstrap.servers": "dr-broker1:9092",
    "topics": ".*",
    "replication.factor": "3",
    "sync.topic.configs.enabled": "true",
    "tasks.max": "4"
  }'

선택 기준: PoC·소규모라면 전용 모드(connect-mirror-maker.sh)가 간편합니다. 이미 Connect 운영 역량과 모니터링이 갖춰진 조직이라면 기존 Connect 클러스터에 커넥터로 올리는 편이 장기적으로 관리가 쉽습니다.


5. 복제가 잘 되는지 검증하기

띄웠다고 끝이 아닙니다. 실제로 데이터가 흐르는지 세 단계로 확인합니다.

5-1. 미러 토픽이 DR에 생겼는가

먼저 DR 클러스터에서 토픽 목록을 보고, primary. 접두사가 붙은 미러 토픽이 나타나는지 확인합니다.

$ bin/kafka-topics.sh --bootstrap-server dr-broker1:9092 --list
 
heartbeats
mm2-offset-syncs.dr.internal
primary.checkpoints.internal
primary.heartbeats
primary.orders          # ← primary의 orders가 복제됨
primary.payments        # ← primary의 payments가 복제됨

primary.orders가 보인다면 MirrorSourceConnector가 정상 동작하는 것입니다.

5-2. 복제 지연(lag)을 본다

소스에 메시지를 하나 넣고, 타깃 미러 토픽의 끝 오프셋이 따라 오르는지 봅니다.

# 소스 토픽의 끝 오프셋
$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell \
    --bootstrap-server primary-broker1:9092 --topic orders --time -1
 
# 타깃 미러 토픽의 끝 오프셋 (시차를 두고 따라와야 정상)
$ bin/kafka-run-class.sh kafka.tools.GetOffsetShell \
    --bootstrap-server dr-broker1:9092 --topic primary.orders --time -1

두 값의 차이가 곧 복제 지연의 근사치입니다. 이 차이가 시간이 지나도 계속 벌어진다면 태스크 부족(tasks.max)이나 네트워크 대역폭을 의심해야 합니다.

5-3. 하트비트로 엔드투엔드 연결성 확인

가장 직관적인 헬스 체크는 하트비트 토픽을 직접 읽어 보는 것입니다.

$ bin/kafka-console-consumer.sh \
    --bootstrap-server dr-broker1:9092 \
    --topic primary.heartbeats \
    --from-beginning --max-messages 5

레코드가 꾸준히 흘러 들어온다면, primary→dr 경로 전체가 살아 있다는 뜻입니다. 하트비트가 멈추면 복제 경로 어딘가가 끊긴 것이므로, 알람의 1차 신호로 삼기에 좋습니다.


6. 그런데, 이걸로 페일오버가 될까?

여기까지 하면 DR 클러스터에는 primary의 모든 메시지가 차곡차곡 쌓입니다. 데이터는 안전합니다. 그렇다면 primary가 죽었을 때 컨슈머를 그냥 dr로 옮기면 끝일까요?

아닙니다. 여기에 DR의 가장 큰 함정이 있습니다.

  • 미러 토픽 primary.orders의 오프셋은 원본 orders의 오프셋과 번호가 다릅니다. 컨슈머 그룹이 소스에서 "오프셋 1,000,000까지 읽었다"고 해서, dr의 primary.orders에서 1,000,000번을 읽으면 엉뚱한 위치입니다.
  • 그래서 MM2는 mm2-offset-syncs와 체크포인트로 "소스 오프셋 → 타깃 오프셋" 변환표를 만들어 둡니다. 페일오버 시 이 변환을 적용해야만 컨슈머가 중복도 누락도 없이 이어서 소비할 수 있습니다.
  • 이 오프셋 변환(offset translation)과, RemoteClusterUtils/MirrorClient를 활용한 컨슈머 그룹 이전, 그리고 active-active 양방향 구성에서의 페일오버 절차가 다음 편의 주제입니다.

정리하면, 데이터 복제는 DR의 절반일 뿐입니다. 나머지 절반인 "컨슈머를 끊김 없이 옮기는 법"이 없으면, 장애 순간 운영팀은 어디서부터 다시 읽어야 할지 알 수 없습니다.


마치며

  • MM2는 Kafka Connect 위에서 동작하며, 복제 방향당 MirrorSource(데이터)·MirrorCheckpoint(오프셋)·MirrorHeartbeat(연결성) 3개 커넥터로 구성됩니다.
  • DefaultReplicationPolicy소스 별칭을 접두사로 붙여(ordersprimary.orders) 출처를 명확히 하고 복제 루프를 끊습니다. IdentityReplicationPolicy는 이름을 보존하지만 루프 방지가 없어 주의가 필요합니다.
  • 단방향 DR은 mm2.properties 하나면 충분합니다. primary->dr.enabled = true, topics/groups 필터, replication.factor, sync.topic.configs.enabled, emit.checkpoints.enabled가 핵심 스위치입니다.
  • 실행은 connect-mirror-maker.sh(전용 모드)로 가장 빠르게 시작하고, 운영 표준화가 필요하면 기존 Connect 클러스터에 커넥터로 올립니다.
  • 검증은 미러 토픽 생성 → 복제 지연 → 하트비트 3단계로 확인하세요.
  • 데이터 복제만으로는 컨슈머 페일오버가 되지 않습니다. 오프셋 변환과 컨슈머 그룹 이전은 [Kafka DR ③]에서 다룹니다.

참고 자료


— Data Dynamics 엔지니어링 팀