Engineering Note

Kafka Active-Active의 핵심은 Kafka가 아니라 쓰기 소유권이다

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

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

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

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

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

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

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

핵심은 이렇습니다.

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

Active-Active라는 말의 함정

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

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

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

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

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

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

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

가장 현실적인 기본 구조는 홈 리전(Home Region) 방식입니다.

각 데이터, 더 정확히는 각 애그리게이트나 테넌트에 쓰기 소유 리전이 있습니다. 읽기는 양쪽 리전에서 로컬 DB를 통해 처리할 수 있지만, 쓰기는 해당 데이터의 홈 리전에서만 수행합니다.

거울 대칭으로 구성된 두 리전의 Kafka가 서로 이벤트를 복제하고 각 리전의 Inbox가 로컬 Kafka에서 이벤트를 소비하는 흐름
리전 간 복제는 Kafka 계층 사이에서 일어나고, 각 리전의 Inbox는 로컬 Kafka에서 이벤트를 소비해 DB에 반영합니다. 다른 메시징 시스템을 선택하더라도 쓰기 가능 리전은 쓰기 소유권 모델이 결정합니다.

흐름은 다음과 같습니다.

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

여기서 클러스터 간 복제는 Kafka 브로커의 복제 계수(replication factor)와 다른 계층입니다. Apache Kafka에서는 MirrorMaker 2 같은 도구로 A -> B, B -> A 방향의 복제 흐름을 구성할 수 있고, 상용 Kafka 서비스의 클러스터 연결 기능이나 별도 복제 제품을 선택할 수도 있습니다.

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

따라서 “읽기는 항상 로컬 DB”라는 규칙도 절대 규칙은 아닙니다. 대부분의 조회는 로컬 DB로 처리하되, 강한 최신성이 필요한 작업은 홈 리전으로 보내거나, 복제 지연과 소유권 정보를 함께 보고 판단해야 합니다.

Kafka만이 정답은 아니다

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

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

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

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

Kafka를 선택할 때의 가장 큰 장점은 하나의 변경 이벤트를 여러 컨슈머 그룹이 각자의 오프셋으로 읽고, 장애 복구나 프로젝션 재생성이 필요할 때 다시 처리할 수 있다는 점입니다. aggregate_id를 키로 사용하면 같은 애그리게이트의 순서도 파티션 안에서 유지할 수 있습니다. Outbox, CDC, 스트림 처리 도구가 풍부하다는 점도 실용적입니다.

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

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

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

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

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

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

Kafka는 이벤트를 저장하고 전달합니다. 컨슈머가 잠시 느려져도 이벤트를 다시 읽을 수 있고, 여러 컨슈머 그룹이 같은 이벤트 스트림을 독립적으로 처리할 수 있습니다. 파티션 키를 잘 잡으면 같은 애그리게이트에 대한 이벤트 순서도 유지할 수 있습니다.

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

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

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

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

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

Outbox와 Inbox는 지역 간 계약이다

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

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

컨슈머 쪽도 마찬가지입니다. Kafka 복제, 중계기 재시도, 컨슈머 재처리 때문에 같은 이벤트가 다시 들어올 수 있습니다. 수신 리전은 Inbox나 멱등 처리기로 중복을 견뎌야 합니다.

중요한 점은 Inbox 기록과 업무 DB 변경도 같은 로컬 DB 트랜잭션으로 묶어야 한다는 것입니다. Inbox만 기록하고 업무 변경 전에 실패하면 재처리할 근거를 잃습니다. 반대로 업무 DB만 바뀌고 Inbox 기록이 실패하면 같은 변경을 다시 수행할 수 있습니다. event_id에 고유 제약 조건을 두고, Inbox 레코드 삽입과 업무 상태 변경을 함께 커밋한 뒤 Kafka 오프셋을 전진시키는 방식이 기본입니다. 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 키 후보가 될 수 있습니다. home_regionsource_region은 이벤트의 출처를 추적하는 근거입니다. 다만 필드가 존재하는 것만으로 복제 루프가 차단되지는 않습니다. MirrorMaker 2의 원격 토픽 명명 규칙과 순환 감지, 또는 사용 중인 복제 계층의 소스 별칭과 복제 제외 정책이 함께 필요합니다.

owner_epochversion도 그 자체로 쓰기 주체를 막지 않습니다. 모든 쓰기 경계가 소유권 기준 저장소의 현재 소유 리전과 에포크를 검증하고, 오래된 에포크를 가진 요청을 거부할 때 비로소 쓰기 차단 토큰(fencing token)으로 작동합니다.

대표적인 쓰기 모델

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

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

리전별 쓰기(Regional Write) 방식은 테넌트, 고객, 업무, 지역 같은 단위로 쓰기 리전을 나눕니다.

테넌트 A -> 리전 A
테넌트 B -> 리전 B

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

이벤트 소싱(Event Sourcing) 방식은 이벤트 로그를 사실상의 원장으로 보고 DB를 프로젝션으로 다룹니다. 감사와 재처리에 유리하지만, 보존, 로그 압축, 스키마 변경, 재생 정책을 더 엄격하게 설계해야 합니다. Kafka를 이벤트 로그로 사용할 수는 있지만, “Kafka가 있으니 자동으로 이벤트 소싱이 된다”는 뜻은 아닙니다.

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

버전 비교
병합 정책
최종 쓰기 우선
보상 처리
사용자 또는 관리자 개입

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

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

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

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

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

소유권 기준 저장소가 소유권 에포크를 증가시키고 쓰기 경계가 이전 리전의 요청을 거부하는 흐름
장애 전환은 트래픽 라우팅 변경만이 아닙니다. 모든 쓰기 경계가 최신 소유 리전과 에포크를 검증해야 이전 쓰기 주체를 실제로 차단할 수 있습니다.

안전한 쓰기 소유권 이전에는 보통 다음 요소가 필요합니다.

강한 일관성을 제공하는 소유권 기준 저장소
소유권 에포크 또는 쓰기 차단 토큰
모든 쓰기 경계에서 소유 리전과 에포크 검증
이전 소유 리전의 쓰기 차단 또는 리스 만료
수신 리전의 복제 지연 확인
Inbox 처리 완료 여부 확인
전환 후 데이터 조정 절차
원복 절차

라우팅 테이블은 요청을 새 리전으로 보내는 데는 유용하지만 이전 쓰기 주체를 막는 권한 저장소는 아닙니다. 리전 A가 라우팅 계층이나 소유권 기준 저장소와 단절된 상태에서도 DB에 접근할 수 있다면 계속 쓸 수 있습니다. 따라서 DB의 조건부 쓰기, 최신 에포크를 검증하는 공통 쓰기 게이트웨이, 만료 가능한 리스처럼 이전 쓰기 주체가 우회할 수 없는 경계에서 검증해야 합니다.

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

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

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

리전 A 신규 쓰기 중단
-> Outbox 발행 완료 확인
-> 클러스터 간 복제와 리전 B의 Inbox 반영 완료 확인
-> 소유 리전을 B로 변경하고 에포크 증가
-> B에서 새 에포크로 쓰기 시작

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

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

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

Kafka를 리전 간 이벤트 허브로 쓸 때 파티션 키도 중요합니다.

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

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

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

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

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

이 데이터의 현재 쓰기 소유 리전은 어디인가
현재 소유권 에포크는 몇인가
반대 리전은 어디까지 이벤트를 반영했는가
현재 복제 지연은 허용 범위 안인가
장애 전환 중 어느 단계인가
이전 쓰기 주체가 아직 살아 있지는 않은가

이런 질문은 Kafka 토픽 설정만으로 답할 수 없습니다. Kafka 클러스터 간 복제 지연, Outbox 적체, Inbox 적체, 수신 DB에 마지막으로 적용된 이벤트 버전, 쓰기 소유권 상태, 라우팅 상태, 애플리케이션의 쓰기 거부 로그를 함께 봐야 합니다.

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

결론

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

하지만 Active-Active의 핵심 난제는 Kafka가 아니라 쓰기 소유권입니다.

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

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

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

함께 읽기 좋은 글