Engineering Note

Kafka Active-Active의 핵심은 Kafka가 아니라 Ownership이다

Kafka 기반 Active-Active DR 아키텍처에서 Kafka가 해결하는 문제와 애플리케이션이 직접 책임져야 하는 Ownership, Outbox, Inbox, 장애 전환 전략을 정리합니다.

2026년 7월 11일 · Pletor Engineering kafkaactive-activedisaster-recoverymsaarchitecture

Kafka를 이용해 Active-Active DR 아키텍처를 구성한다고 하면, 처음에는 Kafka 클러스터를 어떻게 복제할지에 관심이 쏠리기 쉽습니다.

양쪽 리전에 Kafka를 두고, topic을 복제하고, consumer가 각 리전에서 이벤트를 읽도록 만들면 Active-Active가 되는 것처럼 보입니다.

하지만 실제 난제는 Kafka 바깥에 있습니다.

같은 데이터를 두 리전에서 동시에 수정할 수 있는가. 어느 리전이 쓰기 권한을 갖는가. 장애가 났을 때 그 권한을 어떻게 옮기는가. 오래된 리전이 뒤늦게 살아났을 때 쓰기를 어떻게 막는가.

이 질문에 답하지 못하면 Kafka가 이벤트를 안정적으로 전달하더라도 데이터 정합성은 깨질 수 있습니다.

핵심은 이렇습니다.

Kafka는 리전 사이의 이벤트 전달 계층이다.
Active-Active의 정합성은 애플리케이션의 Ownership 모델이 결정한다.
두 개의 작동 중인 시스템 사이에 하나의 황동 열쇠가 놓여 쓰기 소유권을 상징하는 정물 사진
두 리전이 모두 작동하더라도, 쓰기 권한을 여는 열쇠는 한 시점에 하나의 리전에만 있어야 합니다.

Active-Active라는 말의 함정

Active-Active는 문맥에 따라 서로 다른 의미로 쓰입니다.

하나는 양쪽 리전이 모두 사용자 트래픽을 받는다는 뜻입니다. 사용자는 가까운 리전으로 접속하고, 읽기는 로컬 DB에서 처리됩니다. 이 정도는 비교적 현실적인 목표입니다.

다른 하나는 양쪽 리전이 같은 데이터를 동시에 쓸 수 있다는 뜻입니다. 이 경우에는 충돌 해결, version 관리, merge 정책, 보상 처리, 사용자 개입까지 필요할 수 있습니다. 일반적인 업무 시스템에서는 이 두 번째 의미의 Active-Active가 훨씬 어렵습니다.

그래서 Kafka 기반 Active-Active를 설계할 때는 먼저 용어를 좁혀야 합니다.

양쪽 리전은 모두 active 하다.
하지만 하나의 데이터에 대한 쓰기 권한은 한 시점에 하나의 리전에만 있다.

이 글에서는 이 모델을 기본 전제로 봅니다.

기본 구조: 읽기는 로컬, 쓰기는 소유 리전

가장 현실적인 기본 구조는 Home Region 방식입니다.

각 데이터, 더 정확히는 각 aggregate나 tenant에 쓰기 소유 리전이 있습니다. 읽기는 양쪽 리전에서 로컬 DB를 통해 처리할 수 있지만, 쓰기는 해당 데이터의 Home Region에서만 수행합니다.

두 리전의 애플리케이션, DB, Outbox, Kafka, Inbox가 이벤트를 복제하는 Active-Active 흐름
Kafka를 전달 계층으로 사용한 예시입니다. 다른 메시징 시스템을 선택하더라도 어떤 리전이 쓸 수 있는지는 Ownership 모델이 결정합니다.

흐름은 다음과 같습니다.

1. Region A가 소유한 데이터는 Region A에서만 쓴다.
2. 애플리케이션은 비즈니스 DB 변경과 Outbox 기록을 같은 transaction으로 commit한다.
3. Outbox relay가 이벤트를 Kafka에 발행한다.
4. 별도의 cross-cluster replication 계층이 이벤트를 반대 리전의 Kafka로 복제한다.
5. 수신 리전은 Inbox 기록과 자기 DB 변경을 하나의 transaction으로 commit한다.
6. 읽기는 각 리전의 로컬 DB에서 처리한다.

여기서 cross-cluster replication은 Kafka broker의 replication factor와 다른 계층입니다. Apache Kafka에서는 MirrorMaker 2 같은 도구로 A -> B, B -> A 방향의 복제 흐름을 구성할 수 있고, 상용 Kafka 서비스의 cluster linking이나 별도 replication 제품을 선택할 수도 있습니다.

이 구조는 완전한 동기 정합성을 제공하지 않습니다. 반대 리전의 DB는 비동기 이벤트를 적용하며 따라오기 때문에, 짧은 시간 동안 stale read가 발생할 수 있습니다.

따라서 “읽기는 항상 로컬 DB”라는 규칙도 절대 규칙은 아닙니다. 대부분의 조회는 로컬 DB로 처리하되, 강한 최신성이 필요한 작업은 Home Region으로 보내거나, replication lag와 ownership context를 함께 보고 판단해야 합니다.

Kafka만이 정답은 아니다

이 구조에 반드시 Kafka가 필요한 것은 아닙니다. 필요한 것은 Outbox에서 발생한 변경 이벤트를 반대 리전으로 전달하고, 수신 측이 자기 속도로 처리할 수 있게 하는 비동기 메시징 계층입니다. 전통적인 메시지 브로커, 관리형 Pub/Sub, 다른 분산 로그 플랫폼도 요구사항에 따라 같은 자리를 맡을 수 있습니다.

Kafka는 흔히 메시지 큐라고 불리지만, 전통적인 queue보다 보존되는 분산 이벤트 로그에 가깝습니다. Consumer가 메시지를 가져갔다고 바로 사라지는 것이 아니라 retention 기간 동안 남아 있고, consumer group마다 offset을 독립적으로 관리합니다. 이 성질이 Active-Active의 변경 이벤트 전달과 잘 맞습니다.

대표적인 선택지의 성향을 비교하면 다음과 같습니다. 개별 제품과 서비스의 기능은 서로 겹치므로, 이 표는 절대적인 분류가 아니라 설계 출발점입니다.

선택지잘 맞는 상황확인할 비용과 제약
Kafka 계열의 분산 로그이벤트를 일정 기간 보존하고 여러 consumer group이 독립적으로 재처리해야 할 때, key 단위 순서와 CDC·connector 생태계가 중요할 때클러스터와 storage 운영 비용, partition 설계, 비동기 리전 복제의 지연·중복·루프 방지, 장애 전환 절차가 필요함
전통적인 메시지 브로커복잡한 routing, 작업 큐, 개별 메시지 ack, retry·dead letter 처리가 중심일 때장기 보존과 대규모 replay, 동일 이벤트를 여러 독립 소비자가 반복 활용하는 모델은 별도 설계가 필요할 수 있음
관리형 Pub/Sub브로커 운영을 줄이고 빠르게 리전 간 전달 계층을 구성할 때retention·ordering·재처리 제약, 리전 장애 모델, 비용, 특정 클라우드 종속성을 확인해야 함

Kafka를 선택할 때의 가장 큰 장점은 하나의 변경 이벤트를 여러 consumer group이 각자의 offset으로 읽고, 장애 복구나 projection 재생성이 필요할 때 다시 처리할 수 있다는 점입니다. aggregate_id를 key로 사용하면 같은 aggregate의 순서도 partition 안에서 유지할 수 있습니다. Outbox, CDC, stream processing 도구가 풍부하다는 점도 실용적입니다.

반대로 Kafka는 메시지를 전달했다고 해서 업무 처리가 끝나는 작업 큐가 아닙니다. 전역 순서를 제공하지 않고, 리전 간 복제는 대체로 비동기이며, 중복 전달과 복제 루프를 애플리케이션과 운영 정책이 감당해야 합니다. 복잡한 routing이나 짧은 수명의 command 처리만 필요하다면 전통적인 메시지 브로커가 더 단순할 수 있습니다. 운영 인력을 최소화해야 한다면 관리형 Pub/Sub가 더 나은 선택일 수도 있습니다.

따라서 선택 질문은 “Active-Active에는 Kafka가 필요한가?”가 아니라 다음에 가깝습니다.

이벤트를 얼마나 오래 보존해야 하는가?
장애 이후 replay가 필요한가?
몇 개의 독립 소비자가 같은 변경 이벤트를 읽는가?
어떤 단위의 순서를 보장해야 하는가?
리전 간 복제 지연과 중복을 어디까지 허용할 수 있는가?
메시징 계층을 직접 운영할 수 있는가?

어떤 제품을 선택하더라도 쓰기 Ownership, fencing, idempotency, failover 정책은 그대로 남습니다. 이 글이 Kafka를 중심으로 설명하는 이유는 Kafka가 유일한 답이라서가 아니라, 보존과 재처리가 중요한 이벤트 기반 Active-Active에 잘 맞는 선택지이기 때문입니다.

Kafka가 해결하는 것과 해결하지 않는 것

Kafka가 잘하는 일은 명확합니다.

Kafka는 이벤트를 저장하고 전달합니다. Consumer가 잠시 느려져도 이벤트를 다시 읽을 수 있고, 여러 consumer group이 같은 이벤트 스트림을 독립적으로 처리할 수 있습니다. Partition key를 잘 잡으면 같은 aggregate에 대한 이벤트 순서도 유지할 수 있습니다.

Kafka가 담당하는 역할은 대략 다음과 같습니다.

이벤트 저장
이벤트 전달
별도 replication 계층을 통한 리전 간 이벤트 복제
이벤트 재처리
서비스 간 느슨한 결합
consumer group 단위의 독립 처리

하지만 Kafka가 자동으로 해결하지 않는 것도 분명합니다.

어느 리전이 쓰기 권한을 갖는가
같은 데이터의 동시 수정을 어떻게 막을 것인가
장애 시 Ownership을 어떻게 넘길 것인가
중복 이벤트를 어떻게 무해하게 만들 것인가
stale read를 어디까지 허용할 것인가
failback 시 데이터를 어떻게 다시 맞출 것인가

이 부분은 Kafka 설정이 아니라 애플리케이션 아키텍처의 책임입니다.

Outbox와 Inbox는 지역 간 계약이다

리전 간 이벤트 전달에서는 Outbox와 Inbox가 거의 기본 계약에 가깝습니다.

Producer 서비스는 비즈니스 상태 변경과 이벤트 발행 의도를 같은 DB transaction으로 묶어야 합니다. 그래야 DB commit은 성공했는데 이벤트가 사라지는 상황을 줄일 수 있습니다.

Consumer 쪽도 마찬가지입니다. Kafka 복제, relay 재시도, consumer 재처리 때문에 같은 이벤트가 다시 들어올 수 있습니다. 수신 리전은 Inbox나 idempotent handler로 중복을 견뎌야 합니다.

중요한 점은 Inbox 기록과 업무 DB 변경도 같은 로컬 DB transaction으로 묶어야 한다는 것입니다. Inbox만 기록하고 업무 변경 전에 실패하면 재처리할 근거를 잃습니다. 반대로 업무 DB만 바뀌고 Inbox 기록이 실패하면 같은 변경을 다시 수행할 수 있습니다. event_id에 unique constraint를 두고, Inbox insert와 업무 상태 변경을 함께 commit한 뒤 Kafka offset을 전진시키는 방식이 기본입니다. Kafka도 외부 시스템까지 자동으로 exactly-once를 보장하지 않으며, 외부 저장소와의 원자성은 해당 시스템의 협력이 필요합니다.

이벤트에는 최소한 다음 정보가 있어야 운영하기 쉽습니다.

{
  "event_id": "01JZ7R3W3K3DZ6X7F4Y5Z6A9TQ",
  "aggregate_type": "Order",
  "aggregate_id": "ORD-1001",
  "event_type": "OrderApproved",
  "home_region": "region-a",
  "source_region": "region-a",
  "owner_epoch": 12,
  "version": 42,
  "occurred_at": "2026-07-11T09:30:00Z"
}

event_id는 중복 제거에 필요합니다. aggregate_id는 Kafka key 후보가 될 수 있습니다. home_regionsource_region은 이벤트의 출처를 추적하는 근거입니다. 다만 필드가 존재하는 것만으로 복제 루프가 차단되지는 않습니다. MirrorMaker 2의 remote topic naming과 cycle detection, 또는 사용 중인 replication 계층의 source alias와 복제 제외 정책이 함께 필요합니다.

owner_epochversion도 그 자체로 writer를 막지 않습니다. 모든 쓰기 경계가 authoritative ownership store의 현재 owner와 epoch를 검증하고, 오래된 epoch를 가진 요청을 거부할 때 비로소 fencing token으로 작동합니다.

대표적인 쓰기 모델

Active-Active에서 선택할 수 있는 쓰기 모델은 몇 가지가 있습니다.

Home Region 방식은 데이터마다 쓰기 소유 리전을 두는 방식입니다. 가장 단순하고, 가장 운영하기 쉽습니다. 장애 전환 시에는 해당 데이터의 Home Region을 다른 리전으로 옮깁니다.

Regional Write 방식은 tenant, 고객, 업무, 지역 같은 단위로 쓰기 리전을 나눕니다.

Tenant A -> Region A
Tenant B -> Region B

양쪽 리전이 모두 쓰기를 수행하지만, 같은 데이터는 한 리전에서만 수정됩니다. 충돌 가능성이 낮고, 업무 경계가 명확한 시스템에서 현실적입니다.

Event Sourcing 방식은 이벤트 로그를 사실상의 원장으로 보고 DB를 projection으로 다룹니다. 감사와 재처리에 유리하지만, retention, compaction, schema evolution, replay 정책을 더 엄격하게 설계해야 합니다. Kafka를 event log로 사용할 수는 있지만, “Kafka가 있으니 자동으로 Event Sourcing이 된다”는 뜻은 아닙니다.

Multi-Writer 방식은 양쪽 리전이 같은 데이터를 직접 수정하는 방식입니다. 가장 Active-Active답게 보이지만 가장 위험합니다. 충돌 해결 정책이 반드시 필요합니다.

version 비교
merge 정책
last write wins
compensation
사용자 또는 관리자 개입

일반적인 업무 시스템이라면 Multi-Writer는 기본 선택지가 아니라 마지막 선택지에 가깝습니다.

가장 위험한 순간은 장애 전환이다

평상시에는 Home Region 모델이 잘 동작하는 것처럼 보입니다. 문제는 장애 전환 시점입니다.

Region A가 특정 데이터를 소유하고 있다가 장애가 발생했다고 가정해보겠습니다. Region B가 쓰기를 이어받으려면 단순히 라우팅만 바꿔서는 안 됩니다.

먼저 Region A가 더 이상 같은 데이터를 쓸 수 없도록 막아야 합니다. 그렇지 않으면 네트워크 단절, 지연된 복구, 부분 장애 상황에서 두 리전이 동시에 자신이 owner라고 믿는 split-brain 상태가 생길 수 있습니다.

Authoritative ownership store가 owner epoch를 증가시키고 쓰기 경계가 이전 리전의 요청을 거부하는 흐름
장애 전환은 트래픽 라우팅 변경만이 아닙니다. 모든 쓰기 경계가 최신 owner와 epoch를 검증해야 이전 writer를 실제로 fencing할 수 있습니다.

안전한 Ownership 이전에는 보통 다음 요소가 필요합니다.

강한 일관성을 제공하는 authoritative ownership store
owner epoch 또는 fencing token
모든 쓰기 경계에서 owner와 epoch 검증
이전 owner의 쓰기 차단 또는 lease 만료
수신 리전의 replication lag 확인
Inbox 처리 완료 여부 확인
전환 후 reconcile 절차
failback 절차

Routing table은 요청을 새 리전으로 보내는 데는 유용하지만 이전 writer를 막는 권한 저장소는 아닙니다. Region A가 routing 계층이나 ownership store와 단절된 상태에서도 DB에 접근할 수 있다면 계속 쓸 수 있습니다. 따라서 DB의 conditional write, 최신 epoch를 검증하는 공통 write gateway, 만료 가능한 lease처럼 이전 writer가 우회할 수 없는 경계에서 검증해야 합니다.

핵심은 “새 리전이 쓰기 시작한다”가 아니라 “이전 리전이 더 이상 쓸 수 없다는 것을 보장한다”입니다.

계획 전환과 비계획 전환은 다르다

계획된 점검이나 리전 이전에서는 쓰기를 먼저 멈추고 파이프라인이 따라온 뒤 Ownership을 넘길 수 있습니다.

Region A 신규 쓰기 중단
-> Outbox publish 완료 확인
-> cross-cluster replication과 Region B Inbox 반영 완료 확인
-> owner를 B로 변경하고 epoch 증가
-> B에서 새 epoch로 쓰기 시작

비계획 장애는 다릅니다. Region A가 완전히 단절되면 마지막 DB transaction과 Outbox 이벤트가 다른 리전에 도달했는지 증명하지 못할 수 있습니다. 이때 선택지는 정합성을 위해 쓰기를 계속 차단하거나, 정의된 RPO 범위의 데이터 유실 가능성을 받아들이고 강제 전환하는 것입니다.

따라서 failover 정책에는 “lag이 얼마 이하면 전환한다”뿐 아니라 lag을 측정할 수 없을 때 누가 어떤 근거로 강제 전환하는가, 그리고 그때 허용하는 RPO가 얼마인지까지 포함되어야 합니다. Kafka가 살아 있다는 사실만으로 이 결정을 대신할 수는 없습니다.

Kafka key는 순서 보장 단위를 따라야 한다

Kafka를 리전 간 이벤트 허브로 쓸 때 partition key도 중요합니다.

같은 aggregate에 대한 이벤트는 가능한 한 같은 partition으로 들어가야 합니다. 그래야 해당 aggregate 안에서 이벤트 순서를 유지하기 쉽습니다.

예를 들어 주문별 이벤트 순서가 중요하다면 order_id가 key가 될 수 있습니다. Tenant 전체의 변경 순서가 실제 업무 규칙이라면 tenant_id를 선택할 수 있지만, 단순히 Ownership을 tenant 단위로 관리한다는 이유만으로 모든 주문을 하나의 key에 모을 필요는 없습니다.

Key를 너무 큰 단위로 잡으면 특정 partition에 부하가 몰릴 수 있습니다. 반대로 너무 작은 단위로 잡으면 업무적으로 필요한 순서 보장이 깨질 수 있습니다. Ownership 단위와 ordering 단위는 서로 호환되어야 하지만 반드시 같을 필요는 없습니다. Kafka의 순서 보장은 topic 전체가 아니라 partition 안에서 제공됩니다.

운영에서는 토폴로지보다 상태 전이가 중요하다

Active-Active DR을 그림으로 그리면 Kafka 클러스터, DB, 애플리케이션, 복제 화살표가 먼저 보입니다. 하지만 실제 운영에서 더 중요한 것은 상태 전이입니다.

이 데이터의 현재 owner는 어디인가
owner epoch는 몇인가
반대 리전은 어디까지 이벤트를 반영했는가
현재 lag는 허용 범위 안인가
failover 중 어느 단계인가
오래된 writer가 아직 살아 있지는 않은가

이런 질문은 Kafka topic 설정만으로 답할 수 없습니다. Kafka cross-cluster replication lag, outbox backlog, inbox backlog, 수신 DB의 마지막 적용 event version, ownership 상태, routing 상태, 애플리케이션의 write rejection 로그를 함께 봐야 합니다.

Kafka 같은 클러스터형 리소스는 코드 설계와 운영 신호가 강하게 연결됩니다. Konduo는 Kafka 같은 운영 대상을 플러그인으로 연결하고, 리소스 상태와 메트릭 근거, 진단, 알림 대응을 하나의 운영 흐름에서 확인할 수 있도록 설계된 통합 관리 운영 플랫폼입니다.

결론

Kafka는 Active-Active 환경에서 강력한 이벤트 허브가 될 수 있습니다. 이벤트를 안정적으로 저장하고, 전달하고, 다시 읽게 해줍니다.

하지만 Active-Active의 핵심 난제는 Kafka가 아니라 Ownership입니다.

어떤 데이터가 어느 리전에서 쓰일 수 있는지, 그 권한을 장애 시점에 어떻게 안전하게 옮길지, 오래된 writer를 어떻게 막을지, 중복 이벤트를 어떻게 무해하게 만들지가 먼저 결정되어야 합니다.

그 결정이 선 뒤에야 Kafka는 리전 사이의 변경 이벤트를 안정적으로 전달하는 계층으로 제 역할을 합니다.

Active-Active의 성공 여부는 Kafka 클러스터 구성이 아니라
데이터 기록 전략과 Ownership 강제 방식에 의해 결정된다.

함께 읽기 좋은 글