Kafka 마스터 — Fan-Out / Fan-In 패턴

2026-05-03확률과 통계 마스터 노트

카프카 심화편 (9~15편) 3편. Fan-Out 패턴(1 메시지 → N 토픽)이 토픽별 독립 보존·부하 분산을 가능케 하는 원리, Fan-In 패턴(N 토픽 → 1 컨슈머)으로 여러 스트림을 한 곳에서 집계, Flux.zip/combineLatest로 Fan-In 구현, 토픽 분리 vs 통합의 트레이드오프, Kafka Streams와의 차이까지.

이 글은 카프카 마스터 노트 시리즈의 열한 번째 편입니다. 2편(StreamBridge)에서 동적 라우팅을 봤다면, 이번엔 메시지의 분산·집계 기본 패턴 — Fan-Out / Fan-In.

마이크로서비스 통신의 두 축. 하나의 이벤트를 여러 서비스가 받아 처리(Fan-Out), 여러 서비스 결과를 하나로 모음(Fan-In). 이 두 패턴이 분산 시스템의 토대.

처음 Fan-Out / Fan-In이 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, 이름이 추상적입니다. Fan-Out·Fan-In이 정확히 어떤 모양? 둘째, **"왜 토픽을 분리하나"**가 막연합니다. 한 토픽으로 통합하면 안 되나?

해결법은 한 가지예요. "부채(Fan)" 모양 비유로 묶는 것. Fan-Out = 부채가 펼쳐지듯 하나가 N으로, Fan-In = 부채가 접히듯 N이 하나로. 토픽 분리 = 각 부서가 자기 우편함을 가짐, 통합 = 모두가 한 우편함을 공유. 이 그림이 잡히면 트레이드오프가 보입니다.

Fan-Out — 1 → N 분배

[OrderCreated 이벤트]
       ↓
   ┌───┴───┬─────┬─────┐
   ↓       ↓     ↓     ↓
[Payment] [Inv] [Ship] [Notif]

한 이벤트를 여러 서비스가 받아 처리.

두 가지 구현 방식

방식 1 — 단일 토픽 + 다중 그룹

order-events 토픽
   ├── group=payment-service
   ├── group=inventory-service
   ├── group=shipping-service
   └── group=notification-service

각 그룹이 독립적으로 모든 메시지 소비. 4편(기초편 (1~8편))에서 본 패턴.

장점 — 단순, 토픽 적음. 단점 — 토픽별 보존 정책 같음, 모든 서비스가 모든 메시지 봄.

방식 2 — 다중 토픽 (StreamBridge)

public void publish(OrderEvent order) {
    streamBridge.send("payment-events", order);
    streamBridge.send("inventory-events", order);
    streamBridge.send("shipping-events", order);
    streamBridge.send("notification-events", order);
}
[OrderCreated]
   ↓ Fan-Out (multiple sends)
   ├── payment-events 토픽 → Payment Service
   ├── inventory-events 토픽 → Inventory Service
   ├── shipping-events 토픽 → Shipping Service
   └── notification-events 토픽 → Notification Service

장점:

  • 토픽별 독립 보존 정책 — 알림은 1일, 결제는 30일
  • 각 서비스 부하 분리
  • 토픽 단위 보안 (ACL 차등)
  • 스키마 진화 독립

단점 — 토픽 수 늘어남, 발행 측 부담.

여기서 정말 중요한 시험 함정 — 마이크로서비스에선 다중 토픽 권장. 각 도메인이 자기 토픽 가짐 = 명확한 경계. 서비스 추가도 토픽 신설로 격리.

Fan-In — N → 1 집계

[Payment]  → payment-events  ─┐
[Inventory] → inventory-events ─┼→ [Order Service] (모두 모음)
[Shipping]  → shipping-events  ─┘

Order Service가 여러 결과를 모아 최종 상태 결정.

단순 Fan-In — 다중 구독

@Bean
public Consumer<PaymentEvent> onPayment() { ... }

@Bean
public Consumer<InventoryEvent> onInventory() { ... }

@Bean
public Consumer<ShippingEvent> onShipping() { ... }

각 토픽에 별도 컨슈머. DB로 상태 모음.

동기 집계 — Flux.zip

@Bean
public Function<Flux<OrderEvent>, Flux<OrderResult>> aggregate() {
    return orders -> Flux.zip(
        kafkaConsumer.receiveFrom("payment-events"),
        kafkaConsumer.receiveFrom("inventory-events"),
        kafkaConsumer.receiveFrom("shipping-events")
    )
    .map(tuple -> aggregate(tuple.getT1(), tuple.getT2(), tuple.getT3()));
}

여기서 시험 함정이 하나 있어요. Flux.zip은 같은 인덱스끼리 묶음. 모든 스트림이 같은 페이스로 도착해야. Order별 매칭은 ID 기반 별도 처리.

실전 — 주문 결과 집계

여러 서비스 응답을 Order Service가 모음:

@Service
public class OrderAggregator {
    private Map<String, OrderState> states = new ConcurrentHashMap<>();

    @Bean
    public Consumer<PaymentEvent> onPayment() {
        return event -> {
            states.computeIfAbsent(event.getOrderId(), k -> new OrderState())
                .setPayment(event);
            tryComplete(event.getOrderId());
        };
    }

    @Bean
    public Consumer<InventoryEvent> onInventory() {
        return event -> {
            states.computeIfAbsent(event.getOrderId(), k -> new OrderState())
                .setInventory(event);
            tryComplete(event.getOrderId());
        };
    }

    private void tryComplete(String orderId) {
        OrderState state = states.get(orderId);
        if (state.isComplete()) {
            // 둘 다 도착 → 최종 처리
            states.remove(orderId);
            publishResult(state);
        }
    }
}

여기서 정말 중요한 시험 함정 — 메모리 기반 집계는 인스턴스 다운에 취약. 운영 환경엔 DB 또는 Redis 사용. 또는 Kafka Streams의 KTable.

토픽 분리 전략

도메인 단위

order-events (주문 도메인)
payment-events (결제 도메인)
inventory-events (재고 도메인)

이벤트 종류별

order-created
order-updated
order-cancelled

우선순위별

high-priority-events
normal-priority-events
low-priority-events

데이터 양·보존별

events-7days
events-30days
events-365days

여기서 시험 함정이 하나 있어요. 너무 세분화하면 토픽 폭발. 한 도메인 안 이벤트는 한 토픽에 + 헤더로 종류 구분도 가능.

Fan-Out vs Kafka Streams

Kafka Streams는 스트림 처리 라이브러리 — branching, joining, aggregation 모두 자체 제공.

// Kafka Streams
stream.split()
    .branch((k, v) -> v.isHighValue(), Branched.as("high"))
    .branch((k, v) -> v.isUrgent(), Branched.as("urgent"))
    .defaultBranch();
측면 SCS + StreamBridge Kafka Streams
학습 곡선 낮음 높음
상태 관리 DB·Redis KTable·StateStore
Reactive O X
복잡 패턴 직접 구현 라이브러리 제공

여기서 정말 중요한 시험 함정 — WebFlux 마이크로서비스 → SCS / 데이터 분석 파이프라인 → Kafka Streams. 도구 선택이 다름.

Fan-Out 시 일관성

여러 토픽에 발행 시:

streamBridge.send("payment-events", order);  // 성공
streamBridge.send("inventory-events", order); // 실패!

부분 발행 = 일관성 깨짐. 해결 — Outbox Pattern (이 시리즈 15편).

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 3편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • Fan-Out = 1 → N 분배 (부채 펼치듯)
  • Fan-In = N → 1 집계 (부채 접듯)
  • Fan-Out 두 방식 — 단일 토픽 + 다중 그룹 / 다중 토픽
  • 마이크로서비스 = 다중 토픽 권장 (도메인 경계)
  • 토픽별 독립 — 보존·부하·보안·스키마
  • Fan-In — 다중 구독 / Flux.zip
  • Flux.zip은 같은 인덱스 묶음 (페이스 같아야)
  • ID별 매칭은 별도 처리 (Map·DB·Redis)
  • 메모리 집계는 인스턴스 다운에 취약 — DB 또는 Redis
  • 또는 Kafka Streams KTable
  • 토픽 분리 전략 — 도메인 / 이벤트 종류 / 우선순위 / 보존 기간
  • 너무 세분화 = 토픽 폭발
  • 도메인 안 이벤트는 한 토픽 + 헤더로 종류 구분 가능
  • SCS + StreamBridge vs Kafka Streams
  • WebFlux 마이크로서비스 → SCS
  • 데이터 파이프라인 → Kafka Streams
  • 학습 곡선·상태 관리·Reactive·복잡 패턴 차이
  • Fan-Out 시 부분 발행 = 일관성 깨짐 → Outbox Pattern (7편)

시리즈 다른 편

공식 문서: Microservices Patterns — Fan-Out 에서 더 깊이.

다음 글(4편)에서는 Spring Cloud Stream Tips — content-type, Native Encoding, DLT, 파티셔닝, typeId까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!