Spring Boot · 2026-02-19

Spring Boot과 Kafka 스트리밍: 트랜잭션과 재시도

Spring Boot와 Apache Kafka의 트랜잭션, 재시도 패턴을 예제와 설정 중심으로 쉽게 설명하는 스트리밍 처리 개념

작성일 : 2026-02-19 ㆍ 작성자 : 관리자
post
목차

개요

Kafka로 스트리밍을 처리할 때 데이터 일관성과 내구성을 확보하는 것이 핵심이다. 특히 트랜잭션을 사용한 정확히 한 번 처리와 재시도 전략은 운영 환경에서 흔히 직면하는 요구사항이다. 이 글에서는 Spring Boot 환경에서 Kafka 트랜잭션 설정, 재시도 구성, 그리고 실무에서 고려할 점을 예제와 함께 정리한다.

트랜잭션의 목적과 한계

왜 트랜잭션이 필요한가

트랜잭션은 생산자 측에서 여러 레코드를 원자적으로 전송하거나, 소비 후 외부 시스템에 반영할 때 중간 상태가 남지 않도록 보장한다. 정확히 한 번 처리(exactly-once)를 목표로 할 때 주요 수단이다.

한계와 현실적 고려사항

  • Kafka의 트랜잭션은 토픽 파티션 단위 성격과 브로커 설정에 따라 동작이 달라진다.
  • 컨슈머와 프로듀서를 함께 트랜잭션 경계에 묶을 때에는 offset 커밋 방식과 격리 수준(read_committed)을 맞춰야 한다.
  • 외부 DB와의 2PC는 지원하지 않으므로, 외부 시스템은 보상 또는 아이덴포턴트(idempotent) 설계가 필요하다.

Spring Kafka에서 트랜잭션 설정

Spring Boot에서 Kafka 트랜잭션을 사용하려면 ProducerFactory에 트랜잭션 아이디 프리픽스를 설정하고 KafkaTransactionManager 또는 KafkaTemplate의 트랜잭션 API를 사용한다. 아래는 기본 설정 예제다.

package com.example.config;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

@Configuration
public class KafkaProducerConfig {
    @Bean
    public ProducerFactory<String, String> producerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
        // 트랜잭션을 사용하려면 transactionIdPrefix 설정
        DefaultKafkaProducerFactory<String, String> pf = new DefaultKafkaProducerFactory<>(props);
        pf.setTransactionIdPrefix("tx-");
        return pf;
    }

    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
}

이 설정으로 KafkaTemplate.executeInTransaction(...)를 이용해 트랜잭션 경계 내에서 전송할 수 있다. 또한 컨슈머에서 트랜잭션과 오프셋을 함께 처리하려면 ContainerProperties.setTransactionManager에 KafkaTransactionManager를 연결하면 된다.

정확히 한 번 처리 전략 (spring boot kafka exactly once)

정확히 한 번 처리를 위해 필요한 요소는 다음과 같다.

  • 프로듀서: idempotence 활성화 + 트랜잭션 사용
  • 브로커: transaction state log를 위한 충분한 설정과 안정성 확보
  • 컨슈머: isolation.level=read_committed 설정으로 커밋된 레코드만 소비
  • 외부 시스템: 아이덴포턴트 설계 또는 보상 트랜잭션

Spring Kafka에서는 컨테이너 팩토리의 속성으로 isolation.level을 설정하고, 트랜잭션 매니저를 연계하여 컨슈머 오프셋 커밋을 트랜잭션에 포함시킬 수 있다. 다만 네트워크 오류나 브로커 문제 등 현실적 실패에 대한 대비책은 별도로 설계해야 한다.

재시도 전략 (kafka spring boot 재시도)

재시도를 어디에 둘 것인가

재시도는 크게 두 층에서 수행할 수 있다.

  • 컨슈머 레벨: 메시지 처리 실패 시 컨테이너가 재시도하거나 별도의 Dead Letter Topic으로 이동
  • 프로듀서 레벨: 전송 실패 시 재시도 정책 (RetryTemplate 또는 프로듀서 자체 재시도)

Spring Kafka는 RetryTemplate과 SeekToCurrentErrorHandler, DefaultErrorHandler 등을 제공한다. 주로 처리 로직에서 일시적 오류를 재시도하고, 영구 오류는 DLT로 보낸다.

// 간단한 컨테이너 팩토리 재시도 예
ConcurrentKafkaListenerContainerFactory<String, String> factory =
        new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
// DefaultErrorHandler를 사용해 재시도와 DLT 설정
DefaultErrorHandler errorHandler = new DefaultErrorHandler(
        new DeadLetterPublishingRecoverer(kafkaTemplate), new FixedBackOff(1000L, 3));
factory.setCommonErrorHandler(errorHandler);

운영에서 고려할 점

  • 트랜잭션을 남발하면 성능 저하가 발생하므로, 트랜잭션 경계를 신중히 설계한다.
  • DLT 관리는 비즈니스적 처리와 재처리 정책을 명확히 한다.
  • 모니터링과 메트릭(지연, 재시도 횟수, DLT 비율)을 통해 이상을 조기에 탐지한다.
  • 테스트: 브로커 장애 시나리오와 재시도 로직을 통합 테스트로 검증한다.

요약

spring kafka 트랜잭션 설정은 프로듀서의 트랜잭션 아이디와 idempotence를 활성화하는 것부터 시작된다. kafka spring boot 재시도는 컨슈머와 프로듀서 양쪽에서 적절히 설계해야 하며, spring boot kafka exactly once 구현은 트랜잭션과 소비 격리 수준, 외부 시스템의 아이덴포턴스 설계가 결합되어야 가능하다. 실무에서는 성능, 복구 전략, 모니터링을 균형 있게 고려하는 것이 중요하다.

spring kafka 트랜잭션 설정 kafka spring boot 재시도 spring boot kafka exactly once Kafka 트랜잭션 Kafka 재시도 Spring Boot Kafka 스트리밍 처리 Dead Letter Topic