fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • postgresql 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • gaps and islands: SQL 창 함수로 이어진 구간 찾기
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Alpine 이미지의 Java 에서 링크 바꿔치기(TOCTOU)를 막지 못하는 이유
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • personal-projects 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/architecture/Spring Batch와 Event-Driv…
architecture

Spring Batch와 Event-Driven의 선택 기준

백엔드 시스템을 운영하다 보면 "이 작업은 동기로 처리하기 어렵다"는 결론에 도달하는 경우가 있다. 사용자가 결제 버튼을 누르면 결제 승인은 동기적으로 끝나야 하지만, 알림 발송, 통계 집계, 추천 모델 재학습, 검색 색인 갱신, CRM 동기화는 비동기로 분리할 수 있다. 이때 두 가지 선택지가 생긴다. 하나는 데이터를 모아 일정 시점에 한 번에 처리하는...

2026.09.03·9 min read·69 views

관련 문서: Outbox / Inbox Pattern 심화, 분산 트랜잭션과 Outbox 패턴. 본 문서는 두 처리 패러다임의 선택 기준과 trade-off에 집중하고, 위 두 문서는 이벤트 발행의 정합성 메커니즘에 집중한다.

왜 중요한가

백엔드 시스템을 운영하다 보면 "이 작업은 동기로 처리하기 어렵다"는 결론에 도달하는 경우가 있다. 사용자가 결제 버튼을 누르면 결제 승인은 동기적으로 끝나야 하지만, 알림 발송, 통계 집계, 추천 모델 재학습, 검색 색인 갱신, CRM 동기화는 비동기로 분리할 수 있다. 이때 두 가지 선택지가 생긴다.

하나는 데이터를 모아 일정 시점에 한 번에 처리하는 길**(batch)이다. 또 하나는 발생하는 변화를 그 즉시 메시지로 흘려보내고 소비자들이 각자 받아 처리하는 길(event-driven)**이다. 표면적으로는 둘 다 "비동기"라는 이름으로 묶여 동등하게 비교되지만, 실전에서 두 길은 운영 방식, 장애 처리, 데이터 정합성, 모니터링 지표, 심지어 채용 면접에서 받는 질문의 형태까지 완전히 다르다.

면접관이 "왜 이건 Kafka 안 쓰고 Spring Batch로 했어요?"라고 묻거나 그 반대로 "왜 배치 안 돌리고 이벤트로 처리하셨어요?"라고 물었을 때, "그게 더 적합해서"라는 답은 통하지 않는다. 시니어 면접에서 보고 싶은 건 두 패러다임의 본질적 차이(latency, throughput, ordering, replay, 운영 비용)를 자기 언어로 정리해 둔 사람이다. 이 문서는 그 정리를 목표로 한다.


두 패러다임의 본질적 차이

트리거와 시간 관점

  • Spring Batch는 시간이 트리거다. Cron이든 사용자 호출이든, "지금 이 순간부터 어디까지의 데이터를 처리하라"는 윈도우가 명시적으로 정해진다.
  • Event-Driven은 이벤트가 트리거다. 어떤 도메인 사실이 일어났다는 정보가 broker로 흘러가는 순간이 처리 시작점이다.

이 차이가 거의 모든 후속 결정에 영향을 준다. Spring Batch에서 "마지막으로 처리한 ID 이후"라는 cursor가 진실의 원천이라면, Event-Driven에서는 broker offset이 진실의 원천이다.

처리 단위 관점

  • Batch는 chunk(예 100건, 500건) 단위로 처리한다. chunk 안에서 ItemReader → ItemProcessor → ItemWriter가 묶여 동작하고 chunk 단위로 트랜잭션이 끊긴다.
  • Event-Driven은 message 단위로 처리한다. 한 메시지가 하나의 비즈니스 사실에 대응한다.

정합성 관점

  • Batch는 최종 일관성을 일정 주기로 회복시키는 데 강하다. 새벽 3시 정산 배치가 그 예다.
  • Event-Driven은 최종 일관성을 사건 발생 직후 회복시키는 데 강하다. 주문 직후 재고가 깎이는 흐름이 그 예다.

이 두 관점이 상호 배타적이지 않다는 점이 중요하다. 실제 서비스에서는 둘이 거의 항상 같이 쓰인다. 다만 어느 쪽이 그 도메인의 1차 처리 통로인지를 명확히 해야 한다.


Spring Batch의 Step 단위 실패 격리

Spring Batch는 단순한 "스케줄러와 루프"가 아니다. 핵심은 재시작 가능성(restartability)과 Step 단위 실패 격리다.

java
@Configuration
@EnableBatchProcessing
public class IndexConfluenceJobConfig {
 
    @Bean
    public Job indexConfluenceJob(JobRepository jobRepository,
                                  Step fetchPagesStep,
                                  Step parseAdfStep,
                                  Step embeddingStep,
                                  Step indexStep,
                                  Step deleteSyncStep) {
        return new JobBuilder("indexConfluenceJob", jobRepository)
            .start(fetchPagesStep)
            .next(parseAdfStep)
            .next(embeddingStep)
            .next(indexStep)
            .next(deleteSyncStep)
            .build();
    }
}

이 코드의 가치는 다섯 Step 중 embeddingStep이 OpenAI rate limit으로 실패해도, fetchPagesStep과 parseAdfStep의 결과는 보존된다는 점이다. 재시작하면 실패한 embeddingStep부터 시작한다. 만약 이 전체를 거대한 단일 메서드로 짰다면 임베딩 한 번 실패에 처음부터 다시 돌려야 한다.

Chunk-Oriented Processing

java
@Bean
public Step embeddingStep(JobRepository jobRepository,
                          PlatformTransactionManager txm,
                          ItemReader<Document> reader,
                          ItemProcessor<Document, EmbeddedDocument> processor,
                          ItemWriter<EmbeddedDocument> writer) {
    return new StepBuilder("embeddingStep", jobRepository)
        .<Document, EmbeddedDocument>chunk(100, txm)
        .reader(reader)
        .processor(processor)
        .writer(writer)
        .faultTolerant()
        .retryLimit(3)
        .retry(TransientApiException.class)
        .skipLimit(50)
        .skip(InvalidDocumentException.class)
        .build();
}

여기서 알아둘 점:

  • chunk(100): 100건씩 모아 한 트랜잭션으로 커밋. chunk 도중 실패하면 그 chunk만 롤백.
  • retry: 일시적 예외**(rate limit, network blip)**는 같은 메시지로 재시도.
  • skip: 데이터 자체가 잘못된 경우 그 한 건만 건너뛰고 다음 chunk 진행.

AsyncItemProcessor와 I/O 병렬화

배치는 단일 스레드 직렬 처리가 기본이라 I/O 바운드 작업에 약하다. AsyncItemProcessor로 chunk 내부를 병렬화하면 임베딩 API처럼 청크당 수십 초 걸리는 외부 호출을 동시 실행할 수 있다.

java
@Bean
public AsyncItemProcessor<Document, EmbeddedDocument> asyncProcessor(
        ItemProcessor<Document, EmbeddedDocument> delegate,
        TaskExecutor taskExecutor) {
    AsyncItemProcessor<Document, EmbeddedDocument> async = new AsyncItemProcessor<>();
    async.setDelegate(delegate);
    async.setTaskExecutor(taskExecutor);
    return async;
}

병렬화는 공짜가 아니다. 외부 API의 rate limit을 깰 수 있고, 메모리 사용량이 늘어난다. 그래서 chunk size와 thread pool size를 함께 튜닝해야 한다.


Event-Driven의 핵심 구성요소

Event-Driven에서는 broker(Kafka, RabbitMQ, SQS)가 핵심 인프라다.

java
@Component
@RequiredArgsConstructor
public class OrderEventPublisher {
 
    private final ApplicationEventPublisher events;
    private final OrderRepository orderRepository;
 
    @Transactional
    public Order placeOrder(OrderCommand cmd) {
        Order order = orderRepository.save(Order.from(cmd));
        events.publishEvent(new OrderPlaced(order.getId()));
        return order;
    }
}
java
@Component
public class OrderPlacedKafkaBridge {
 
    private final KafkaTemplate<String, OrderPlaced> kafka;
 
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onCommitted(OrderPlaced event) {
        kafka.send("order.placed.v1", String.valueOf(event.orderId()), event);
    }
}

이 두 조각이 합쳐져 "주문이 커밋된 직후" Kafka로 메시지가 흘러간다. 이 메시지를 알림 서비스, 재고 서비스, 통계 서비스가 각자 자기 속도로 소비한다.

Partition Key 선택이 ordering을 결정한다

같은 orderId를 partition key로 쓰면 같은 주문에 대한 이벤트는 같은 파티션에 쌓이고, 한 컨슈머 인스턴스가 순서대로 받는다. partition key를 잘못 고르면 후속 이벤트가 선행 이벤트보다 먼저 처리되는 사고가 생긴다.

Consumer Group과 수평 확장

같은 topic을 여러 인스턴스가 분할 소비하면 throughput이 늘어난다. 하지만 partition 수가 consumer 수의 상한이다. partition을 16개로 잡으면 컨슈머 인스턴스를 17개로 늘려도 한 인스턴스는 놀게 된다.

Replay와 Offset

Kafka의 강점은 메시지를 일정 기간 보존한다는 점이다. consumer group을 새로 만들어 from-beginning으로 다시 소비하면 과거 이벤트를 그대로 재생할 수 있다. 신규 소비 서비스가 합류했을 때 backfill을 이 메커니즘으로 한다. RabbitMQ에는 이런 자연스러운 replay가 없으므로 Outbox 테이블이나 별도 archive에서 다시 발행해야 한다.


선택 기준

관점Batch가 유리Event-Driven이 유리
latency분~시 단위 허용초 단위 이하 요구
데이터 규모한 번에 수십만~수억 건건당 처리, 분산 누적
처리 윈도우시간 윈도우가 명확"발생 시점부터 즉시"
외부 시스템 동기화정산, 리포트, 색인 재구축알림, CDC, 상태 전이 후속
실패 처리Step 재시작, skipDLQ, retry topic
ordering 요구보통 약함 (집계)partition 단위로 강함
운영 인프라scheduler, monitoring jobbroker, consumer lag
비용 모델CPU/메모리 burst메시지 보존 비용

기억해야 할 단일 룰: "지금 일어난 사건에 대한 즉각 반응"이 가치라면 event-driven, "어떤 시점까지의 누적 결과"가 가치라면 batch.


실패 처리 방식의 차이

Batch의 실패 처리 모델

Step 1~5 중 Step 3에서 실패했다면, JobRepository에 그 Step의 ExecutionContext가 남는다. 다시 실행할 때 Step 3부터 같은 cursor 위치에서 재개한다. 이게 가능하려면 ItemReader가 ItemStream을 구현해 read 위치를 저장해야 한다.

java
public class ConfluenceCursorReader extends AbstractItemCountingItemStreamItemReader<Page> {
 
    private String nextCursor;
 
    @Override
    public void open(ExecutionContext ctx) {
        this.nextCursor = ctx.getString("nextCursor", null);
    }
 
    @Override
    public void update(ExecutionContext ctx) {
        if (nextCursor != null) ctx.putString("nextCursor", nextCursor);
    }
 
    @Override
    protected Page doRead() {
        PageBatch batch = client.fetchAfter(nextCursor);
        nextCursor = batch.nextCursor();
        return batch.next();
    }
}

핵심은 "어디까지 읽었는가"를 외부에 위임할 수 있다는 점이다. Spring Batch는 이걸 자동으로 직렬화해 DB에 넣는다.

Event-Driven의 실패 처리 모델

같은 실패가 event 흐름에서는 DLQ**(Dead Letter Queue)**로 빠진다.

java
@KafkaListener(topics = "order.placed.v1", containerFactory = "retryFactory")
public void onOrderPlaced(OrderPlaced event, Acknowledgment ack) {
    try {
        notificationService.notify(event);
        ack.acknowledge();
    } catch (TransientException e) {
        throw e;
    } catch (PoisonMessageException e) {
        dlqTemplate.send("order.placed.v1.dlq", event);
        ack.acknowledge();
    }
}

여기서 가장 자주 틀리는 부분은 재시도와 영구 실패의 구분이다. Transient는 같은 메시지로 다시 시도해야 하지만, PoisonMessage**(스키마 깨짐, 비즈니스 규칙 위반)**는 같은 흐름에서 무한 재시도하면 컨슈머 전체가 멈춘다. DLQ 분리는 옵션이 아니라 필수다.

Batch와 달리 Event-Driven은 "skip"이 명시적이지 않다

Spring Batch의 skipLimit(50)은 "잘못된 건이 50개 이하라면 진행"이라는 명시적 정책이다. Event-Driven에서 같은 의미를 구현하려면 DLQ, 모니터링과 알림 임계치를 별도로 설계해야 한다. 이게 운영 부담의 한 축이다.


잘못된 선택 사례

Bad 1. "이벤트가 많으니까 일단 다 Kafka로 흘리자"

신규 서비스 설계 초기에 자주 보이는 함정이다. 하루 1~2회만 필요한 정산 집계까지 Kafka 메시지로 흘리면 broker 운영 비용은 늘고 consumer 코드는 복잡해진다. 명백히 batch가 맞는 시나리오를 굳이 event-driven으로 끌고 갈 이유가 없다.

Improved 1. "사건 반응은 event, 시간 윈도우 집계는 batch"

같은 주문 도메인이라도:

  • 주문 완료 → 알림/재고/통계 (event)
  • 일 단위 매출 정산 → batch
  • 휴면 회원 처리 → batch
  • 결제 실패 후속 (PG 재조회) → event 또는 short-cycle scheduler

Bad 2. "batch 한 번이 너무 무거우니 그냥 더 자주 돌리자"

5분 주기 batch로 만들면 사실상 micro-batching이 된다. 그 정도 latency가 필요하면 처음부터 event-driven 흐름이 적절하다. 5분 주기로 변경 데이터를 polling 하는 batch는 broker가 해줄 일을 DB에 떠넘기는 anti-pattern이 되기 쉽다.

Improved 2. CDC**(Change Data Capture)**로 polling을 대체

Debezium 같은 도구로 binlog 변경을 Kafka로 흘리면 polling batch의 부담이 broker로 옮겨간다. Outbox 패턴의 Transaction Log Tailing 변형이 이 사고방식이다.

Bad 3. 이벤트로 강한 ordering을 요구하기

같은 사용자에 대해 이벤트 A → B → C가 반드시 이 순서로 처리되어야 하는데 partition key를 user-agnostic하게 잡으면 깨진다. ordering이 핵심이라면 partition key 설계부터 들어가거나, batch로 정렬된 데이터를 처리하는 길이 더 안전하다.


Batch와 Event-Driven의 조합

패턴 1. Event-Driven이 1차, Batch가 정합성 백업

이벤트로 즉시 동기화하되, 새벽 batch가 "지난 24시간 동안 누락된 동기화가 있는지" 비교해 보정하는 흐름이다. 알림 도착률, 검색 색인 동기화율 등에 자주 쓴다.

패턴 2. Batch가 끝나면서 이벤트를 발행

대용량 색인 재구축**(reindex)** batch가 끝나면 IndexRebuildCompleted 이벤트를 발행해 그걸 듣는 다른 컴포넌트들이 캐시 무효화, 별칭 스왑, 알림을 수행하는 흐름이다. Spring Batch의 JobExecutionListener.afterJob에서 이벤트를 발행한다.

java
@Component
@RequiredArgsConstructor
public class IndexJobCompletionPublisher implements JobExecutionListener {
 
    private final ApplicationEventPublisher events;
 
    @Override
    public void afterJob(JobExecution jobExecution) {
        if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
            events.publishEvent(new IndexRebuildCompleted(
                jobExecution.getJobInstance().getJobName(),
                jobExecution.getEndTime()
            ));
        }
    }
}

패턴 3. Outbox + Batch 폴백

이벤트 발행은 Outbox 테이블로 안전화하고, 별도 batch가 "오래된 published=false 행이 있는지" 점검해 알림을 띄운다. 이벤트 시스템의 빈틈을 batch가 메우는 구조다.


로컬 실습 환경

bash
docker run -d --name mysql-batch \
  -e MYSQL_ROOT_PASSWORD=root \
  -e MYSQL_DATABASE=batch \
  -p 3306:3306 \
  mysql:8.4
bash
docker run -d --name kafka-local \
  -e KAFKA_ENABLE_KRAFT=yes \
  -e KAFKA_CFG_PROCESS_ROLES=broker,controller \
  -e KAFKA_CFG_NODE_ID=1 \
  -e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
  -e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 \
  -e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 \
  -e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
  -p 9092:9092 bitnami/kafka:3.7

Spring Batch 쪽은 spring-boot-starter-batch, BatchAutoConfiguration과 JobLauncherApplicationRunner로 가장 빠르게 띄울 수 있다. Event-Driven 쪽은 spring-kafka, KafkaTemplate과 @KafkaListener로 publisher와 consumer를 같은 모듈 안에 두고 실습하면 된다.

실습 시나리오 추천:

  • batch — 1만 건 더미 데이터를 reader가 읽어 processor가 임베딩 흉내**(Thread.sleep 100ms)**를 내고 writer가 OpenSearch에 쓴다. chunk size를 10/100/1000으로 바꿔가며 throughput을 비교.
  • event — order.placed.v1 topic에 producer가 초당 100건 발행하고 consumer 두 개**(notification, statistics)**가 각자 처리. consumer 하나를 강제 죽였다가 부활시켜 lag을 확인.
on this page
  • 01왜 중요한가
  • 02두 패러다임의 본질적 차이
  • 트리거와 시간 관점
  • 처리 단위 관점
  • 정합성 관점
  • 03Spring Batch의 Step 단위 실패 격리
  • Chunk-Oriented Processing
  • AsyncItemProcessor와 I/O 병렬화
  • 04Event-Driven의 핵심 구성요소
  • Partition Key 선택이 ordering을 결정한다
  • Consumer Group과 수평 확장
  • Replay와 Offset
  • 05선택 기준
  • 06실패 처리 방식의 차이
  • Batch의 실패 처리 모델
  • Event-Driven의 실패 처리 모델
  • Batch와 달리 Event-Driven은 "skip"이 명시적이지 않다
  • 07잘못된 선택 사례
  • Bad 1. "이벤트가 많으니까 일단 다 Kafka로 흘리자"
  • Improved 1. "사건 반응은 event, 시간 윈도우 집계는 batch"
  • Bad 2. "batch 한 번이 너무 무거우니 그냥 더 자주 돌리자"
  • Improved 2. CDC**(Change Data Capture)**로 polling을 대체
  • Bad 3. 이벤트로 강한 ordering을 요구하기
  • 08Batch와 Event-Driven의 조합
  • 패턴 1. Event-Driven이 1차, Batch가 정합성 백업
  • 패턴 2. Batch가 끝나면서 이벤트를 발행
  • 패턴 3. Outbox + Batch 폴백
  • 09로컬 실습 환경
tags
#study

이런 글도

  • architecture

    코드 제거 작업의 grep 통과 조건과 문서 부패

    2026.10.02
  • architecture

    캐시 설계 전략 총정리

    2026.09.03
  • architecture

    분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가

    2026.09.03
  • architecture

    분산 트랜잭션

    2026.09.03

댓글 (0)