Kafka 마스터 — Spring Cloud Stream Tips & Tricks

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

카프카 심화편 (9~15편) 4편. content-type 기본값 application/json과 변경 방법, 네이티브 인코딩으로 Go·Python 같은 이기종 시스템 연동, typeId 헤더와 trusted.packages 보안 설정, 전역 default 설정, Reactive Kafka Binder의 트랜잭션 미지원과 그 해결책 Outbox 예고, Dead Letter Topic 설정, SCS 파티셔닝 전략까지.

이 글은 카프카 마스터 노트 시리즈의 열두 번째 편입니다. 1~3편이 SCS 핵심이었다면, 이번엔 운영에서 부딪히는 디테일 — content-type, Native Encoding, DLT, 파티셔닝, typeId.

5~7편 Saga·Outbox에 들어가기 전 마무리. SCS의 잘 안 보이는 함정 모음.

처음 SCS 디테일이 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, 이름이 비슷한 옵션이 많습니다. content-type·typeId·trusted.packages·Native Encoding·default… 어느 게 어디서 동작하나? 둘째, 운영에서야 나타나는 함정들이라 직관이 안 잡힙니다. 이기종 시스템·DLT 설정·파티셔닝 같은.

해결법은 한 가지예요. 각 옵션을 "왜 만들어졌나"로 보는 것. content-type = 직렬화 방식, typeId = 자바 시스템 사이 클래스 정보, Native Encoding = 자바 외 시스템 호환, DLT = 처리 실패 격리, 파티셔닝 = 부하·순서 제어. 이 그림이 잡히면 디테일은 채우기만.

content-type — 메시지 형식

기본값 application/json. 변경 가능:

spring.cloud.stream.bindings.orderEvents-in-0:
  content-type: application/json   # 기본
content-type 의미
application/json JSON (기본)
application/octet-stream 바이트 배열
application/x-java-object Java 객체 (직접)
text/plain 텍스트

여기서 시험 함정이 하나 있어요. content-type이 잘못되면 deserialize 실패. 발행자·소비자 같아야. 다른 시스템 연동 시 명시 권장.

typeId — Java 객체 직렬화 정보

Spring의 JsonSerializer가 자동으로 헤더에 클래스 이름 추가:

헤더:
  __TypeId__: com.example.OrderEvent

소비자는 이 정보로 정확한 타입 deserialize.

trusted.packages — 보안 설정

spring.kafka.consumer.properties:
  spring.json.trusted.packages: "com.example,com.acme"

신뢰 패키지 명시 필수. 안 하면 임의 클래스 deserialize 가능 → RCE 위험.

여기서 정말 중요한 시험 함정 — trusted.packages는 보안 핵심. Spring 4.1.5+ 기본 차단 (* 사용 X). 명시적으로 안전 패키지만.

Native Encoding/Decoding — 이기종 호환

Java SCS의 typeId 헤더 때문에 Go·Python 시스템과 호환 X:

Java Producer → typeId 헤더 박힘
                   ↓
Python Consumer → "이게 뭐야" (Java 클래스 이름)

해결 — Native Encoding:

spring.cloud.stream.bindings.orderEvents-out-0:
  producer:
    use-native-encoding: true

spring.cloud.stream.kafka.bindings.orderEvents-out-0:
  producer:
    configuration:
      value.serializer: org.apache.kafka.common.serialization.ByteArraySerializer

SCS의 변환 로직 우회 → 표준 Kafka serializer 사용. typeId 헤더 X.

대칭적으로 Native Decoding (consumer side):

spring.cloud.stream.bindings.orderEvents-in-0:
  consumer:
    use-native-decoding: true

DLT (Dead Letter Topic)

처리 실패 메시지 자동 별도 토픽으로:

spring.cloud.stream.kafka.bindings.orderEvents-in-0:
  consumer:
    enable-dlq: true
    dlq-name: order-events.dlt    # 기본은 "error.<destination>.<group>"
    dlq-producer-properties:
      configuration:
        acks: all

처리 실패 → 자동으로 order-events.dlt 토픽으로.

재시도 설정

spring.cloud.stream.bindings.orderEvents-in-0.consumer:
  max-attempts: 3              # 3회 재시도
  back-off-initial-interval: 1000
  back-off-max-interval: 10000
  back-off-multiplier: 2.0

3회 모두 실패 → DLT로.

여기서 정말 중요한 시험 함정 — DLT는 운영 필수. 안 켜면 처리 실패 메시지가 컨슈머 영원히 막음. 켜기만 하면 격리 자동.

파티셔닝 (SCS 측)

spring.cloud.stream.bindings.orderEvents-out-0:
  producer:
    partition-key-expression: payload.userId
    partition-count: 6

payload.userId SpEL 표현식으로 Key 추출. 같은 user → 같은 파티션.

# 명시적 파티션 결정
spring.cloud.stream.bindings.orderEvents-out-0:
  producer:
    partition-selector-expression: payload.region
    partition-count: 3

여기서 시험 함정이 하나 있어요. partition-key-expression vs partition-selector-expression. key는 hash 기반, selector는 직접 0~N 반환. 일반은 key.

컨슈머 측 — partitioned

spring.cloud.stream.bindings.orderEvents-in-0:
  consumer:
    partitioned: true

파티션 인덱스 기반 분배.

전역 기본 설정 — default

spring.cloud.stream:
  default:
    content-type: application/json
    consumer:
      auto-startup: true
    producer:
      use-native-encoding: false

모든 binding에 적용. 개별 binding이 override.

Reactive Kafka Binder — 트랜잭션 미지원

spring.cloud.stream.kafka.binder:
  transaction:
    transaction-id-prefix: tx-

여기서 정말 중요한 시험 함정 — Reactive Binder는 Kafka Transaction 미지원. 일반 Kafka Binder만. WebFlux + 정확히 한 번 = Outbox Pattern 필수 (이 시리즈 15편).

headers + Message

@Bean
public Function<Message<OrderEvent>, Message<PaymentEvent>> processWithHeaders() {
    return msg -> MessageBuilder
        .withPayload(toPayment(msg.getPayload()))
        .copyHeaders(msg.getHeaders())
        .setHeader("processed-at", Instant.now().toString())
        .build();
}

헤더 보존하며 추가.

동적 destination 캐시

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

StreamBridge의 동적 destination 캐싱.

consumer group 자동 생성

spring.cloud.stream.bindings.orderEvents-in-0:
  group: order-processor   # 명시적

group 안 명시하면 매번 새 그룹 (anonymous) → 모든 메시지 새로 받음. 운영에선 명시 필수.

여기서 시험 함정이 하나 있어요. group 누락 = anonymous consumer. 매 시작 때 다른 그룹 ID. 처리 진행 안 추적. 운영 절대 X.

DefaultBinder — 다중 binder 환경

spring.cloud.stream:
  binders:
    kafka1:
      type: kafka
      environment.spring.cloud.stream.kafka.binder.brokers: localhost:9092
    kafka2:
      type: kafka
      environment.spring.cloud.stream.kafka.binder.brokers: other-host:9092
  bindings:
    orderEvents-in-0:
      binder: kafka1
    paymentEvents-out-0:
      binder: kafka2

여러 카프카 클러스터 동시 사용.

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

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

  • content-type — 기본 application/json
  • 발행자·소비자 같아야
  • typeId 헤더 — Java 클래스 정보
  • spring.json.trusted.packages = 보안 핵심 (RCE 방어)
  • Spring 4.1.5+ 기본 차단
  • Native Encoding/Decoding = 이기종 시스템 (Go·Python) 연동
  • typeId 헤더 우회 + 표준 serializer
  • DLTenable-dlq: true
  • 처리 실패 자동 격리
  • 운영 필수 — 안 켜면 컨슈머 막힘
  • 재시도 — max-attempts, back-off
  • 파티셔닝 — partition-key-expression (SpEL)
  • partition-key vs partition-selector — hash vs 직접 반환
  • 컨슈머 — partitioned: true
  • 전역 defaultspring.cloud.stream.default
  • 개별 binding이 override
  • Reactive Binder = Kafka Transaction 미지원 → Outbox 필요 (7편)
  • Message<T> + headers — copyHeaders로 헤더 보존
  • group 누락 = anonymous (매 시작 다른 그룹) — 운영 X
  • 다중 binder — binders 정의 + binding마다 binder 지정

시리즈 다른 편

공식 문서: Spring Cloud Stream — Apache Kafka Binder 에서 더 깊이.

다음 글(5편)에서는 Saga Choreography — 분산 트랜잭션의 한 축, 중앙 관제 없이 이벤트로만 협력하는 구현까지 풀어 갑니다.

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

답글 남기기

error: Content is protected !!