Kafka 마스터 — StreamBridge·동적 라우팅

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

카프카 심화편 (9~15편) 2편. 정적 바인딩의 한계(1 빈 = 1 destination 고정)와 StreamBridge가 푸는 방법, 런타임에 토픽 동적 결정 패턴, 콘텐츠 기반 라우팅 구현(switch + StreamBridge.send), 부수 효과로 발행하는 흐름, acknowledgement 호출 시점, 정적 vs 동적 바인딩 선택 기준까지.

이 글은 카프카 마스터 노트 시리즈의 열 번째 편입니다. 1편(SCS 기초)에서 정적 바인딩(Function·Supplier·Consumer)을 다졌다면, 이번엔 그 한계를 푸는 도구 — StreamBridge.

마이크로서비스에서 모든 라우팅을 정적으로 결정 못합니다. 주문 금액별 토픽, 사용자 등급별 채널, 동적 알림 발송… 런타임 결정이 필요할 때가 있어요. StreamBridge가 그 답.

처음 StreamBridge가 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, **"왜 정적 바인딩으로 안 되나"**가 막연합니다. Function 빈을 여러 개 만들면 되지 않나? 둘째, "부수 효과(side effect)" 라는 말이 모호합니다. 함수형 빈이 아닌데 어떻게 메시지가 발행되나?

해결법은 한 가지예요. "우체통 vs 우체부" 비유로 묶는 것. 정적 바인딩 = 항상 같은 우체통(고정 destination), StreamBridge = 들고 다니는 우체부(어디든 보낼 수 있음). 부수 효과 = 우체부가 처리 도중 슬쩍 보내는 편지. 이 그림이 잡히면 흐름이 보입니다.

정적 바인딩의 한계

@Bean
public Function<OrderEvent, PaymentEvent> orderToPayment() {
    return order -> new PaymentEvent(...);
}
spring.cloud.stream.bindings.orderToPayment-out-0:
  destination: payment-events

이 빈은 항상 payment-events 토픽으로. 다른 토픽으로 보낼 수 없음.

왜 문제?

// 주문 금액별 다른 토픽
public void process(OrderEvent order) {
    if (order.getAmount() > 1000) {
        // → high-value-orders 토픽
    } else if (order.isUrgent()) {
        // → urgent-orders 토픽
    } else {
        // → normal-orders 토픽
    }
}

3개 다른 토픽 = 3개 빈? 빈 폭발. 그리고 동적 토픽명(order-{userId})은 빈으로 표현 불가.

StreamBridge — 동적 발행

@Service
public class OrderRouter {
    @Autowired
    private StreamBridge streamBridge;

    public void route(OrderEvent order) {
        String destination = decideTopic(order);   // 런타임 결정
        streamBridge.send(destination, order);
    }

    private String decideTopic(OrderEvent order) {
        if (order.getAmount() > 1000) return "high-value-orders";
        if (order.isUrgent()) return "urgent-orders";
        return "normal-orders";
    }
}

StreamBridge.send(destination, payload) — 어느 토픽이든 런타임에 결정.

여기서 정말 중요한 시험 함정 — StreamBridge는 Spring Cloud Stream 빈. @Autowired로 주입. 직접 new X.

콘텐츠 기반 라우팅 패턴

@Bean
public Consumer<Flux<OrderEvent>> orderRouter(StreamBridge streamBridge) {
    return orders -> orders
        .doOnNext(order -> {
            String destination = switch (order.getType()) {
                case PRIORITY -> "priority-orders";
                case BULK -> "bulk-orders";
                case STANDARD -> "standard-orders";
            };
            streamBridge.send(destination, order);
        })
        .subscribe();
}

여기서 시험 함정이 하나 있어요. StreamBridge.send()는 동기 호출. Reactive 흐름에 박을 땐 부수 효과(side effect)로 처리. doOnNext·tap 같은 곳에서.

Function + StreamBridge 결합

Function의 출력을 발행하는 동시에, 조건부 추가 발행:

@Bean
public Function<OrderEvent, PaymentEvent> processWithBranching(StreamBridge streamBridge) {
    return order -> {
        // 일반 출력 (정적 바인딩)
        PaymentEvent payment = toPayment(order);

        // 조건부 추가 (동적 라우팅)
        if (order.isVip()) {
            streamBridge.send("vip-orders", order);
        }

        return payment;
    };
}

여기서 정말 중요한 시험 함정 — 함수형 빈의 반환값 = 정적 라우팅 / StreamBridge = 동적 라우팅. 둘 결합 가능.

acknowledgement 호출 시점

수동 acknowledgement 모드에선 처리 + 발행 모두 끝난 후 ack:

@Bean
public Consumer<Message<OrderEvent>> processOrder(StreamBridge streamBridge) {
    return message -> {
        OrderEvent order = message.getPayload();
        Acknowledgment ack = message.getHeaders()
            .get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class);

        try {
            streamBridge.send("processed-orders", processed(order));
            ack.acknowledge();   // 발행 성공 후
        } catch (Exception e) {
            // ack 안 함 → 재처리
        }
    };
}

정적 vs 동적 — 선택 기준

상황 선택
토픽 1개 고정 정적 (Function)
토픽 N개 고정 (조건 분기) StreamBridge
동적 토픽명 (user-{id}) StreamBridge
Pure 변환 정적
부수 효과 발행 StreamBridge

여기서 시험 함정이 하나 있어요. 혼합도 OK. 함수형 빈으로 주 흐름 + StreamBridge로 보조 알림 흐름.

메시지 + 헤더 함께

streamBridge.send(
    "events",
    MessageBuilder.withPayload(order)
        .setHeader("trace-id", traceId)
        .setHeader("source", "order-service")
        .build()
);

Message<T> 빌더로 헤더 포함.

출력 채널 동적 생성

spring.cloud.stream.dynamic-destination-cache-size: 10

자주 쓰는 destination 캐시. 매번 생성 비용 회피.

통합 테스트

@Autowired
private StreamBridge streamBridge;

@Autowired
private TestChannelBinderConfiguration testBinder;

@Test
void test() {
    streamBridge.send("test-topic", testEvent);

    // OutputDestination 검증
    Message<byte[]> received = testBinder.getOutputDestination()
        .receive(1000, "test-topic");
    assertNotNull(received);
}

spring-cloud-stream-test-binder 의존성으로.

실전 예시 — 다단 라우팅

@Bean
public Consumer<Flux<OrderEvent>> orderProcessor(StreamBridge bridge) {
    return orders -> orders
        .doOnNext(order -> {
            // 1. 알림 (조건부)
            if (order.isFirstTime()) {
                bridge.send("welcome-emails", order);
            }

            // 2. 분석 (모든 주문)
            bridge.send("analytics-events", order);

            // 3. 처리 (등급별)
            String tier = order.getCustomerTier();
            bridge.send("orders-" + tier, order);

            // 4. 메트릭
            bridge.send("metrics-events", new MetricEvent(order));
        })
        .subscribe();
}

한 컨슈머 + 4 동적 발행. 마이크로서비스 분산의 출발점.

동적 라우팅 안티패턴

여기서 정말 중요한 시험 함정 — 너무 많은 동적 토픽 = 운영 지옥. 토픽 폭발(order-user-1, order-user-2, ...) 시 메타데이터 부담·모니터링 어려움. 동적 토픽은 신중히. 사용자 단위는 Key 활용 권장.

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

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

  • 정적 바인딩 한계 — 1 빈 = 1 destination 고정
  • 다중 토픽·동적 토픽 표현 X
  • StreamBridge = 런타임 동적 발행
  • @Autowired로 주입
  • streamBridge.send(destination, payload)
  • 동기 호출 — Reactive 흐름엔 부수 효과
  • doOnNext·tap에서 호출
  • 콘텐츠 기반 라우팅 — switch + StreamBridge 표준 패턴
  • Function + StreamBridge 혼합 OK
  • 함수형 빈 반환 = 정적 / StreamBridge = 동적
  • ack는 처리 + 발행 모두 끝난 후
  • 발행 실패 시 ack 안 하기
  • 정적 vs 동적 선택 — 토픽 1 고정 vs N 조건 분기 vs 동적 토픽명
  • Message<T> 빌더로 헤더 포함
  • dynamic-destination-cache-size로 destination 캐시
  • 테스트 — TestChannelBinderConfiguration + OutputDestination
  • 너무 많은 동적 토픽은 운영 지옥 — 사용자별 토픽 X, Key 활용
  • 메타데이터 부담·모니터링 어려움

시리즈 다른 편

공식 문서: Spring Cloud Stream — StreamBridge 에서 더 깊이.

다음 글(3편)에서는 Fan-Out / Fan-In — 1 메시지를 N 토픽으로, N 토픽을 1 컨슈머로 묶는 분산 패턴까지 풀어 갑니다.

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

답글 남기기

error: Content is protected !!