Split View: 토스뱅크 Data Engineer (Kafka & Streaming) 합격을 위한 완벽 가이드: 기술스택·면접·공부 로드맵
토스뱅크 Data Engineer (Kafka & Streaming) 합격을 위한 완벽 가이드: 기술스택·면접·공부 로드맵
- 1. 토스뱅크 Real-Time Data 팀 분석
- 2. JD 완전 해부: 라인 바이 라인 분석
- 3. 필수 기술스택 딥다이브
- 4. 면접 예상 질문 30선
- 5. 6개월 학습 로드맵
- 6. 이력서 작성 전략 (JD 기반)
- 7. 포트폴리오 프로젝트 아이디어
- 실전 퀴즈
- 참고 자료
- 마무리: 합격까지의 여정
1. 토스뱅크 Real-Time Data 팀 분석
1-1. 팀 미션: 전사 데이터 인프라의 심장
토스뱅크의 Real-Time Data 팀은 단순한 데이터 파이프라인 운영팀이 아니다. 이 팀의 미션은 토스뱅크 전체 서비스의 데이터 흐름을 실시간으로 보장하는 것이다. 이체, 대출 심사, 사기 탐지, 마케팅 이벤트 — 이 모든 것이 밀리초 단위의 데이터 처리 위에서 돌아간다.
은행이라는 도메인 특성상 데이터의 유실은 곧 금전적 손실이다. 단 하나의 메시지도 잃어버리면 안 되고, 단 한 건의 중복 처리도 허용되지 않는다. 이것이 일반 IT 기업의 데이터 엔지니어링과 핀테크 데이터 엔지니어링의 결정적인 차이다.
Real-Time Data 팀이 책임지는 영역을 정리하면 다음과 같다.
- Kafka Broker Cluster 운영: 수십억 건의 일일 메시지를 안정적으로 처리
- Kafka Client 지원: 전사 개발팀이 사용하는 Spring Boot Kafka Client의 문제 해결
- 데이터 파이프라인: CDC, Kafka Connect, Flink를 활용한 실시간 데이터 흐름 구축
- 이중화 및 DR: Active-Active 구성으로 무중단 서비스 보장
- 실시간 분석 인프라: ClickHouse를 활용한 실시간 데이터 분석 플랫폼
1-2. 기술 스택 오버뷰
토스뱅크 Real-Time Data 팀의 기술 스택을 한눈에 보자.
| 영역 | 기술 | 역할 |
|---|---|---|
| 메시징 | Apache Kafka | 전사 이벤트 스트리밍 백본 |
| 스트림 처리 | Apache Flink | 실시간 데이터 변환 및 집계 |
| CDC | Debezium + Kafka Connect | DB 변경 사항 실시간 캡처 |
| 분석 DB | ClickHouse | 실시간 OLAP 쿼리 |
| 애플리케이션 | Spring Boot | Kafka Client 통합 |
| 인프라 | Kubernetes, Docker | 컨테이너 오케스트레이션 |
| 모니터링 | Prometheus, Grafana | 메트릭 수집 및 시각화 |
| 언어 | Java, Kotlin, Python | 주요 개발 언어 |
1-3. 왜 이 포지션이 매력적인가
첫째, 핀테크 도메인의 높은 기술적 요구 수준이다. 일반 커머스나 소셜 서비스에서는 메시지 몇 건이 유실되어도 큰 문제가 되지 않을 수 있다. 하지만 은행에서 이체 메시지가 유실되면? 그것은 곧 고객의 돈이 사라지는 것이다. 이런 극한의 신뢰성 요구사항이 엔지니어를 성장시킨다.
둘째, 국내 최대 규모의 Kafka 운영 경험을 쌓을 수 있다. 토스 그룹 전체의 데이터 흐름이 Kafka를 통해 이루어진다. 하루 수십억 건의 메시지, 수천 개의 파티션, 수백 개의 토픽 — 이 규모의 Kafka를 운영해본 경험은 시장에서 매우 희소하다.
셋째, 최신 기술 스택을 실전에서 사용한다. Flink, ClickHouse, Debezium 같은 기술을 PoC 수준이 아니라 프로덕션에서 대규모로 운영한다. 이 경험은 이후 어디를 가든 강력한 무기가 된다.
넷째, 토스의 엔지니어링 문화다. 수평적 의사결정, 기술적 도전에 대한 존중, 실패를 용인하는 문화 — 이런 환경에서 일하면 기술적으로도 인간적으로도 성장한다.
2. JD 완전 해부: 라인 바이 라인 분석
JD의 각 항목이 실제로 무엇을 의미하는지, 어떤 역량을 요구하는지 하나씩 뜯어보자.
2-1. 대용량 메시지 처리를 위한 Kafka Broker Cluster 운영
이 한 줄이 이 포지션의 핵심이다. "대용량"이라는 단어가 의미하는 바를 구체적으로 생각해보자.
- 일일 메시지 수: 수십억 건 (peak 시간대에는 초당 수십만 건)
- 파티션 수: 수천~수만 개
- 브로커 수: 수십 대 이상의 멀티 클러스터
- 데이터 보존: 금융 규제에 따른 장기 보존 요구
이 규모에서 발생하는 실제 운영 이슈들은 다음과 같다.
# 실제 운영에서 마주하는 문제 유형들
1. 파티션 리밸런싱 중 Consumer Lag 급증
2. 브로커 디스크 I/O 병목으로 인한 지연시간 증가
3. ISR shrink로 인한 under-replicated partition 발생
4. 네트워크 파티션 상황에서의 리더 선출 문제
5. OS Page Cache 경합으로 인한 성능 저하
6. Kafka 버전 업그레이드 시 롤링 리스타트 전략
7. Topic 설정 변경 시 기존 Consumer에 미치는 영향
면접에서는 이런 상황을 경험해봤는지, 혹은 경험이 없더라도 어떻게 대응할 것인지를 물어볼 것이다.
2-2. Spring Boot 환경의 Kafka Client와 Broker 간 통신 문제 해결
토스뱅크의 수백 개 마이크로서비스는 대부분 Spring Boot 기반이다. 이 서비스들이 Kafka와 통신할 때 발생하는 문제를 해결하는 것이 두 번째 핵심 역할이다.
실제로 발생하는 문제 유형들을 살펴보자.
Producer 측 문제:
// 잘못된 Producer 설정 예시
// acks=0이면 메시지 유실 가능 - 금융에서는 절대 사용 불가
spring.kafka.producer.acks=0
// 올바른 설정
spring.kafka.producer.acks=all
spring.kafka.producer.retries=3
spring.kafka.producer.enable.idempotence=true
spring.kafka.producer.max.in.flight.requests.per.connection=5
Consumer 측 문제:
// Consumer 설정에서 자주 발생하는 실수
// max.poll.records가 너무 크면 처리 시간 초과로 리밸런싱 발생
spring.kafka.consumer.max-poll-records=1000
spring.kafka.consumer.max-poll-interval-ms=300000
// 적절한 값은 처리 로직의 복잡도에 따라 다르다
// 복잡한 비즈니스 로직이 있다면 작게 설정
spring.kafka.consumer.max-poll-records=100
spring.kafka.consumer.max-poll-interval-ms=600000
직렬화/역직렬화 문제:
// Avro Schema 변경 시 호환성 문제
// BACKWARD 호환성: Consumer가 먼저 업데이트
// FORWARD 호환성: Producer가 먼저 업데이트
// FULL 호환성: 순서 무관
// Schema Registry를 사용한 직렬화 설정
spring.kafka.producer.value-serializer=io.confluent.kafka.serializers.KafkaAvroSerializer
spring.kafka.producer.properties.schema.registry.url=http://schema-registry:8081
2-3. Active-Active 이중화 Kafka Broker
금융기관에서 이중화는 선택이 아니라 의무다. 감독 기관의 규정상 핵심 시스템은 반드시 이중화 구성을 갖추어야 한다.
Active-Active 구성이란 두 개의 Kafka 클러스터가 동시에 활성 상태로 운영되면서, 하나가 장애가 나더라도 다른 하나가 즉시 서비스를 이어받는 구조다.
+-------------------+ +-------------------+
| DC-A Kafka | <---> | DC-B Kafka |
| Cluster | Mirror | Cluster |
| | Maker2 | |
| Broker 1,2,3 | | Broker 4,5,6 |
+-------------------+ +-------------------+
| |
Producers/ Producers/
Consumers Consumers
(Active) (Active)
이 구성에서 고려해야 할 핵심 포인트:
- MirrorMaker 2 (MM2): Kafka 클러스터 간 토픽 복제
- Offset 동기화: 두 클러스터 간 Consumer offset 매핑
- Conflict Resolution: 양쪽에서 동시에 같은 토픽에 쓸 때의 충돌 해결
- Failover 전략: 장애 감지부터 전환까지의 자동화
- 네트워크 지연: 데이터센터 간 네트워크 지연이 복제에 미치는 영향
면접에서는 Active-Active 구성의 장단점, Active-Standby와의 차이, 그리고 실제 Failover 시나리오를 설계해보라는 질문이 나올 가능성이 높다.
2-4. 우대사항에서 읽어야 할 것들
JD의 우대사항은 팀이 앞으로 나아갈 방향을 알려준다.
- Flink 경험: 팀이 Flink 기반 스트림 처리를 확대하고 있다는 신호
- CDC 경험: Debezium 기반 CDC 파이프라인이 핵심 인프라라는 의미
- ClickHouse 경험: 실시간 분석 플랫폼을 구축 중이거나 확장하고 있다
- JVM 튜닝 경험: Kafka Broker와 Client 모두 JVM 위에서 돌아가므로 성능 최적화가 중요
3. 필수 기술스택 딥다이브
3-1. Kafka Core (가장 중요!)
Kafka는 이 포지션의 알파이자 오메가다. 면접의 70% 이상이 Kafka 관련 질문일 것이다.
Kafka 아키텍처 완전 이해
Kafka의 핵심 구성 요소를 하나씩 깊이 파보자.
Broker: Kafka 서버 하나하나를 Broker라고 부른다. 각 Broker는 고유한 ID를 갖고, 토픽의 파티션 일부를 담당한다.
Kafka Cluster
+--------+ +--------+ +--------+
|Broker 0| |Broker 1| |Broker 2|
| | | | | |
|Topic-A | |Topic-A | |Topic-A |
| P0(L) | | P1(L) | | P2(L) |
| P1(F) | | P2(F) | | P0(F) |
+--------+ +--------+ +--------+
L = Leader, F = Follower
Topic과 Partition: Topic은 메시지의 논리적 분류이고, Partition은 물리적 분산 단위다. Partition 수는 한번 늘리면 줄일 수 없으므로 신중하게 결정해야 한다.
Topic: payment-events (Partition 3, Replication Factor 3)
Partition 0: [msg0, msg3, msg6, msg9, ...] -> Broker 0 (Leader)
Partition 1: [msg1, msg4, msg7, msg10, ...] -> Broker 1 (Leader)
Partition 2: [msg2, msg5, msg8, msg11, ...] -> Broker 2 (Leader)
ISR (In-Sync Replica): 리더와 동기화가 완료된 팔로워 복제본의 집합이다. ISR에서 빠진 복제본이 있으면 under-replicated 상태가 되며, 이는 데이터 유실 위험을 의미한다.
# ISR 상태 확인 명령어
kafka-topics.sh --describe --topic payment-events \
--bootstrap-server localhost:9092
# 출력 예시
Topic: payment-events Partition: 0 Leader: 0
Replicas: 0,1,2 Isr: 0,1,2 <- 정상: 모든 복제본이 ISR에 포함
Topic: payment-events Partition: 1 Leader: 1
Replicas: 1,2,0 Isr: 1,2 <- 경고: Broker 0이 ISR에서 빠짐
Producer 심화
Producer의 핵심 설정 파라미터들을 이해해야 한다.
acks 설정:
| 값 | 의미 | 지연시간 | 안정성 |
|---|---|---|---|
| acks=0 | 응답 안 기다림 | 최소 | 메시지 유실 가능 |
| acks=1 | 리더만 확인 | 중간 | 리더 장애 시 유실 가능 |
| acks=all | 모든 ISR 확인 | 최대 | 가장 안전 |
금융 시스템에서는 반드시 acks=all을 사용해야 한다.
Idempotent Producer:
// Idempotent Producer 설정
Properties props = new Properties();
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
// Idempotent Producer는 PID(Producer ID)와 Sequence Number를 사용하여
// 같은 메시지가 중복 전송되는 것을 방지한다.
// Broker 측에서 (PID, Partition, SeqNum) 조합으로 중복을 감지한다.
Batching과 Compression:
// 처리량 최적화 설정
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 32768); // 32KB
props.put(ProducerConfig.LINGER_MS_CONFIG, 20); // 20ms 대기
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4"); // LZ4 압축
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 67108864); // 64MB 버퍼
// lz4: 가장 빠른 압축/해제 속도 (CPU 효율적)
// zstd: 가장 높은 압축률 (네트워크 절약)
// snappy: 균형 잡힌 선택
// gzip: 높은 압축률이지만 느림
Consumer 심화
Consumer Group과 Partition 할당:
Consumer Group: payment-processor
Consumer 1: [Partition 0, Partition 1]
Consumer 2: [Partition 2, Partition 3]
Consumer 3: [Partition 4, Partition 5]
# Consumer 3이 죽으면? -> Rebalancing 발생
Consumer 1: [Partition 0, Partition 1, Partition 4]
Consumer 2: [Partition 2, Partition 3, Partition 5]
Partition Assignment 전략:
| 전략 | 특징 | 사용 시기 |
|---|---|---|
| RangeAssignor | 토픽별로 연속 파티션 할당 | 기본값, 단일 토픽 |
| RoundRobinAssignor | 모든 파티션을 순환 할당 | 멀티 토픽, 균등 분배 |
| StickyAssignor | 리밸런싱 시 기존 할당 최대 유지 | 리밸런싱 비용 최소화 |
| CooperativeStickyAssignor | 점진적 리밸런싱 | 최신 권장 전략 |
Offset 관리:
// 자동 커밋 (간단하지만 메시지 유실/중복 가능성)
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 5000);
// 수동 커밋 (정확한 처리 보장)
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 동기 커밋
consumer.commitSync();
// 비동기 커밋 (성능 우수, 실패 시 재시도 어려움)
consumer.commitAsync((offsets, exception) -> {
if (exception != null) {
log.error("Commit failed for offsets: {}", offsets, exception);
}
});
Kafka Internals
면접에서 차별화되려면 Kafka 내부 동작을 이해해야 한다.
Log Segment 구조:
/var/kafka-logs/payment-events-0/
00000000000000000000.log # 메시지 데이터
00000000000000000000.index # offset -> 물리적 위치 매핑
00000000000000000000.timeindex # timestamp -> offset 매핑
00000000000012345678.log # 다음 세그먼트
00000000000012345678.index
leader-epoch-checkpoint
Zero-Copy Transfer: Kafka가 빠른 핵심 이유 중 하나. 디스크에서 네트워크로 데이터를 전송할 때 커널 버퍼에서 직접 소켓으로 전송하여 유저 공간 복사를 생략한다.
전통적인 방식:
Disk -> Kernel Buffer -> User Buffer -> Socket Buffer -> NIC
Kafka의 Zero-Copy (sendfile 시스템 콜):
Disk -> Kernel Buffer -> NIC
Page Cache 활용: Kafka는 자체 캐시를 두지 않고 OS의 Page Cache에 의존한다. 이것이 JVM 힙 크기를 작게 유지하고 OS에 메모리를 많이 주는 이유다.
# Kafka Broker JVM 설정 (권장)
# 힙은 6GB 정도로 제한하고 나머지는 OS Page Cache에 할당
KAFKA_HEAP_OPTS="-Xmx6g -Xms6g"
# Page Cache 상태 확인
free -h
# 또는
vmstat 1
Kafka 운영 필수 지식
Rack Awareness:
# Broker에 rack 정보 설정
broker.rack=dc-a-rack-1
# Topic 생성 시 replica가 다른 rack에 분산되어
# rack 하나가 통째로 장애나도 데이터 유실 없음
Quotas 설정:
# 특정 클라이언트의 produce/fetch 속도 제한
kafka-configs.sh --alter --add-config \
'producer_byte_rate=10485760,consumer_byte_rate=20971520' \
--entity-type clients --entity-name my-app \
--bootstrap-server localhost:9092
핵심 JMX 메트릭:
# 반드시 모니터링해야 할 메트릭들
kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec
kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec
kafka.server:type=BrokerTopicMetrics,name=BytesOutPerSec
kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions
kafka.server:type=ReplicaManager,name=IsrShrinksPerSec
kafka.server:type=ReplicaManager,name=IsrExpandsPerSec
kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce
kafka.network:type=RequestMetrics,name=TotalTimeMs,request=FetchConsumer
kafka.controller:type=KafkaController,name=ActiveControllerCount
kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent
실습: Docker Compose로 멀티브로커 클러스터 구축
version: '3.8'
services:
kafka-1:
image: confluentinc/cp-kafka:7.6.0
hostname: kafka-1
ports:
- '9092:9092'
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093,EXTERNAL://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-1:29092,EXTERNAL://localhost:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
kafka-2:
image: confluentinc/cp-kafka:7.6.0
hostname: kafka-2
ports:
- '9093:9093'
environment:
KAFKA_NODE_ID: 2
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093,EXTERNAL://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-2:29092,EXTERNAL://localhost:9093
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
kafka-3:
image: confluentinc/cp-kafka:7.6.0
hostname: kafka-3
ports:
- '9094:9094'
environment:
KAFKA_NODE_ID: 3
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093,EXTERNAL://0.0.0.0:9094
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-3:29092,EXTERNAL://localhost:9094
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
# 클러스터 시작
docker compose up -d
# 토픽 생성 (replication-factor 3)
kafka-topics.sh --create --topic test-topic \
--partitions 6 --replication-factor 3 \
--bootstrap-server localhost:9092
# 메시지 프로듀싱
kafka-console-producer.sh --topic test-topic \
--bootstrap-server localhost:9092
# 메시지 컨슈밍
kafka-console-consumer.sh --topic test-topic \
--group test-group --from-beginning \
--bootstrap-server localhost:9092
# Consumer Group 상태 확인
kafka-consumer-groups.sh --describe --group test-group \
--bootstrap-server localhost:9092
추천 자료
- Kafka: The Definitive Guide (2nd Edition) — O'Reilly, 필독서
- Confluent 공식 문서 — 가장 정확하고 최신 정보
- Apache Kafka 공식 문서 — 설정 파라미터 레퍼런스
- Kafka 소스코드 (GitHub) — 내부 동작 이해의 끝판왕
3-2. JVM 그리고 Spring Boot Kafka
JVM 메모리 구조
Kafka Broker도, Spring Boot Kafka Client도 모두 JVM 위에서 실행된다. JVM 메모리 구조를 이해하는 것은 필수다.
JVM 메모리 구조
+------------------------------------------+
| Heap Memory |
| +------+ +--------+ +-----------+ |
| | Young | | Old | | Metaspace | |
| | Gen | | Gen | | (Non-Heap)| |
| +------+ +--------+ +-----------+ |
| |
| +--------------------------------------+|
| | Off-Heap (Direct Memory) ||
| | - ByteBuffer.allocateDirect() ||
| | - Kafka 네트워크 버퍼 ||
| +--------------------------------------+|
+------------------------------------------+
| Native Memory |
| - Thread stacks |
| - JIT compiled code |
| - GC internal data |
+------------------------------------------+
GC 알고리즘 비교
| GC | 특징 | 적합한 경우 | Kafka와의 궁합 |
|---|---|---|---|
| G1GC | 영역 기반, 예측 가능한 pause | 범용, 6GB 이상 힙 | Kafka Broker 기본 권장 |
| ZGC | 초저지연 (sub-ms pause) | 지연시간 극도로 민감 | Kafka Client 적합 |
| Shenandoah | 동시 압축, 낮은 pause | G1GC 대안 | 실험적으로 사용 가능 |
# Kafka Broker에서 권장하는 G1GC 설정
KAFKA_JVM_OPTS="-XX:+UseG1GC \
-XX:MaxGCPauseMillis=20 \
-XX:InitiatingHeapOccupancyPercent=35 \
-XX:G1HeapRegionSize=16M \
-XX:MinMetaspaceFreeRatio=50 \
-XX:MaxMetaspaceFreeRatio=80"
# GC 로그 활성화 (문제 분석에 필수)
KAFKA_JVM_OPTS="$KAFKA_JVM_OPTS \
-Xlog:gc*:file=/var/log/kafka/gc.log:time,uptime:filecount=10,filesize=100M"
Spring Kafka 핵심 컴포넌트
// 1. KafkaTemplate - 메시지 전송
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, PaymentEvent> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
config.put(ProducerConfig.ACKS_CONFIG, "all");
config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public KafkaTemplate<String, PaymentEvent> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
@Service
public class PaymentEventPublisher {
private final KafkaTemplate<String, PaymentEvent> kafkaTemplate;
public CompletableFuture<SendResult<String, PaymentEvent>> publish(
PaymentEvent event) {
return kafkaTemplate.send("payment-events", event.getPaymentId(), event);
}
}
// 2. KafkaListener - 메시지 수신
@Component
public class PaymentEventConsumer {
@KafkaListener(
topics = "payment-events",
groupId = "payment-processor",
concurrency = "3" // 3개의 Consumer 스레드
)
public void consume(
@Payload PaymentEvent event,
@Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
@Header(KafkaHeaders.OFFSET) long offset,
Acknowledgment acknowledgment) {
try {
processPayment(event);
acknowledgment.acknowledge(); // 수동 커밋
} catch (RetryableException e) {
// 재시도 가능한 예외 -> DLQ로 보내지 않고 재시도
throw e;
} catch (Exception e) {
// 재시도 불가능한 예외 -> DLQ로 전송
log.error("Failed to process payment: {}", event.getPaymentId(), e);
acknowledgment.acknowledge(); // DLQ 처리 후 커밋
sendToDlq(event, e);
}
}
}
Error Handling과 DLQ 전략
// Spring Kafka Error Handler 설정
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConcurrentKafkaListenerContainerFactory<String, PaymentEvent>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, PaymentEvent> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setConcurrency(3);
factory.getContainerProperties().setAckMode(
ContainerProperties.AckMode.MANUAL_IMMEDIATE);
// 재시도 설정: 3번 재시도, 1초 간격, 2배 백오프
factory.setCommonErrorHandler(new DefaultErrorHandler(
new DeadLetterPublishingRecoverer(kafkaTemplate(),
(record, exception) -> new TopicPartition(
record.topic() + ".DLQ", record.partition())),
new FixedBackOff(1000L, 3L)
));
return factory;
}
}
Kafka Transactions과 Spring Transactional 조합
// Kafka + DB 트랜잭션을 함께 사용하는 패턴
@Configuration
public class TransactionalConfig {
@Bean
public ProducerFactory<String, Object> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
config.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "payment-tx-");
config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public KafkaTransactionManager<String, Object> kafkaTransactionManager() {
return new KafkaTransactionManager<>(producerFactory());
}
}
@Service
public class PaymentService {
@Transactional // DB 트랜잭션
public void processPayment(PaymentRequest request) {
// 1. DB에 결제 정보 저장
Payment payment = paymentRepository.save(
Payment.from(request));
// 2. Kafka로 이벤트 발행 (Kafka 트랜잭션)
kafkaTemplate.executeInTransaction(template -> {
template.send("payment-completed", payment.getId(),
PaymentCompletedEvent.from(payment));
return null;
});
}
}
추천 자료
- Spring for Apache Kafka 공식 문서 — Spring Kafka의 모든 기능
- Baeldung Spring Kafka 시리즈 — 실전 예제 중심
- 인프런 Kafka 강좌 — 한국어 실습 위주 학습
3-3. CDC (Change Data Capture)
CDC는 데이터베이스의 변경 사항을 실시간으로 감지하여 Kafka로 스트리밍하는 기술이다. 토스뱅크에서는 마이크로서비스 간 데이터 동기화, 실시간 분석, 감사 로그 등에 CDC를 핵심적으로 활용한다.
Debezium 아키텍처
+----------+ +-------------------+ +-------+ +-----------+
| MySQL | --> | Debezium MySQL | --> | Kafka | --> | Consumer |
| (binlog) | | Connector | | | | (Flink, |
+----------+ | (Kafka Connect) | | | | ClickHouse|
+-------------------+ +-------+ | etc.) |
+-----------+
Debezium은 Kafka Connect 프레임워크 위에서 동작하는 Source Connector다. MySQL의 binlog, PostgreSQL의 WAL을 읽어서 Kafka 토픽으로 변경 이벤트를 전송한다.
Debezium MySQL Connector 설정 예시:
{
"name": "tossbank-mysql-connector",
"config": {
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"database.hostname": "mysql-primary",
"database.port": "3306",
"database.user": "debezium",
"database.password": "secret",
"database.server.id": "184054",
"topic.prefix": "tossbank",
"database.include.list": "payments,accounts",
"table.include.list": "payments.transactions,accounts.balances",
"include.schema.changes": "true",
"schema.history.internal.kafka.bootstrap.servers": "kafka:9092",
"schema.history.internal.kafka.topic": "schema-changes.tossbank",
"snapshot.mode": "initial",
"transforms": "route",
"transforms.route.type": "io.debezium.transforms.ByLogicalTableRouter",
"transforms.route.topic.regex": "(.*)",
"transforms.route.topic.replacement": "cdc.$1"
}
}
CDC 이벤트 구조:
{
"schema": {},
"payload": {
"before": {
"id": 1001,
"balance": 50000,
"updated_at": "2026-03-20T10:00:00Z"
},
"after": {
"id": 1001,
"balance": 45000,
"updated_at": "2026-03-21T14:30:00Z"
},
"source": {
"version": "2.5.0.Final",
"connector": "mysql",
"name": "tossbank",
"ts_ms": 1711025400000,
"db": "accounts",
"table": "balances",
"server_id": 184054,
"file": "mysql-bin.000003",
"pos": 1234567
},
"op": "u",
"ts_ms": 1711025400123
}
}
op 필드의 의미: c (create), u (update), d (delete), r (read/snapshot)
Outbox 패턴
마이크로서비스에서 DB 변경과 이벤트 발행의 원자성을 보장하는 패턴이다.
문제 상황:
1. DB에 주문 저장 (성공)
2. Kafka에 이벤트 발행 (실패!)
-> DB와 이벤트가 불일치
해결: Outbox 패턴
1. DB에 주문 저장 + Outbox 테이블에 이벤트 저장 (같은 트랜잭션)
2. Debezium이 Outbox 테이블 변경을 감지하여 Kafka로 전송
-> DB 트랜잭션이 보장하므로 원자성 확보
-- Outbox 테이블 정의
CREATE TABLE outbox_events (
id BINARY(16) PRIMARY KEY,
aggregate_type VARCHAR(255) NOT NULL,
aggregate_id VARCHAR(255) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
// Outbox 패턴 구현
@Service
public class OrderService {
@Transactional
public Order createOrder(OrderRequest request) {
// 1. 주문 저장
Order order = orderRepository.save(Order.from(request));
// 2. 같은 트랜잭션에서 Outbox 이벤트 저장
outboxRepository.save(OutboxEvent.builder()
.aggregateType("Order")
.aggregateId(order.getId())
.eventType("OrderCreated")
.payload(objectMapper.writeValueAsString(
OrderCreatedEvent.from(order)))
.build());
return order;
}
}
Schema Evolution 전략
데이터베이스 스키마가 변경될 때 CDC 파이프라인에 미치는 영향을 관리해야 한다.
| 변경 유형 | 영향 | 대응 전략 |
|---|---|---|
| 컬럼 추가 | 하위 호환 (BACKWARD) | Consumer가 새 필드 무시 가능 |
| 컬럼 삭제 | 상위 호환 (FORWARD) | Consumer 먼저 업데이트 필요 |
| 컬럼 타입 변경 | 호환 불가 (BREAKING) | 새 토픽 생성 + 마이그레이션 |
| 테이블 이름 변경 | 새 토픽 생성 | 라우팅 설정 변경 |
3-4. Apache Flink
Flink는 실시간 스트림 처리의 사실상 표준이 되어가고 있다. 토스뱅크에서는 실시간 집계, 이상 탐지, 데이터 변환 등에 Flink를 사용한다.
Flink 아키텍처
+-------------------+
| Flink Client | (Job 제출)
+-------------------+
|
+-------------------+
| JobManager | (Job 조율, Checkpoint 관리)
| - Dispatcher |
| - ResourceMgr |
| - JobMaster |
+-------------------+
| |
+----------+ +----------+
|TaskMgr 1 | |TaskMgr 2 | (실제 연산 수행)
| Slot 1 | | Slot 1 |
| Slot 2 | | Slot 2 |
+----------+ +----------+
DataStream API 핵심
// Kafka -> Flink -> ClickHouse 파이프라인 예시
public class PaymentStreamJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// Checkpoint 설정 (Exactly-Once 보장)
env.enableCheckpointing(60000); // 60초마다
env.getCheckpointConfig().setCheckpointingMode(
CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000);
// Kafka Source
KafkaSource<PaymentEvent> source = KafkaSource
.<PaymentEvent>builder()
.setBootstrapServers("kafka:9092")
.setTopics("payment-events")
.setGroupId("flink-payment-processor")
.setStartingOffsets(OffsetsInitializer.committedOffsets(
OffsetResetStrategy.EARLIEST))
.setDeserializer(new PaymentEventDeserializer())
.build();
DataStream<PaymentEvent> payments = env.fromSource(
source, WatermarkStrategy
.<PaymentEvent>forBoundedOutOfOrderness(
Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) ->
event.getTimestamp()),
"Kafka Source");
// 5분 단위 텀블링 윈도우로 결제 금액 집계
DataStream<PaymentSummary> summaries = payments
.keyBy(PaymentEvent::getMerchantId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.aggregate(new PaymentAggregator());
// 이상 탐지: 1분 내 동일 카드로 5건 이상 결제
DataStream<FraudAlert> alerts = payments
.keyBy(PaymentEvent::getCardNumber)
.window(SlidingEventTimeWindows.of(
Time.minutes(1), Time.seconds(10)))
.process(new FraudDetectionFunction());
// Sink로 전송
summaries.sinkTo(createClickHouseSink());
alerts.sinkTo(createAlertKafkaSink());
env.execute("Payment Stream Processing");
}
}
Windowing 종류
Tumbling Window (텀블링): 겹치지 않는 고정 크기 윈도우
|-------|-------|-------|
0 5 10 15 (분)
Sliding Window (슬라이딩): 겹치는 고정 크기 윈도우
|-----------|
|-----------|
|-----------|
0 2 4 6 8 10 (윈도우 크기 6분, 슬라이드 2분)
Session Window (세션): 비활동 기간(gap)으로 구분
|------| |------------| |--|
gap gap gap
사용자 활동 기반 윈도우 (예: 30분 비활동 시 세션 종료)
State Management
// Flink State 예시: 사용자별 누적 결제 금액
public class CumulativePaymentFunction
extends KeyedProcessFunction<String, PaymentEvent, PaymentTotal> {
// State 선언
private ValueState<Long> totalAmountState;
private ValueState<Long> countState;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<Long> amountDesc =
new ValueStateDescriptor<>("totalAmount", Long.class);
ValueStateDescriptor<Long> countDesc =
new ValueStateDescriptor<>("count", Long.class);
totalAmountState = getRuntimeContext().getState(amountDesc);
countState = getRuntimeContext().getState(countDesc);
}
@Override
public void processElement(PaymentEvent event,
Context ctx, Collector<PaymentTotal> out) throws Exception {
Long total = totalAmountState.value();
Long count = countState.value();
total = (total == null ? 0 : total) + event.getAmount();
count = (count == null ? 0 : count) + 1;
totalAmountState.update(total);
countState.update(count);
out.collect(new PaymentTotal(
event.getUserId(), total, count));
}
}
State Backend 비교:
| Backend | 저장 위치 | 크기 제한 | 성능 | 사용 시기 |
|---|---|---|---|---|
| HashMapStateBackend | JVM Heap | 힙 크기 | 매우 빠름 | 작은 State |
| EmbeddedRocksDBStateBackend | 디스크 | 디스크 크기 | 느리지만 큰 State 가능 | 대규모 State |
3-5. ClickHouse
ClickHouse는 실시간 분석을 위한 컬럼형 OLAP 데이터베이스다. Kafka에서 수집된 데이터를 ClickHouse에 적재하여 실시간 대시보드와 분석 쿼리를 제공한다.
컬럼형 스토리지의 원리
행(Row) 기반 저장 (MySQL, PostgreSQL):
+----+--------+--------+-------+
| id | name | amount | date |
+----+--------+--------+-------+
| 1 | Alice | 50000 | 03-21 |
| 2 | Bob | 30000 | 03-21 |
| 3 | Carol | 70000 | 03-20 |
+----+--------+--------+-------+
-> 디스크: [1,Alice,50000,03-21][2,Bob,30000,03-21]...
컬럼(Column) 기반 저장 (ClickHouse):
id: [1, 2, 3, ...]
name: [Alice, Bob, Carol, ...]
amount: [50000, 30000, 70000, ...]
date: [03-21, 03-21, 03-20, ...]
-> 같은 타입 데이터가 연속 -> 압축률 극대화
-> SUM(amount) 같은 집계 시 amount 컬럼만 읽으면 됨
MergeTree 엔진 계열
-- MergeTree: ClickHouse의 기본이자 가장 중요한 엔진
CREATE TABLE payments (
payment_id UUID,
user_id UInt64,
merchant_id UInt64,
amount Decimal64(2),
currency String,
status Enum8('pending' = 0, 'completed' = 1, 'failed' = 2),
created_at DateTime64(3)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (merchant_id, created_at)
TTL created_at + INTERVAL 1 YEAR
SETTINGS index_granularity = 8192;
-- ReplacingMergeTree: 같은 키의 중복 제거
-- CDC 데이터를 수집할 때 유용
CREATE TABLE account_balances (
account_id UInt64,
balance Decimal64(2),
updated_at DateTime64(3),
version UInt64
)
ENGINE = ReplacingMergeTree(version)
ORDER BY account_id;
-- AggregatingMergeTree: 사전 집계
CREATE TABLE payment_hourly_stats (
merchant_id UInt64,
hour DateTime,
total_amount AggregateFunction(sum, Decimal64(2)),
tx_count AggregateFunction(count, UInt64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (merchant_id, hour);
Kafka 테이블 엔진으로 실시간 수집
-- Kafka 엔진 테이블 (Kafka Consumer 역할)
CREATE TABLE kafka_payments (
payment_id UUID,
user_id UInt64,
amount Decimal64(2),
created_at DateTime64(3)
)
ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'kafka:9092',
kafka_topic_list = 'payment-events',
kafka_group_name = 'clickhouse-consumer',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 3;
-- 최종 저장 테이블
CREATE TABLE payments_final (
payment_id UUID,
user_id UInt64,
amount Decimal64(2),
created_at DateTime64(3)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (created_at, payment_id);
-- Materialized View로 자동 전송
CREATE MATERIALIZED VIEW payments_mv TO payments_final AS
SELECT * FROM kafka_payments;
실시간 분석 쿼리 최적화
-- 1. 적절한 ORDER BY로 Primary Key 활용
-- 자주 조회하는 패턴에 맞춰 ORDER BY 설정
-- 예: 가맹점별 일별 매출 조회가 많다면
ORDER BY (merchant_id, toDate(created_at))
-- 2. Projection을 활용한 다중 정렬
ALTER TABLE payments ADD PROJECTION prj_by_user (
SELECT * ORDER BY (user_id, created_at)
);
ALTER TABLE payments MATERIALIZE PROJECTION prj_by_user;
-- 3. 집계 쿼리 최적화 예시
-- 가맹점별 일일 매출 (최근 7일)
SELECT
merchant_id,
toDate(created_at) as day,
sum(amount) as daily_total,
count() as tx_count,
avg(amount) as avg_amount
FROM payments
WHERE created_at >= now() - INTERVAL 7 DAY
AND status = 'completed'
GROUP BY merchant_id, day
ORDER BY merchant_id, day
SETTINGS max_threads = 8;
3-6. 분산시스템 기초 (CS 기반)
Kafka, Flink, ClickHouse — 모두 분산시스템이다. 분산시스템의 이론적 기초를 이해해야 이 기술들을 제대로 운영할 수 있다.
CAP 이론과 PACELC
CAP 이론: 분산시스템은 다음 세 가지를 동시에 만족할 수 없다
Consistency (일관성)
/\
/ \
/ \
/ CA \ <- 네트워크 파티션 없을 때만 가능 (현실에서 불가)
/--------\
/ CP AP \
/__________\
Partition Availability
Tolerance (가용성)
(파티션 내성)
CP 시스템: Kafka (acks=all), ZooKeeper, etcd
AP 시스템: Cassandra, DynamoDB (eventual consistency)
PACELC: CAP을 확장한 모델. 파티션 발생 시(PA) Availability vs Consistency 선택이고, 정상 시(EL) Latency vs Consistency 선택이다.
Kafka의 경우:
- PA/EC: 파티션 시 가용성 포기(CP), 정상 시 일관성 우선(EC)
- unclean.leader.election.enable=false -> 데이터 유실보다 가용성 포기
Consensus 알고리즘
Raft (KRaft 모드에서 사용):
Kafka 3.3부터 ZooKeeper 없이 자체 합의 알고리즘인 KRaft를 사용할 수 있다.
KRaft Consensus:
1. Leader Election
- Controller 노드 중 하나가 Leader로 선출
- 과반수 투표 필요 (3대 중 2대, 5대 중 3대)
2. Log Replication
- Leader가 메타데이터 변경 로그를 Follower에 복제
- 과반수가 확인하면 커밋
3. Kafka에서의 적용
- Topic/Partition 메타데이터 관리
- Broker 등록/해제
- ACL 관리
KRaft 설정:
process.roles=broker,controller
controller.quorum.voters=1@host1:9093,2@host2:9093,3@host3:9093
ZooKeeper vs KRaft 비교:
| 항목 | ZooKeeper | KRaft |
|---|---|---|
| 별도 클러스터 | 필요 (3~5대) | 불필요 |
| 파티션 수 한계 | 수만 개에서 성능 저하 | 수십만 개 가능 |
| 운영 복잡도 | 높음 | 낮음 |
| Kafka 버전 | 기존 모든 버전 | 3.3+ |
분산 트랜잭션
2PC (Two-Phase Commit):
Phase 1 (Prepare): Coordinator가 모든 참여자에게 준비 요청
Phase 2 (Commit): 모두 OK면 커밋, 하나라도 실패면 롤백
장점: 강한 일관성
단점: Coordinator 장애 시 블로킹, 성능 병목
Saga 패턴:
각 서비스가 로컬 트랜잭션 수행 후 이벤트 발행
실패 시 보상 트랜잭션(Compensating Transaction) 실행
주문 생성 -> 결제 처리 -> 재고 차감
| | |
(보상) 주문 취소 <- 결제 취소 <- 재고 복원
Kafka가 이벤트 전달을 보장하므로 Saga 패턴과 잘 어울림
Linux I/O와 Kafka 성능
Kafka의 성능은 OS 수준의 I/O 최적화에 크게 의존한다.
# Kafka에 중요한 Linux 커널 파라미터
# 파일 디스크립터 수 (수천 개 파티션 = 수천 개 파일)
fs.file-max = 1000000
ulimit -n 1000000
# 네트워크 버퍼 크기 (대량 데이터 전송에 필수)
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 65536 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Page Cache (vm.dirty 관련)
vm.dirty_background_ratio = 5
vm.dirty_ratio = 60
vm.swappiness = 1 # swap 최소화
# Disk I/O 스케줄러 (SSD의 경우 noop 또는 none)
echo none > /sys/block/sda/queue/scheduler
4. 면접 예상 질문 30선
4-1. Kafka 기술 질문 (10개)
Q1. ISR이 무엇이고, ISR에서 replica가 빠지는 조건은?
ISR은 In-Sync Replica의 약자로, 리더와 동기화가 유지되고 있는 복제본의 집합이다. replica.lag.time.max.ms 설정값(기본 30초) 이내에 리더로부터 fetch 요청을 보내지 않으면 ISR에서 제외된다.
Q2. Consumer Group에서 리밸런싱이 발생하는 시나리오 3가지를 설명하세요.
Consumer 추가/제거, 구독 토픽의 파티션 수 변경, Consumer가 max.poll.interval.ms 내에 poll을 호출하지 않은 경우다.
Q3. Exactly-Once Semantics를 Kafka에서 어떻게 구현하나요?
Idempotent Producer (PID + Sequence Number)와 Kafka Transactions (Transaction Coordinator + two-phase commit)를 조합하여 구현한다. Consumer 측에서는 read_committed isolation level을 사용한다.
Q4. Log Compaction은 어떤 상황에서 사용하나요?
키 기반으로 최신 값만 유지하면 되는 경우에 사용한다. 예를 들어 사용자 프로필 업데이트, 설정값 변경 등이다. Compaction된 토픽은 각 키의 마지막 값을 보장한다.
Q5. Kafka의 Zero-Copy 전송이 성능에 어떤 영향을 주나요?
전통적인 데이터 전송은 커널 버퍼에서 유저 공간으로, 다시 소켓 버퍼로 복사하는 4번의 복사가 필요하다. Kafka는 sendfile 시스템 콜을 사용하여 커널 버퍼에서 직접 NIC로 전송함으로써 CPU 사용률과 메모리 복사를 크게 줄인다.
Q6. 파티션 수를 결정할 때 고려해야 할 요소는?
목표 처리량(처리량 나누기 단일 파티션 처리량), Consumer 수(파티션 수보다 많은 Consumer는 유휴 상태), 파일 디스크립터 수, 리밸런싱 비용, 엔드투엔드 지연시간 등을 고려해야 한다.
Q7. unclean.leader.election.enable 설정의 의미와 트레이드오프는?
ISR에 속하지 않는 replica를 리더로 선출할 수 있는지 결정하는 설정이다. true로 설정하면 가용성은 높아지지만 데이터 유실 위험이 있고, false로 설정하면 데이터 안전성은 높지만 모든 ISR이 다운되면 서비스가 중단된다. 금융 시스템에서는 반드시 false로 설정해야 한다.
Q8. Kafka의 min.insync.replicas 설정을 설명하세요.
acks=all일 때, 쓰기가 성공하기 위해 필요한 최소 ISR 수를 지정한다. replication.factor=3이고 min.insync.replicas=2이면, 최소 2개의 replica가 동기화되어야 쓰기가 성공한다. 1개만 살아있으면 NotEnoughReplicasException이 발생한다.
Q9. Consumer Lag을 모니터링하고 대응하는 방법은?
kafka-consumer-groups.sh로 확인하거나 Burrow 같은 도구를 사용한다. Lag이 지속적으로 증가하면 Consumer 수 증가(파티션 수까지), Consumer 로직 최적화, max.poll.records 조정, Consumer 서버 스펙 업그레이드 등을 고려한다.
Q10. Kafka Streams와 Apache Flink의 차이점은?
Kafka Streams는 라이브러리로 별도 클러스터 불필요, Kafka에서만 읽기/쓰기 가능하다. Flink는 독립 클러스터 필요하지만 다양한 소스/싱크 지원, 더 복잡한 연산(CEP, ML 등), 더 유연한 윈도우, 그리고 대규모 State 관리에 강하다.
4-2. Spring Boot 및 JVM 질문 (10개)
Q1. G1GC의 동작 원리와 Kafka에서 G1GC를 권장하는 이유는?
G1GC는 힙을 동일 크기의 Region으로 나누어 관리하며, Garbage가 많은 Region부터 우선 수집한다. Kafka Broker는 6-8GB의 비교적 작은 힙을 사용하므로 G1GC의 예측 가능한 pause time이 적합하다.
Q2. Spring Kafka에서 ConcurrentKafkaListenerContainerFactory의 concurrency 설정은 무엇을 의미하나요?
Consumer 스레드 수를 의미한다. concurrency=3이면 3개의 KafkaMessageListenerContainer가 생성되어 각각 독립적인 Consumer로 동작한다. 파티션 수보다 크게 설정하면 유휴 Consumer가 발생한다.
Q3. Spring Boot에서 Kafka Consumer가 메시지 처리 중 예외가 발생하면 어떻게 되나요?
기본적으로 DefaultErrorHandler가 동작하며, 설정된 횟수만큼 재시도 후 실패하면 Recovery 로직을 실행한다. DeadLetterPublishingRecoverer를 설정하면 DLQ 토픽으로 전송된다.
Q4. JVM에서 Off-Heap 메모리가 Kafka에서 중요한 이유는?
Kafka의 네트워크 I/O는 Java NIO의 DirectByteBuffer를 사용하는데, 이는 Off-Heap에 할당된다. Off-Heap은 GC 대상이 아니므로 GC pause에 영향을 주지 않는다. Kafka Broker의 네트워크 스레드 성능에 직접적인 영향을 준다.
Q5. Thread Dump를 분석하여 Kafka Consumer의 문제를 진단한 경험이 있나요?
jstack이나 async-profiler로 Thread Dump를 수집하여, Consumer 스레드가 어디에서 블로킹되는지 확인한다. 흔한 원인으로는 외부 API 호출 타임아웃, DB 커넥션 풀 고갈, 동기화 블록에서의 경합 등이 있다.
Q6. Spring Boot의 actuator 엔드포인트를 활용한 Kafka 모니터링 방법은?
spring-boot-actuator와 micrometer-registry-prometheus를 함께 사용하면 kafka.consumer.records-consumed-total, kafka.consumer.fetch-manager-records-lag 등의 메트릭을 Prometheus로 수집할 수 있다.
Q7. Kafka Producer의 직렬화에서 Avro vs Protobuf vs JSON의 트레이드오프는?
JSON은 가독성이 좋지만 크기가 크고 스키마 강제가 없다. Avro는 스키마 강제가 가능하고 Schema Registry와 잘 통합되지만 러닝커브가 있다. Protobuf는 성능이 가장 좋고 다양한 언어를 지원하지만 Kafka 생태계 통합이 Avro보다 약하다.
Q8. Spring Kafka에서 트랜잭션을 사용할 때 주의점은?
transactional.id가 고유해야 하고, Consumer는 isolation.level=read_committed를 설정해야 한다. 트랜잭션은 성능 오버헤드가 있으므로 꼭 필요한 경우에만 사용한다. 또한 Transaction Coordinator의 장애 복구 시간도 고려해야 한다.
Q9. JVM Heap Dump 분석 경험이 있나요? 어떤 도구를 사용하나요?
Eclipse MAT, VisualVM, jmap + jhat 등을 사용한다. OOM 발생 시 -XX:+HeapDumpOnOutOfMemoryError 옵션으로 자동 덤프를 생성하고, MAT의 Leak Suspects 리포트로 메모리 누수 원인을 추적한다.
Q10. Spring Boot 애플리케이션의 Kafka Consumer가 느릴 때 성능 튜닝 방법은?
fetch.min.bytes/fetch.max.wait.ms 조정으로 배치 크기 최적화, Consumer 로직 비동기 처리 도입, max.poll.records 조정, GC 튜닝, Consumer 인스턴스 수 증가 (수평 확장) 등을 고려한다.
4-3. 시스템 설계 및 운영 질문 (10개)
Q1. Active-Active Kafka 클러스터를 설계해보세요.
두 데이터센터에 각각 Kafka 클러스터를 운영하고, MirrorMaker 2로 양방향 복제를 구성한다. 토픽 네이밍에 DC prefix를 부여하여 무한 루프를 방지한다. Consumer는 양쪽 클러스터에 모두 연결하되, Failover 시 offset 매핑이 필요하다.
Q2. Kafka 클러스터에서 브로커 하나가 다운되었을 때의 대응 절차는?
먼저 해당 브로커가 리더인 파티션의 리더 선출이 완료되었는지 확인한다. under-replicated partition을 모니터링하고, ISR이 min.insync.replicas 미만으로 떨어지지 않았는지 확인한다. 브로커 복구 후 replica가 ISR에 재진입할 때까지 모니터링한다.
Q3. CDC 파이프라인에서 데이터 정합성을 어떻게 보장하나요?
Debezium의 snapshot 모드를 활용한 초기 데이터 동기화, 스키마 변경 시 Schema Registry 호환성 모드 활용, Outbox 패턴으로 트랜잭션 원자성 보장, Consumer 측 멱등성(idempotency) 구현을 조합한다.
Q4. 일일 100억 건의 메시지를 처리하는 Kafka 클러스터의 용량을 산정해보세요.
메시지당 평균 1KB라면 일일 10TB. 복제 계수 3이면 30TB. 보존 기간 7일이면 210TB. 여기에 파티션 수, 브로커 수, 디스크 I/O 처리량, 네트워크 대역폭을 고려하여 브로커 스펙과 대수를 결정한다.
Q5. Kafka를 사용한 Event Sourcing 패턴을 설명하세요.
모든 상태 변경을 이벤트로 Kafka에 저장하고, 현재 상태는 이벤트를 순서대로 재생하여 구축한다. Log Compaction을 사용하면 각 키의 최신 상태를 유지할 수 있다. CQRS 패턴과 함께 사용하면 읽기/쓰기를 분리할 수 있다.
Q6. Kafka Connect와 직접 Producer/Consumer를 구현하는 것의 장단점은?
Kafka Connect는 설정 기반으로 빠르게 구축 가능하고 offset 관리가 자동이며 분산 모드 지원이 내장되어 있다. 직접 구현은 비즈니스 로직 유연성이 높고 디버깅이 쉽지만, 운영 부담이 크다. 정형화된 데이터 이동에는 Connect가 적합하다.
Q7. 실시간 사기 탐지 시스템을 Kafka와 Flink로 설계해보세요.
결제 이벤트를 Kafka로 수집하고, Flink에서 Sliding Window로 패턴 분석한다. 예를 들어 동일 카드로 1분 내 5건 이상 결제, 지역 이상(서울에서 결제 후 1분 내 부산에서 결제) 등의 룰을 CEP(Complex Event Processing)로 구현한다. 탐지된 이상 이벤트는 별도 Kafka 토픽으로 전송하여 차단 시스템이 즉시 대응한다.
Q8. Kafka 버전 업그레이드 시 무중단으로 진행하는 방법은?
Rolling Upgrade 방식을 사용한다. inter.broker.protocol.version과 log.message.format.version을 기존 버전으로 유지한 상태에서 브로커를 하나씩 업그레이드한다. 모든 브로커 업그레이드 완료 후 프로토콜 버전을 새 버전으로 올린다.
Q9. ClickHouse에서 실시간 데이터를 수집하면서 쿼리 성능을 유지하는 방법은?
Kafka 테이블 엔진과 Materialized View로 자동 수집하되, Buffer 테이블을 중간에 두어 소량 Insert를 모아서 배치로 MergeTree에 적재한다. 쿼리 성능을 위해 Projection, 적절한 파티셔닝, 그리고 AggregatingMergeTree로 사전 집계한다.
Q10. 모니터링 및 알림 체계를 어떻게 구성하시겠습니까?
Kafka JMX 메트릭을 Prometheus로 수집하고 Grafana 대시보드로 시각화한다. 핵심 알림 항목은 Under-Replicated Partitions, Consumer Lag 임계치 초과, ISR Shrink 빈도, 브로커 CPU/디스크 사용률 등이다. PagerDuty 같은 온콜 시스템과 연동하여 장애 대응 체계를 구축한다.
5. 6개월 학습 로드맵
1개월차: Kafka Core + JVM 기초
| 주차 | 학습 내용 | 실습 과제 |
|---|---|---|
| 1주 | Kafka 아키텍처, Broker/Topic/Partition | Docker Compose 3-Broker 클러스터 구축 |
| 2주 | Producer (acks, idempotence, batching) | 다양한 설정으로 성능 테스트 |
| 3주 | Consumer (Consumer Group, offset, rebalancing) | Consumer Group 실습, 리밸런싱 관찰 |
| 4주 | JVM 기초 (메모리 구조, GC 알고리즘) | GC 로그 분석, jstat/jmap 실습 |
이 달의 목표: Kafka 멀티브로커 클러스터를 직접 구축하고, Producer/Consumer의 주요 설정 파라미터를 실험해본다. GC 로그를 읽고 해석할 수 있다.
2개월차: Spring Boot Kafka + 심화
| 주차 | 학습 내용 | 실습 과제 |
|---|---|---|
| 1주 | Spring Kafka (KafkaTemplate, KafkaListener) | Spring Boot + Kafka 프로젝트 시작 |
| 2주 | Error Handling, DLQ, 재시도 전략 | DLQ 파이프라인 구현 |
| 3주 | Kafka Transactions + Spring Transactional | 트랜잭션 기반 메시지 전송 |
| 4주 | Schema Registry (Avro), 직렬화 심화 | Avro 스키마 기반 Producer/Consumer |
이 달의 목표: Spring Boot 기반의 실전 Kafka 애플리케이션을 만들 수 있다. 에러 처리, DLQ, 트랜잭션을 이해하고 구현할 수 있다.
3개월차: CDC + Kafka Connect
| 주차 | 학습 내용 | 실습 과제 |
|---|---|---|
| 1주 | Kafka Connect 아키텍처, Source/Sink Connector | FileSource/FileSink Connector 실습 |
| 2주 | Debezium 아키텍처, MySQL CDC | MySQL 변경 사항 Kafka로 캡처 |
| 3주 | Outbox 패턴, Schema Evolution | Outbox 패턴 구현 |
| 4주 | CDC 운영: 스냅샷, 장애 복구, 모니터링 | CDC 장애 시나리오 테스트 |
이 달의 목표: MySQL에서 Kafka로 CDC 파이프라인을 구축할 수 있다. Outbox 패턴을 이해하고 구현할 수 있다.
4개월차: Flink + 스트림 처리
| 주차 | 학습 내용 | 실습 과제 |
|---|---|---|
| 1주 | Flink 아키텍처, DataStream API 기초 | Flink 로컬 클러스터 + 기본 Job |
| 2주 | Windowing (Tumbling, Sliding, Session) | 실시간 집계 Job 구현 |
| 3주 | State Management, Checkpoint/Savepoint | Stateful Processing 실습 |
| 4주 | Kafka Source/Sink, Exactly-Once | Kafka-Flink-Kafka 파이프라인 |
이 달의 목표: Flink로 실시간 데이터 처리 파이프라인을 구축할 수 있다. Windowing과 State Management를 이해한다.
5개월차: ClickHouse + 모니터링
| 주차 | 학습 내용 | 실습 과제 |
|---|---|---|
| 1주 | ClickHouse 기초, MergeTree 엔진 | ClickHouse 설치, 기본 쿼리 |
| 2주 | Kafka 테이블 엔진, Materialized View | Kafka에서 ClickHouse로 실시간 수집 |
| 3주 | 쿼리 최적화, Projection, 파티셔닝 | 분석 쿼리 성능 튜닝 |
| 4주 | Prometheus + Grafana 모니터링 구축 | Kafka/Flink/ClickHouse 대시보드 |
이 달의 목표: 전체 파이프라인(Kafka 수집, Flink 처리, ClickHouse 적재/분석, Grafana 시각화)을 완성한다.
6개월차: 시스템 설계 면접 + 포트폴리오
| 주차 | 학습 내용 | 실습 과제 |
|---|---|---|
| 1주 | 분산시스템 이론 정리 (CAP, Raft, 2PC) | 시스템 설계 문제 풀기 |
| 2주 | Active-Active 설계, DR 전략 | MM2 기반 이중화 실습 |
| 3주 | 포트폴리오 정리, 이력서 작성 | GitHub README, 아키텍처 다이어그램 |
| 4주 | 모의면접, 기술 블로그 작성 | 면접 질문 답변 연습 |
이 달의 목표: 면접에서 시스템 설계 질문에 자신 있게 답할 수 있다. 포트폴리오와 이력서가 완성되어 있다.
6. 이력서 작성 전략 (JD 기반)
6-1. STAR 기법으로 임팩트 보여주기
이력서의 프로젝트 경험은 STAR 기법(Situation, Task, Action, Result)으로 작성하는 것이 효과적이다.
나쁜 예시:
- Kafka 클러스터 운영
- Spring Boot 기반 메시지 처리 시스템 개발
- CDC 파이프라인 구축
좋은 예시:
- 일 5억 건 메시지를 처리하는 Kafka 클러스터(15 Broker, 3,000 Partition)의
안정적 운영을 담당하여 연간 가용성 99.99%를 달성
- Spring Boot Kafka Consumer의 처리 지연 문제를 분석하여 GC 튜닝과
batch 설정 최적화로 p99 지연시간을 800ms에서 120ms로 85% 개선
- Debezium 기반 CDC 파이프라인을 구축하여 주문-결제-정산 시스템 간
데이터 동기화 지연을 5분에서 3초 이내로 단축
6-2. JD 키워드를 이력서에 매핑하기
| JD 요구사항 | 이력서에 녹여야 할 포인트 |
|---|---|
| 대용량 Kafka Cluster 운영 | 구체적인 규모 (메시지 수, 파티션 수, 브로커 수) |
| Spring Boot Kafka Client | Spring Kafka 활용 경험, 문제 해결 사례 |
| Active-Active 이중화 | DR 구성 경험, Failover 테스트 경험 |
| CDC, Flink, ClickHouse | 각 기술의 프로덕션 적용 경험 또는 프로젝트 |
| JVM 튜닝 | GC 분석, 메모리 최적화 사례 |
6-3. 장애 사례와 해결 과정으로 깊이 보여주기
면접관이 정말 알고 싶은 것은 "문제를 만났을 때 어떻게 분석하고 해결하는가"이다.
사례: Kafka Consumer Lag 급증 이슈
상황: 특정 시간대에 Consumer Lag이 급증하여 메시지 처리 지연 발생
분석:
1. Consumer 스레드 덤프로 블로킹 포인트 확인
2. GC 로그에서 Full GC 빈도 확인
3. Kafka 메트릭에서 fetch 요청 지연시간 확인
원인: Young Generation이 작아 Minor GC 빈도가 높았고,
GC pause 동안 max.poll.interval.ms를 초과하여 리밸런싱 반복
해결:
- JVM Heap 크기 조정 (4GB -> 8GB)
- G1GC MaxGCPauseMillis를 100ms로 설정
- max.poll.records를 500에서 100으로 줄임
결과: Consumer Lag이 0에 수렴, 리밸런싱 발생 빈도 0으로 감소
6-4. 기술적 성장 스토리를 전달하기
단순히 "이 기술을 사용했다"보다 "이 기술을 왜 선택했고, 무엇을 배웠는지"를 보여주는 것이 중요하다.
- 기술 선택의 이유와 트레이드오프
- 실패에서 배운 교훈
- 기술 블로그나 사내 공유를 통한 지식 전파
- 오픈소스 기여나 커뮤니티 활동
7. 포트폴리오 프로젝트 아이디어
7-1. 실시간 주문 처리 시스템
전체 아키텍처:
[Order Service] [Payment Service] [Notification Service]
| | |
v v v
+--------------------------------------------------+
| Apache Kafka |
| order-events payment-events notification-events|
+--------------------------------------------------+
| | |
v v v
[Apache Flink - Real-time Processing]
|
v
[ClickHouse - Real-time Analytics]
|
v
[Grafana Dashboard]
구현 포인트:
- Spring Boot 마이크로서비스 3개 (주문, 결제, 알림)
- Kafka로 이벤트 기반 통신
- Saga 패턴으로 분산 트랜잭션 관리
- Flink로 실시간 매출 집계
- ClickHouse로 실시간 대시보드
7-2. CDC 기반 데이터 동기화 파이프라인
[MySQL (Source)]
|
| Debezium CDC
v
[Kafka (Event Store)]
|
+---> [Elasticsearch (검색)]
|
+---> [ClickHouse (분석)]
|
+---> [Redis (캐시)]
구현 포인트:
- Debezium MySQL Connector 설정
- Outbox 패턴 구현
- Schema Evolution 시나리오 테스트
- 장애 복구 (Connector 재시작, 스냅샷 복구)
7-3. Flink 실시간 이상 탐지 시스템
[Web/App Events] -> [Kafka] -> [Flink CEP] -> [Alert Kafka Topic]
| |
v v
[ClickHouse] [Alert Service]
| |
v v
[Dashboard] [Slack/Email]
구현 포인트:
- Flink CEP로 복합 이벤트 패턴 탐지
- Sliding Window 기반 이상 빈도 감지
- State Management (RocksDB)
- Exactly-Once 보장
실전 퀴즈
지금까지 배운 내용을 점검해보자. 답을 보기 전에 직접 생각해보는 것을 권한다.
Q1. Kafka에서 acks=all이고 min.insync.replicas=2일 때, 3개의 replica 중 2개가 다운되면 어떤 일이 발생하나요?
Producer가 메시지를 보내면 NotEnoughReplicasException이 발생한다. ISR에 남아있는 replica가 1개뿐인데 min.insync.replicas=2를 만족하지 못하기 때문이다. 즉, 쓰기가 실패한다. 이는 데이터 안전성을 가용성보다 우선시하는 설계다. 금융 시스템에서는 이런 동작이 바람직하다 — 잘못된 데이터를 쓰는 것보다 쓰기를 거부하는 것이 낫다.
Consumer는 이미 커밋된 메시지에 대해서는 정상적으로 읽을 수 있다. 다만 해당 파티션의 리더가 다운된 경우라면 리더 선출이 완료될 때까지 읽기도 잠시 중단된다.
Q2. Debezium CDC 파이프라인 운영 중 MySQL의 binlog가 만료되어 Connector가 더 이상 읽을 수 없게 되었습니다. 어떻게 복구하나요?
이 상황은 Connector가 오랜 시간 중단되었거나, MySQL의 binlog 보존 기간(expire_logs_days)이 짧을 때 발생한다.
복구 방법:
- Connector를 삭제한 후, snapshot.mode=initial로 새로 생성하여 전체 스냅샷부터 다시 시작한다.
- 또는 snapshot.mode=when_needed를 사용하면 Debezium이 binlog를 읽을 수 없는 상황을 자동 감지하여 스냅샷을 실행한다.
예방 방법:
- MySQL의 binlog 보존 기간을 충분히 길게 설정한다 (최소 7일 이상).
- Connector의 상태를 모니터링하여 장기 중단을 방지한다.
- Connector의 offset을 주기적으로 백업한다.
Q3. Flink에서 Checkpoint와 Savepoint의 차이는 무엇이고, 각각 언제 사용하나요?
Checkpoint: Flink가 자동으로 주기적으로 생성하는 스냅샷이다. 장애 복구를 위한 것으로, Flink가 내부적으로 관리한다. 일반적으로 State Backend에 저장되며, Job이 취소되면 함께 삭제된다 (retain 설정 가능).
Savepoint: 사용자가 수동으로 트리거하는 스냅샷이다. Job 업그레이드, 클러스터 마이그레이션, A/B 테스트 등에 사용한다. 사용자가 명시적으로 관리하며, 영구적으로 보존된다.
사용 시기:
- Checkpoint: 일반적인 장애 복구 (자동, 별도 작업 불필요)
- Savepoint: Job 코드 업데이트, Flink 클러스터 업그레이드, 다른 클러스터로 마이그레이션
주의: Savepoint에서 복구할 때 operator의 UID가 변경되면 State 매핑이 깨질 수 있으므로, Flink Job의 모든 operator에 고유 UID를 명시적으로 설정하는 것이 좋다.
Q4. Kafka Consumer에서 CooperativeStickyAssignor를 사용하면 기존 RangeAssignor 대비 어떤 이점이 있나요?
RangeAssignor (기존 방식): 리밸런싱 시 모든 Consumer의 파티션 할당을 해제(revoke)한 후 새로 할당한다. 이 과정에서 모든 Consumer가 일시적으로 메시지 처리를 중단하므로 처리량이 급격히 떨어진다 (Stop-the-World 리밸런싱).
CooperativeStickyAssignor: 점진적(incremental) 리밸런싱을 지원한다. 변경이 필요한 파티션만 revoke하고 나머지는 계속 처리한다. 즉, Consumer 하나가 추가되거나 제거될 때 전체가 멈추지 않고 일부 파티션만 재할당된다.
이점:
- 리밸런싱 중에도 대부분의 Consumer가 정상 처리 가능
- Consumer Lag 급증 방지
- 전체 처리량 감소 최소화
설정 방법 (Spring Kafka):
spring.kafka.consumer.properties.partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
Q5. ClickHouse의 ReplacingMergeTree에서 중복 제거가 즉시 이루어지지 않는 이유와 해결 방법은?
ReplacingMergeTree는 같은 ORDER BY 키를 가진 행 중 가장 최신 버전만 유지하는 엔진이다. 그러나 중복 제거는 백그라운드 머지(merge) 과정에서만 이루어진다. 즉, 데이터를 삽입한 직후에는 중복이 존재할 수 있다.
이유: ClickHouse는 쓰기 성능을 극대화하기 위해 Insert 시점에는 단순히 파트(part)를 추가하기만 한다. 중복 제거는 비동기 머지 과정에서 처리한다.
해결 방법:
- 쿼리 시 FINAL 키워드를 사용한다. 그러면 ClickHouse가 쿼리 시점에 중복 제거를 수행한다. 다만 성능이 저하될 수 있다.
SELECT * FROM account_balances FINAL WHERE account_id = 1001;
- argMax 함수를 사용하여 최신 버전을 직접 선택한다.
SELECT
account_id,
argMax(balance, version) as latest_balance
FROM account_balances
GROUP BY account_id;
- OPTIMIZE TABLE FINAL 명령으로 수동 머지를 강제한다 (운영 환경에서는 주의 필요).
참고 자료
공식 문서
- Apache Kafka Documentation — kafka.apache.org/documentation
- Confluent Documentation — docs.confluent.io
- Apache Flink Documentation — nightlies.apache.org/flink
- ClickHouse Documentation — clickhouse.com/docs
- Debezium Documentation — debezium.io/documentation
- Spring for Apache Kafka Reference — docs.spring.io/spring-kafka
도서
- Kafka: The Definitive Guide, 2nd Edition (O'Reilly) — Kafka의 바이블
- Designing Data-Intensive Applications (Martin Kleppmann) — 분산시스템 필독서
- Streaming Systems (Tyler Akidau 외) — 스트림 처리 이론의 정석
- Java Performance (Scott Oaks) — JVM 성능 최적화
온라인 학습
- Confluent Developer — developer.confluent.io (무료 Kafka 강좌)
- 인프런 Kafka 강좌 — 한국어 실습 중심 학습
- Baeldung Spring Kafka — baeldung.com/spring-kafka (Spring Kafka 튜토리얼)
- Flink Training — training.ververica.com (Flink 공식 트레이닝)
커뮤니티 및 블로그
- Confluent Blog — confluent.io/blog (Kafka 심화 기술 아티클)
- 토스 기술 블로그 — toss.tech (토스의 기술 문화와 사례)
- Debezium Blog — debezium.io/blog (CDC 관련 심화 내용)
- ClickHouse Blog — clickhouse.com/blog (ClickHouse 활용 사례)
마무리: 합격까지의 여정
토스뱅크 Data Engineer (Kafka & Streaming) 포지션은 단순히 Kafka를 "사용할 줄 아는" 수준이 아니라, 대규모 Kafka 클러스터를 직접 운영하고 문제를 해결할 수 있는 수준을 요구한다. 그리고 이 수준에 도달하는 가장 확실한 방법은 직접 만들고 부수고 고쳐보는 것이다.
이 가이드에서 제시한 6개월 로드맵을 그대로 따르지 않아도 좋다. 중요한 것은 각 기술의 "왜"를 이해하는 것이다.
- Kafka가 왜 빠른지 (Zero-Copy, Page Cache, Sequential I/O)
- Consumer Group이 왜 필요한지 (병렬 처리와 내결함성)
- CDC가 왜 필요한지 (폴링 방식의 한계)
- Flink가 왜 필요한지 (배치 처리의 한계)
- Active-Active가 왜 필요한지 (금융 규제와 고가용성)
이 "왜"에 대한 깊은 이해가 있다면, 면접에서 어떤 질문이 나오더라도 자신의 언어로 답할 수 있을 것이다.
마지막으로, 기술만큼 중요한 것이 커뮤니케이션 능력이다. 토스뱅크의 Real-Time Data 팀은 전사 개발팀을 지원하는 플랫폼 팀이다. 다양한 팀의 요구사항을 이해하고, 기술적인 내용을 비기술자에게도 설명할 수 있는 능력이 필요하다.
당신의 합격을 응원한다.
Toss Bank Data Engineer (Kafka & Streaming) Study Guide: Tech Stack, Interview Prep, and 6-Month Roadmap
- Introduction: Why Toss Bank's Real-Time Data Team Matters
- 1. JD Deep Analysis: What Toss Bank Actually Wants
- 2. Tech Stack Deep Dive
- 3. Interview Preparation: 30 Questions
- 4. Six-Month Study Roadmap
- 5. Resume Strategy
- 6. Portfolio Project Ideas
- 7. Quiz: Test Your Knowledge
- 8. References and Resources
- Conclusion
Introduction: Why Toss Bank's Real-Time Data Team Matters
Toss Bank is not just another Korean fintech company. It is a mobile-first bank that processes millions of transactions daily, each of which needs to be captured, transformed, and delivered in near real-time. The Real-Time Data team sits at the center of this architecture. They operate the Kafka infrastructure that every other team depends on, build the streaming pipelines that feed fraud detection models, and maintain the CDC (Change Data Capture) systems that keep dozens of microservices in sync.
When Toss Bank posts a Data Engineer (Kafka and Streaming) position, they are looking for someone who can operate Kafka at a scale that most companies never reach. This is not a "set up a three-node cluster and call it a day" role. This is a role where you will manage hundreds of brokers, handle cross-datacenter replication for financial data that cannot be lost, and build stream processing pipelines that must produce exactly-once results in a regulated banking environment.
This guide breaks down the job description line by line, maps each requirement to specific technologies and study resources, and gives you a realistic 6-month plan to prepare. Whether you are a backend engineer looking to specialize in data infrastructure, or an existing data engineer who wants to level up your Kafka expertise, this guide will show you exactly what to study and in what order.
1. JD Deep Analysis: What Toss Bank Actually Wants
Let us dissect the job description section by section. Understanding what each bullet point really means will help you prioritize your study time.
1.1 Core Responsibilities
"Operate and optimize the Kafka Broker cluster that handles the entire company's event streaming"
This is the headline responsibility. You are not a consumer of Kafka — you are the person who keeps it running. That means:
- Capacity planning for brokers, partitions, and topics
- Performance tuning (OS-level, JVM, and Kafka configuration)
- Monitoring with tools like Kafka Manager, Burrow, or custom Prometheus exporters
- Rolling upgrades with zero downtime
- Incident response when a broker goes down at 3 AM
"Develop and maintain the Spring Boot-based Kafka Client SDK used company-wide"
Toss Bank builds internal libraries that standardize how every team produces and consumes Kafka messages. This means you need to be comfortable with:
- Spring Boot auto-configuration and starter modules
- Kafka Producer and Consumer APIs at a low level
- Schema management with Avro or Protobuf and a Schema Registry
- Error handling patterns: dead letter queues, retry topics, circuit breakers
- Backward compatibility when evolving the SDK
"Design and implement Active-Active replication across data centers"
Financial regulations require that Toss Bank can survive an entire data center going offline. Active-Active Kafka replication means:
- MirrorMaker 2 or Confluent Replicator for cross-cluster replication
- Offset translation between clusters
- Conflict resolution strategies for dual-write scenarios
- Failover and failback procedures that meet RTO/RPO requirements
"Build CDC pipelines using Debezium to capture database changes"
CDC is how microservices stay in sync without tight coupling. Debezium captures row-level changes from databases (MySQL, PostgreSQL) and publishes them to Kafka topics. You need to understand:
- Debezium connectors for different databases
- Kafka Connect architecture and its distributed mode
- Schema evolution when database schemas change
- Handling initial snapshots for large tables
- Exactly-once delivery semantics end to end
"Develop stream processing applications using Flink"
Flink is the team's choice for stateful stream processing. This goes beyond simple transformations:
- Windowing (tumbling, sliding, session windows)
- State management with RocksDB backends
- Checkpointing and savepoints for fault tolerance
- Event time vs processing time semantics
- Complex event processing (CEP) for fraud detection patterns
"Manage ClickHouse for real-time analytics dashboards"
ClickHouse is an OLAP database optimized for fast analytical queries on large datasets. The team uses it for:
- Real-time dashboards showing transaction volumes, latency percentiles, and error rates
- Ad-hoc queries on streaming data that has been materialized
- Data retention and tiered storage strategies
1.2 Required Qualifications
"3+ years of experience operating Kafka in production"
This is not negotiable. They want someone who has debugged ISR (In-Sync Replica) shrink issues, dealt with unbalanced partition leaders, and understands why unclean.leader.election.enable defaults to false. Book knowledge alone will not cut it.
"Proficiency in Java or Kotlin with Spring Boot"
The internal SDK is built on Spring Boot. You need to write production-quality Java or Kotlin code, not just scripts. Understanding Spring's dependency injection, AOP for logging and metrics, and testing with MockBean are all expected.
"Understanding of distributed systems fundamentals"
This means you can discuss CAP theorem trade-offs, leader election algorithms, consensus protocols, and why exactly-once semantics are hard in distributed systems. Expect whiteboard questions on these topics.
"Experience with Linux system administration"
Kafka runs on Linux. You need to be comfortable with:
- File system tuning (XFS, page cache, disk I/O scheduling)
- Network tuning (TCP buffer sizes, socket settings)
- JVM garbage collection tuning (G1GC, ZGC)
- Monitoring with tools like
top,iostat,sar, andperf
1.3 Preferred Qualifications
"Experience with Flink or Spark Structured Streaming"
Having hands-on experience with at least one stream processing framework puts you ahead. Flink is preferred, but Spark Structured Streaming knowledge transfers well.
"Familiarity with Kubernetes-based deployments"
Toss runs much of its infrastructure on Kubernetes. Understanding StatefulSets, persistent volumes, and the challenges of running stateful systems like Kafka on K8s is valuable.
"Contributions to open-source projects"
Toss Bank actively contributes to open source. Having a track record of contributions — even small ones — demonstrates that you understand collaborative software development at scale.
2. Tech Stack Deep Dive
2.1 Apache Kafka: The Foundation
Kafka is the central nervous system of Toss Bank's data infrastructure. Here is what you need to know beyond the basics.
Broker Internals
A Kafka broker stores messages in segments on disk. Understanding the log structure is fundamental:
topic-partition/
00000000000000000000.log # First segment
00000000000000000000.index # Offset index
00000000000000000000.timeindex # Time-based index
00000000000000065536.log # Second segment (after roll)
Key configuration parameters you must understand:
# Replication
default.replication.factor=3
min.insync.replicas=2
unclean.leader.election.enable=false
# Performance
num.io.threads=8
num.network.threads=3
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
# Log management
log.segment.bytes=1073741824
log.retention.hours=168
log.cleanup.policy=delete
Producer Tuning
The producer configuration determines throughput, latency, and durability guarantees:
Properties props = new Properties();
props.put("acks", "all"); // Wait for all ISR to acknowledge
props.put("retries", Integer.MAX_VALUE); // Retry indefinitely
props.put("max.in.flight.requests.per.connection", 5);
props.put("enable.idempotence", true); // Exactly-once per partition
props.put("batch.size", 16384); // Batch size in bytes
props.put("linger.ms", 5); // Wait up to 5ms to fill batch
props.put("compression.type", "lz4"); // Compress batches
Consumer Group Rebalancing
Consumer group rebalancing is one of the most common operational headaches. Understanding the protocols is critical:
- Eager rebalancing: All consumers stop, reassign, resume. Simple but causes stop-the-world pauses.
- Cooperative rebalancing (Incremental): Only affected partitions are revoked and reassigned. Introduced in Kafka 2.4+.
- Static group membership: Consumers with
group.instance.idavoid rebalancing on transient failures.
// Cooperative rebalancing configuration
props.put("partition.assignment.strategy",
"org.apache.kafka.clients.consumer.CooperativeStickyAssignor");
props.put("group.instance.id", "consumer-host-1");
props.put("session.timeout.ms", 30000);
props.put("heartbeat.interval.ms", 10000);
2.2 KRaft Mode: Kafka Without ZooKeeper
Kafka has been migrating away from ZooKeeper dependency since KIP-500. KRaft (Kafka Raft) mode replaces ZooKeeper with an internal Raft-based consensus protocol.
Why this matters for Toss Bank:
- Reduced operational complexity (no separate ZooKeeper ensemble to manage)
- Faster controller failover (seconds instead of tens of seconds)
- Better scalability for metadata (millions of partitions)
- Simplified deployment on Kubernetes
Key KRaft concepts:
# KRaft controller configuration
process.roles=controller
node.id=1
controller.quorum.voters=1@controller1:9093,2@controller2:9093,3@controller3:9093
controller.listener.names=CONTROLLER
You should be able to discuss the migration path from ZooKeeper to KRaft mode, including the dual-write phase and the cutover process.
2.3 Spring Boot Kafka Client SDK
Building an internal SDK is a software engineering challenge as much as a Kafka challenge. Here is what a production-quality SDK looks like:
@Configuration
@EnableKafka
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, GenericRecord> producerFactory(
KafkaProperties properties) {
Map<String, Object> config = properties.buildProducerProperties();
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
KafkaAvroSerializer.class);
config.put("schema.registry.url", schemaRegistryUrl);
return new DefaultKafkaProducerFactory<>(config);
}
@Bean
public KafkaTemplate<String, GenericRecord> kafkaTemplate(
ProducerFactory<String, GenericRecord> factory) {
KafkaTemplate<String, GenericRecord> template =
new KafkaTemplate<>(factory);
template.setObservationEnabled(true); // Micrometer tracing
return template;
}
}
Dead Letter Queue Pattern
When a consumer cannot process a message after retries, it should be routed to a dead letter topic:
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String>
kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setCommonErrorHandler(new DefaultErrorHandler(
new DeadLetterPublishingRecoverer(kafkaTemplate),
new FixedBackOff(1000L, 3) // 3 retries, 1 second apart
));
return factory;
}
2.4 Active-Active Kafka Replication
Active-Active replication is one of the hardest problems in distributed Kafka deployments. Here are the key patterns:
MirrorMaker 2 Architecture
MirrorMaker 2 (MM2) is built on Kafka Connect and provides:
- Topic replication with configurable topic name remapping
- Consumer group offset synchronization
- Automatic topic configuration sync (partitions, configs)
- Heartbeat topics for monitoring replication lag
# MirrorMaker 2 configuration
clusters = dc1, dc2
dc1.bootstrap.servers = dc1-kafka1:9092,dc1-kafka2:9092
dc2.bootstrap.servers = dc2-kafka1:9092,dc2-kafka2:9092
# Bidirectional replication
dc1->dc2.enabled = true
dc2->dc1.enabled = true
# Topic filtering
dc1->dc2.topics = transactions.*, user-events.*
dc2->dc1.topics = transactions.*, user-events.*
# Prevent replication loops
replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy
Conflict Resolution
In Active-Active setups, the same topic can receive writes in both data centers. Strategies include:
- Last-writer-wins with timestamps: Simple but can lose data
- Application-level conflict resolution: The consumer merges conflicting records
- Region-based partitioning: Each DC writes to different partitions, avoiding conflicts entirely
2.5 CDC with Debezium
Debezium captures database changes by reading the database's transaction log (binlog for MySQL, WAL for PostgreSQL).
{
"name": "postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres-primary",
"database.port": "5432",
"database.user": "debezium",
"database.dbname": "tossbank",
"topic.prefix": "cdc",
"table.include.list": "public.accounts,public.transactions",
"plugin.name": "pgoutput",
"slot.name": "debezium_slot",
"publication.name": "debezium_pub",
"snapshot.mode": "initial",
"transforms": "route",
"transforms.route.type": "io.debezium.transforms.ByLogicalTableRouter",
"transforms.route.topic.regex": "(.*)\\.(.*)",
"transforms.route.topic.replacement": "cdc.$2"
}
}
Critical Debezium Concepts
- Snapshot modes:
initial(full snapshot then stream),schema_only(structure only, stream from current position),never(stream only) - Outbox pattern: Instead of CDC on business tables, applications write events to an outbox table, and Debezium captures those events. This decouples the event schema from the database schema.
- Schema evolution: When a database column is added or modified, Debezium reflects the change in the Kafka message schema. Schema Registry compatibility modes (BACKWARD, FORWARD, FULL) determine whether consumers can handle the change.
2.6 Apache Flink: Stream Processing
Flink is the stream processing engine of choice for stateful computations on Kafka streams.
Flink Application Structure
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// Enable exactly-once checkpointing
env.enableCheckpointing(60000, CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000);
env.getCheckpointConfig().setCheckpointTimeout(120000);
// Kafka source
KafkaSource<Transaction> source = KafkaSource.<Transaction>builder()
.setBootstrapServers("kafka:9092")
.setTopics("transactions")
.setGroupId("flink-processor")
.setStartingOffsets(OffsetsInitializer.committedOffsets())
.setDeserializer(new TransactionDeserializer())
.build();
DataStream<Transaction> transactions = env.fromSource(
source,
WatermarkStrategy.<Transaction>forBoundedOutOfOrderness(
Duration.ofSeconds(5))
.withTimestampAssigner((tx, ts) -> tx.getTimestamp()),
"Kafka Source"
);
// Windowed aggregation: transaction count per account per minute
transactions
.keyBy(Transaction::getAccountId)
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.aggregate(new TransactionCountAggregate())
.addSink(new ClickHouseSink());
env.execute("Transaction Aggregation");
State Management
Flink maintains state in state backends. For production workloads:
- HashMapStateBackend: Fast, stores state in JVM heap. Good for small state.
- EmbeddedRocksDBStateBackend: Stores state on local disk via RocksDB. Required for large state that exceeds memory.
env.setStateBackend(new EmbeddedRocksDBStateBackend(true));
env.getCheckpointConfig().setCheckpointStorage(
"s3://flink-checkpoints/job-1");
2.7 ClickHouse: Real-Time Analytics
ClickHouse is a columnar OLAP database that excels at aggregation queries on large datasets.
Table Design for Streaming Data
CREATE TABLE transactions_realtime (
transaction_id UUID,
account_id UInt64,
amount Decimal64(2),
currency LowCardinality(String),
transaction_type LowCardinality(String),
status LowCardinality(String),
created_at DateTime64(3, 'Asia/Seoul'),
region LowCardinality(String)
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(created_at)
ORDER BY (account_id, created_at)
TTL created_at + INTERVAL 90 DAY;
Kafka Integration
ClickHouse can consume directly from Kafka using the Kafka engine:
CREATE TABLE transactions_kafka (
transaction_id UUID,
account_id UInt64,
amount Decimal64(2),
created_at DateTime64(3)
) ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'kafka1:9092,kafka2:9092',
kafka_topic_list = 'transactions',
kafka_group_name = 'clickhouse-consumer',
kafka_format = 'JSONEachRow';
CREATE MATERIALIZED VIEW transactions_mv TO transactions_realtime AS
SELECT * FROM transactions_kafka;
3. Interview Preparation: 30 Questions
3.1 Kafka Fundamentals (Questions 1-10)
Q1. Explain how Kafka achieves high throughput despite writing to disk.
Kafka uses sequential I/O, the OS page cache, and zero-copy transfers (sendfile system call). Sequential writes to disk can achieve throughputs exceeding 600 MB/s on modern SSDs, rivaling network throughput. The OS page cache means frequently accessed data is served from memory without Kafka needing to manage its own cache.
Q2. What happens when a Kafka broker in the ISR set falls behind?
When a replica falls behind the leader by more than replica.lag.time.max.ms (default 30 seconds), it is removed from the ISR set. If min.insync.replicas is set to 2 and only 1 replica remains in sync, producers with acks=all will receive NotEnoughReplicasException. The lagging replica will rejoin the ISR once it catches up.
Q3. Compare eager and cooperative consumer rebalancing protocols.
Eager rebalancing revokes all partitions from all consumers, then reassigns everything. This causes a stop-the-world pause where no consumer processes messages. Cooperative rebalancing only revokes the partitions that need to move, allowing other consumers to continue processing. Cooperative rebalancing is always preferred in production.
Q4. How does Kafka's idempotent producer work?
When enable.idempotence=true, each producer instance gets a unique Producer ID (PID). Every message batch includes a sequence number. The broker deduplicates messages by tracking the last sequence number per PID and partition. If a retry sends the same batch, the broker recognizes the duplicate and acknowledges without writing again.
Q5. What is the difference between log.retention.hours and log.retention.bytes?
log.retention.hours deletes segments older than the specified time. log.retention.bytes deletes the oldest segments when the total partition size exceeds the limit. When both are set, whichever threshold is reached first triggers deletion. For compacted topics, log.cleanup.policy=compact replaces deletion with key-based compaction.
Q6. Explain Kafka transactions and exactly-once semantics.
Kafka transactions allow a producer to atomically write to multiple partitions. The producer calls beginTransaction(), sends messages, and calls commitTransaction(). A transaction coordinator on the broker tracks transaction state. Consumers with isolation.level=read_committed only see committed messages. Combined with idempotent producers, this achieves exactly-once semantics within Kafka.
Q7. How would you monitor Kafka cluster health?
Key metrics to track: UnderReplicatedPartitions (should be 0), ActiveControllerCount (exactly 1), OfflinePartitionsCount (should be 0), RequestHandlerAvgIdlePercent (above 0.3), NetworkProcessorAvgIdlePercent (above 0.3), BytesInPerSec/BytesOutPerSec per broker, consumer group lag per partition. Tools: JMX exporters to Prometheus, Grafana dashboards, Burrow for consumer lag, custom alerting on critical metrics.
Q8. What is the purpose of unclean.leader.election.enable and why should it be false for financial data?
When set to true, a replica that is not in the ISR can become the leader if all ISR replicas are down. This prevents unavailability but risks data loss because the new leader may be missing recently committed messages. For financial data where data loss is unacceptable, this must be false, accepting temporary unavailability over data inconsistency.
Q9. How does log compaction work and when would you use it?
Log compaction retains only the latest value for each key. A background thread (the log cleaner) scans segments and removes older records with the same key. Use compaction for topics that represent the current state of an entity (user profiles, account balances) rather than an event stream. Configuration: log.cleanup.policy=compact, min.cleanable.dirty.ratio, log.cleaner.min.compaction.lag.ms.
Q10. Explain the impact of partition count on performance and ordering.
More partitions increase parallelism (more consumers can read concurrently) but also increase: broker memory usage (each partition has buffers), end-to-end latency (more replication work), leader election time, and the number of file handles. Ordering is guaranteed only within a single partition. For ordered event processing, use a consistent partition key (e.g., account ID) and configure enough partitions for throughput without over-partitioning.
3.2 Spring Boot and SDK Design (Questions 11-15)
Q11. How would you design a Kafka Client SDK that handles schema evolution?
Use Apache Avro with Confluent Schema Registry. The SDK registers schemas on first use and caches them. Configure BACKWARD compatibility so new consumers can read old data. The SDK wraps KafkaAvroSerializer and KafkaAvroDeserializer, handling serialization transparently. When a schema evolves, the registry validates compatibility, and consumers use schema resolution to read old formats.
Q12. What retry patterns would you build into the SDK?
Implement exponential backoff with jitter for transient failures. Use Spring Retry's RetryTemplate or the built-in Kafka consumer retry mechanism. For messages that fail after maximum retries, route to a dead letter topic with the original message, error details, and retry count as headers. Provide a dead letter processor that allows teams to inspect and replay failed messages.
Q13. How do you handle graceful shutdown in a Spring Boot Kafka consumer?
Register a shutdown hook that calls consumer.wakeup() to interrupt the poll loop. The WakeupException breaks the consumer out of poll(), and the finally block calls consumer.close() which commits offsets and leaves the consumer group cleanly. In Spring Boot, the ConcurrentKafkaListenerContainerFactory handles this when you set containerProperties.setShutdownTimeout().
Q14. How would you add distributed tracing to Kafka messages?
Inject trace context (trace ID, span ID) into Kafka message headers on the producer side. On the consumer side, extract the context from headers and create a new span linked to the producer span. With Spring Cloud Sleuth or Micrometer Tracing, this is mostly automatic when using KafkaTemplate and @KafkaListener with observation enabled. For Flink consumers, you need custom header extraction.
Q15. Explain how you would version the SDK without breaking existing consumers.
Follow semantic versioning. Use Spring Boot's auto-configuration to provide sensible defaults that existing consumers inherit. New features use opt-in configuration properties. When breaking changes are unavoidable, provide a migration guide and a compatibility module that maps old configuration keys to new ones. Run integration tests against both the current and previous SDK versions.
3.3 Distributed Systems (Questions 16-20)
Q16. Explain the CAP theorem and where Kafka fits.
CAP states that a distributed system can provide at most two of three guarantees: Consistency, Availability, Partition tolerance. Kafka prioritizes consistency and partition tolerance (CP) when configured with acks=all, min.insync.replicas=2, and unclean.leader.election.enable=false. It sacrifices availability — if not enough replicas are in sync, writes are rejected. With relaxed settings, Kafka can lean toward AP.
Q17. How does Raft consensus work in KRaft mode?
In KRaft mode, controller nodes elect a leader using the Raft protocol. The leader appends metadata changes to a replicated log. Followers replicate the log and apply changes. A leader is elected when it receives votes from a majority of the controller quorum. If the leader fails, a follower with the most up-to-date log initiates a new election. This is faster than the ZooKeeper-based controller because there is no external dependency.
Q18. What is the split-brain problem and how does Kafka prevent it?
Split-brain occurs when two nodes both believe they are the leader. In ZooKeeper mode, Kafka uses ephemeral nodes — when a broker loses its ZooKeeper session, its leadership is revoked. In KRaft mode, the Raft protocol's term-based voting prevents two leaders from existing simultaneously. The controller.quorum.voters configuration ensures a majority quorum is always required.
Q19. Explain backpressure in stream processing and how Flink handles it.
Backpressure occurs when a downstream operator cannot keep up with the upstream rate. Flink uses a credit-based flow control mechanism: each downstream task communicates how many network buffers it can accept (credits). When credits run out, the upstream task stops sending, and this pressure propagates back to the source. Monitoring backpressure in the Flink UI is critical for identifying bottleneck operators.
Q20. How would you design a system that guarantees exactly-once message processing across Kafka and a database?
Use the transactional outbox pattern. Instead of writing to both Kafka and the database, write only to the database (business data plus an outbox table) in a single transaction. Debezium captures the outbox table changes and publishes them to Kafka. This ensures that the message is published if and only if the transaction commits. On the consumer side, use idempotent writes (upserts with unique keys) to handle potential duplicates.
3.4 CDC and Data Pipeline (Questions 21-25)
Q21. Compare CDC approaches: log-based vs query-based vs trigger-based.
Log-based CDC (Debezium) reads the database transaction log directly. It captures all changes with low overhead and minimal impact on the source database. Query-based CDC polls the database for changes using timestamps or version columns. It misses deletes and has higher latency. Trigger-based CDC uses database triggers to capture changes. It is flexible but adds write overhead and complexity. Log-based is preferred for production systems.
Q22. How do you handle schema changes in a CDC pipeline?
When a column is added, Debezium captures the new schema and forwards it to Schema Registry. With BACKWARD compatibility, existing consumers can still read old messages (the new field has a default value). When a column is dropped, use FORWARD compatibility. The key is to configure Schema Registry compatibility mode correctly and test schema changes in a staging environment before production.
Q23. What happens when a Debezium connector falls behind the database WAL?
If the connector falls too far behind, the database may have already recycled the WAL segments. The connector will fail with a "WAL segment not found" error. Recovery options: re-snapshot the affected tables (set snapshot.mode to always temporarily), or use a point-in-time recovery to restore the WAL position. Prevention: monitor connector lag, ensure WAL retention is sufficient (wal_keep_segments in PostgreSQL).
Q24. Explain the outbox pattern and its advantages over dual writes.
In a dual write, the application writes to both the database and Kafka. If one write fails, the system is inconsistent. The outbox pattern avoids this: the application writes business data and an outbox event in a single database transaction. Debezium captures the outbox event and publishes it to Kafka. Since both writes are in the same transaction, they are atomic. The outbox event schema can be different from the database schema, providing a clean API boundary.
Q25. How would you ensure CDC data quality in a financial system?
Implement a reconciliation pipeline that periodically compares source database state with the state derived from CDC events. Use checksums or row counts per table per time window. Alert on discrepancies. Additionally, add end-to-end watermarking: inject synthetic events at the source and verify they arrive at the sink within SLA. Track CDC lag as a key operational metric.
3.5 Operations and Production (Questions 26-30)
Q26. How would you perform a rolling upgrade of a Kafka cluster?
Upgrade one broker at a time. Before stopping a broker, ensure all partitions it leads have up-to-date replicas. Set controlled.shutdown.enable=true so the broker transfers leadership before shutting down. After the upgrade, verify the broker rejoins the cluster and catches up. Monitor UnderReplicatedPartitions throughout the process. Upgrade controllers last in KRaft mode.
Q27. A consumer group has high lag on specific partitions. How do you diagnose?
Check if the affected partitions are on the same broker (possible broker issue). Check consumer processing time per record (slow processing). Check for data skew in the partition key (hot partitions). Check GC logs for long pauses. Check if the consumer is hitting rate limits on downstream systems. Use kafka-consumer-groups.sh --describe to see per-partition lag and consumer assignments.
Q28. How would you handle a datacenter failover for Kafka?
Ensure MirrorMaker 2 is replicating all critical topics with minimal lag. During failover: stop producers in the failed DC, verify replication is caught up in the surviving DC, redirect DNS or load balancers, start consumers in the surviving DC from translated offsets. After failover: monitor for data gaps, run reconciliation, and plan the failback when the original DC recovers.
Q29. What is your approach to Kafka topic naming conventions?
Use a hierarchical naming scheme that encodes the domain, event type, and version:
domain.subdomain.event-type.version
For example: payments.transactions.created.v1, cdc.accounts.changes. Avoid special characters. Use a topic governance tool or naming validation in the SDK to enforce conventions. Document the naming scheme and make it part of the onboarding process for new teams.
Q30. How do you capacity plan for a Kafka cluster?
Start with the required throughput (MB/s) and retention period. Calculate storage: throughput multiplied by retention multiplied by replication factor. Add overhead for compaction and segment rolls. For brokers: each broker can handle roughly 100-200 MB/s depending on hardware. Plan for N+1 (one broker can fail without overloading others). For partitions: target 10-20 MB/s per partition. Monitor actual usage and adjust quarterly. Factor in growth projections from business stakeholders.
4. Six-Month Study Roadmap
Month 1: Kafka Fundamentals
Week 1-2: Core Concepts
- Read "Kafka: The Definitive Guide" (2nd Edition) chapters 1-6
- Set up a 3-broker Kafka cluster locally using Docker Compose
- Practice producing and consuming messages with the CLI tools
- Study the log structure by examining segment files directly
Week 3-4: Producer and Consumer Deep Dive
- Implement a Java producer with different
ackssettings and measure throughput - Implement a consumer group with manual offset commits
- Experiment with rebalancing: add and remove consumers, observe partition reassignment
- Read Kafka source code for
KafkaProducer.send()andKafkaConsumer.poll()
Month 2: Spring Boot and SDK Development
Week 1-2: Spring Kafka
- Build a Spring Boot application with
spring-kafka - Implement producer interceptors for logging and metrics
- Build a consumer with error handling and dead letter queue
- Write integration tests using
EmbeddedKafka
Week 3-4: SDK Design
- Design and build a reusable Spring Boot Kafka starter module
- Add auto-configuration for common serialization formats (JSON, Avro)
- Implement schema registry integration
- Package as a Maven/Gradle library with documentation
Month 3: Distributed Systems and Replication
Week 1-2: Theory
- Read "Designing Data-Intensive Applications" chapters 5, 8, 9
- Study the Raft consensus paper
- Understand KRaft mode architecture
- Practice distributed systems design questions
Week 3-4: Active-Active Replication
- Set up MirrorMaker 2 between two Kafka clusters locally
- Test topic replication, offset synchronization, and failover
- Implement a consumer that handles cluster switching
- Document the failover procedure step by step
Month 4: CDC with Debezium
Week 1-2: Debezium Fundamentals
- Set up Debezium with PostgreSQL and Kafka Connect
- Capture inserts, updates, and deletes on sample tables
- Study the Debezium event format and schema
- Implement the outbox pattern with a sample application
Week 3-4: Production CDC
- Test schema evolution scenarios (add column, rename column, drop column)
- Implement a reconciliation check between source and sink
- Study Debezium's exactly-once delivery configuration
- Handle initial snapshots for large tables
Month 5: Flink Stream Processing
Week 1-2: Flink Basics
- Complete the official Flink training exercises
- Build a streaming application that reads from Kafka and writes aggregations
- Study windowing: tumbling, sliding, session windows
- Implement event time processing with watermarks
Week 3-4: Advanced Flink
- Implement stateful processing with managed keyed state
- Configure checkpointing with RocksDB state backend
- Build a complex event processing (CEP) pattern for fraud detection
- Deploy a Flink job on a local YARN or Kubernetes cluster
Month 6: ClickHouse and Integration
Week 1-2: ClickHouse
- Set up ClickHouse and load sample data
- Design table schemas with appropriate engines (MergeTree, ReplacingMergeTree)
- Build a Kafka-to-ClickHouse pipeline using the Kafka engine and materialized views
- Write analytical queries and optimize with
EXPLAIN
Week 3-4: End-to-End Project and Interview Prep
- Build a complete pipeline: PostgreSQL to Debezium to Kafka to Flink to ClickHouse
- Create a monitoring dashboard with Grafana
- Prepare a 5-minute architecture presentation of your project
- Practice all 30 interview questions with a partner
- Review and refine your resume
5. Resume Strategy
What Toss Bank Wants to See
Your resume should answer one question: can this person operate and evolve our Kafka infrastructure? Here is how to frame your experience:
Lead with Impact Metrics
- "Operated a 50-broker Kafka cluster processing 2M messages per second with 99.99% uptime"
- "Reduced consumer lag from 10 minutes to under 30 seconds by migrating to cooperative rebalancing"
- "Built a CDC pipeline with Debezium that replaced batch ETL, reducing data freshness from 6 hours to under 5 seconds"
Demonstrate Operational Maturity
- Mention on-call experience, incident response, and post-mortem culture
- Highlight zero-downtime upgrades and migration projects
- Show that you understand the boring but critical parts: monitoring, alerting, capacity planning
Show Breadth Beyond Kafka
- Spring Boot SDK development shows software engineering skills
- Flink or Spark experience shows data processing capabilities
- ClickHouse or similar OLAP experience shows analytics awareness
- Kubernetes deployment experience shows operational versatility
Resume Format Tips
- Keep it to 2 pages maximum
- Use the XYZ format: "Accomplished X by implementing Y, resulting in Z"
- List technologies in order of relevance to the JD
- Include a "Technical Skills" section that mirrors the JD's tech stack
- Link to a GitHub profile with relevant projects or contributions
6. Portfolio Project Ideas
Project 1: Real-Time Financial Transaction Pipeline
Build an end-to-end streaming pipeline that simulates financial transaction processing:
- Data Source: A Spring Boot application that generates synthetic transaction events
- Kafka: Multi-topic architecture with transactions, alerts, and audit topics
- Flink: Stream processing for real-time aggregation and anomaly detection
- ClickHouse: Real-time analytics dashboard
- Monitoring: Prometheus + Grafana for Kafka and Flink metrics
This project directly maps to Toss Bank's core architecture.
Project 2: Multi-Datacenter Kafka Replication Lab
Demonstrate your understanding of Active-Active replication:
- Set up two Kafka clusters in Docker (simulating two data centers)
- Configure MirrorMaker 2 for bidirectional replication
- Implement a producer that writes to both clusters
- Build a consumer that fails over between clusters
- Document the failover and failback procedures
- Measure and report replication lag under various load conditions
Project 3: CDC-Powered Event Sourcing System
Build a microservice system that uses CDC for event-driven communication:
- A Spring Boot service with a PostgreSQL database
- Debezium capturing changes from the outbox table
- Kafka as the event bus
- A downstream service that builds a materialized view from events
- A reconciliation tool that verifies consistency between source and materialized view
Project 4: Kafka Operations Toolkit
Build tools that a Kafka operator would actually use:
- A partition rebalancing analyzer that suggests optimal partition assignments
- A consumer lag monitoring dashboard with alerting
- A topic configuration auditor that checks for deviations from standards
- A schema compatibility checker that validates Avro schemas before registration
7. Quiz: Test Your Knowledge
Q1: You set acks=all and min.insync.replicas=2 on a 3-broker cluster. One broker goes down. What happens to produce requests?
Produce requests continue to succeed. With 3 replicas and 1 broker down, 2 replicas remain in the ISR. Since min.insync.replicas=2 is satisfied, the producer receives acknowledgment. If a second broker goes down, the ISR would have only 1 replica, which is less than min.insync.replicas=2, and produce requests would fail with NotEnoughReplicasException.
Q2: A Debezium connector for PostgreSQL stops receiving changes. The connector status shows "RUNNING" but no new messages appear in Kafka. What are the most likely causes?
Possible causes: (1) The replication slot has been dropped or is inactive — check pg_replication_slots. (2) WAL level is not set to logical — verify wal_level configuration. (3) The connector is stuck on a snapshot — check connector task status. (4) Network partition between Debezium and the database. (5) The table was excluded by a filter change. (6) PostgreSQL publication does not include the target tables.
Q3: Your Flink job's checkpoint duration increases from 5 seconds to 5 minutes over a week. What is happening and how do you fix it?
Checkpoint duration increases when state size grows. Possible causes: (1) A keyed state that accumulates without TTL — add state TTL configuration. (2) State backend running out of local disk — increase disk or switch to incremental checkpoints. (3) RocksDB compaction falling behind — tune RocksDB settings. (4) Checkpoint storage (S3/HDFS) throughput bottleneck. Fix: enable incremental checkpointing, configure state TTL, monitor state size per operator in the Flink UI.
Q4: In an Active-Active Kafka setup with MirrorMaker 2, how do you prevent infinite replication loops?
MirrorMaker 2 prevents replication loops using source cluster metadata in record headers. When MM2 replicates a record, it adds a header indicating the source cluster. When the record arrives at the other cluster and is picked up for replication back, MM2 checks the header and skips records that originated from the target cluster. The IdentityReplicationPolicy or default DefaultReplicationPolicy (which prefixes topic names with the source cluster alias) also helps prevent loops.
Q5: You need to migrate a topic from delete-based retention to compaction without downtime. Describe the steps.
Steps: (1) Create a new topic with cleanup.policy=compact and the same partition count. (2) Set up a Kafka Streams or consumer-producer bridge that reads from the old topic and writes to the new topic with the same keys. (3) Wait until the bridge has caught up to the end of the old topic. (4) Switch producers to write to the new topic. (5) Switch consumers to read from the new topic at their last committed offset in the bridge's consumer group. (6) Verify data integrity. (7) Delete the old topic after a grace period.
8. References and Resources
Books
- Neha Narkhede, Gwen Shapira, Todd Palino — Kafka: The Definitive Guide, 2nd Edition, O'Reilly, 2021
- Martin Kleppmann — Designing Data-Intensive Applications, O'Reilly, 2017
- Fabian Hueske, Vasiliki Kalavri — Stream Processing with Apache Flink, O'Reilly, 2019
- Robert Yokota — Event Streams in Action, Manning, 2019
Official Documentation
- Apache Kafka Documentation — https://kafka.apache.org/documentation/
- Confluent Platform Documentation — https://docs.confluent.io/
- Debezium Documentation — https://debezium.io/documentation/
- Apache Flink Documentation — https://flink.apache.org/docs/
- ClickHouse Documentation — https://clickhouse.com/docs/
Courses and Tutorials
- Confluent Developer — Free Kafka courses — https://developer.confluent.io/
- Stephane Maarek — Kafka courses on Udemy
- Apache Flink Training — https://nightlies.apache.org/flink/flink-docs-stable/docs/learn-flink/overview/
- ClickHouse Academy — https://clickhouse.com/learn
Community and Talks
- Kafka Summit recordings — https://www.kafka-summit.org/
- Flink Forward conference recordings — https://flink-forward.org/
- Martin Kleppmann's blog — https://martin.kleppmann.com/
- Toss Tech Blog — https://toss.tech/
- Confluent Blog — https://www.confluent.io/blog/
Conclusion
The Toss Bank Data Engineer (Kafka and Streaming) role is one of the most technically demanding data engineering positions in the Korean fintech ecosystem. It requires a rare combination of deep Kafka operational expertise, software engineering skill for SDK development, distributed systems knowledge for Active-Active replication, and data pipeline experience with CDC and stream processing.
The six-month roadmap in this guide is aggressive but realistic. Start with Kafka fundamentals, build the SDK skills early, then layer on the advanced topics. The portfolio projects will give you concrete artifacts to discuss in interviews, and the 30 practice questions cover the most likely topics you will face.
Remember: Toss Bank values operational maturity as much as technical skill. Being able to discuss how you would handle a broker failure at 3 AM, how you would plan capacity for next quarter, or how you would safely migrate a critical topic — these conversations matter as much as your ability to write Flink code.
Start studying today. The pipeline waits for no one.