카프카 심화편 (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
- DLT —
enable-dlq: true - 처리 실패 자동 격리
- 운영 필수 — 안 켜면 컨슈머 막힘
- 재시도 —
max-attempts, back-off - 파티셔닝 —
partition-key-expression(SpEL) - partition-key vs partition-selector — hash vs 직접 반환
- 컨슈머 —
partitioned: true - 전역 default —
spring.cloud.stream.default - 개별 binding이 override
- Reactive Binder = Kafka Transaction 미지원 → Outbox 필요 (7편)
Message<T>+ headers — copyHeaders로 헤더 보존- group 누락 = anonymous (매 시작 다른 그룹) — 운영 X
- 다중 binder —
binders정의 + binding마다binder지정
시리즈 다른 편
- 1편 — EDA·Kafka 기초·KRaft
- 2편 — Topic·Partition·Offset
- 3편 — Producer·Consumer 동작
- 4편 — Consumer Group·리밸런싱
- 5편 — Reactor Kafka
- 6편 — Cluster·HA·Best Practices
- 7편 — 배치·에러·트랜잭션
- 8편 — Spring Kafka·테스트·보안
- 9편 — Spring Cloud Stream 기초
- 10편 — StreamBridge 동적 라우팅
- 11편 — Fan-Out / Fan-In
- 12편 — SCS Tips & Tricks (현재 글)
- 13편 — Saga 코레오그래피
- 14편 — Saga 오케스트레이터
- 15편 — Transactional Outbox
공식 문서: Spring Cloud Stream — Apache Kafka Binder 에서 더 깊이.
다음 글(5편)에서는 Saga Choreography — 분산 트랜잭션의 한 축, 중앙 관제 없이 이벤트로만 협력하는 구현까지 풀어 갑니다.