홈으로 돌아가기

Kafka 멱등성: 실제 전달 보증

이 기사는 Kafka에서 프로듀서와 컨슈머를 위한 멱등성의 실제 설정을 다룹니다. ProducerId, ProducerEpoch, 중요한 구성 매개변수와 PostgreSQL의 비즈니스 로직을 통한 이중 처리 방지를 논의합니다.

멱등 Kafka: 중복 없는 프로듀서와 컨슈머
Advertisement 728x90

Kafka 전달 보장: Producer와 Consumer 멱등성 실전

inventory-service는 Outbox Pattern과 전달 보장을 지원하기 위해 인메모리 저장소에서 PostgreSQL로 전환합니다. docker-compose.yaml에 postgres-inventory 컨테이너를 추가하세요. image postgres:17, ports 5432:5432, 환경 변수 POSTGRES_USER=postgres, POSTGRES_PASSWORD=postgres, POSTGRES_DB=inventory_db.

inventory-service의 pom.xml에 다음 의존성을 추가하세요:

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

application.properties에서 연결을 지정하세요:

Google AdInline article slot
spring.datasource.url=jdbc:postgresql://localhost:5432/inventory_db
spring.datasource.username=postgres
spring.datasource.password=postgres

inventory 테이블 생성:

CREATE TABLE inventory (
    id BIGSERIAL PRIMARY KEY,
    product_name VARCHAR(256) NOT NULL UNIQUE,
    quantity INT NOT NULL CHECK (quantity >= 0)
);

INSERT INTO inventory (product_name, quantity) VALUES
('Smartphone', 5),
('Tablet', 10),
('Desktop', 6);

Inventory 엔티티:

@Entity
@Table(name = "inventory")
public class Inventory {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "product_name", nullable = false, unique = true, length = 256)
    private String productName;

    @Column(name = "quantity", nullable = false)
    private Integer quantity;

    public Inventory(String productName, Integer quantity) {
        this.productName = productName;
        this.quantity = quantity;
    }
    // getters, setters
}

deductStock이 포함된 Repository:

Google AdInline article slot
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
    @Modifying
    @Query("UPDATE Inventory i SET i.quantity = i.quantity - :quantity WHERE i.productName = :productName AND i.quantity >= :quantity")
    int deductStock(@Param("productName") String productName, @Param("quantity") Integer quantity);
}

서비스의 KafkaListener 핸들러는 재고 가용성을 확인하고 InventoryReservedEvent 또는 InventoryRejectedEvent를 전송합니다.

acks 설정과 exactly-once 의미론 이해

spring.kafka.producer.acks 매개변수는 확인 응답을 제어합니다: 0은 at-most-once, 1은 부분적, all은 모든 팔로워 복제 시 at-least-once입니다.

분산 시스템에서 exactly-once 전달은 두 장군 문제(Two Generals' Problem)로 인해 불가능합니다. exactly-once 의미론으로 구현되는데, 이는 at-least-once 전달 + producer와 consumer 멱등성입니다.

Google AdInline article slot

Producer 멱등성: ProducerId, Epoch, 그리고 시퀀스 번호

멱등성은 ack 손실로 인한 재시도 시 중복을 방지합니다. 다음으로 활성화됩니다:

spring.kafka.producer.properties.enable.idempotence=true

브로커는 연결에 (ProducerId, ProducerEpoch) 쌍을 할당합니다:

  • ProducerId: 고유 producer 식별자
  • ProducerEpoch: 재시작 시 증가, 좀비 producer로부터 보호

각 메시지는 시퀀스 번호로 번호가 매겨집니다. 브로커는 ProducerId/Epoch/sequence 내 중복을 거부합니다.

좀비 producer 시나리오:

  • Producer가 크래시하지만 새 프로듀서 재시작 후 "부활"
  • 이전 프로듀서가 높은 시퀀스 번호로 계속 진행
  • 새 프로듀서가 1부터 시작 — ProducerEpoch로 구분

Transactional.id는 재시작 시 ProducerId 보존을 보장합니다 (Spring이 initTransactions() 호출). 없으면 새 랜덤 ProducerId입니다.

중요한 멱등성 매개변수

spring.kafka.producer.acks=all
spring.kafka.producer.retries=2147483647  # >0
spring.kafka.producer.properties.max.in.flight.requests.per.connection=5  # ≤5
spring.kafka.producer.properties.enable.idempotence=true

위반 시 멱등성이 비활성화됩니다. order-service와 inventory-service에 적용하세요.

이중 처리에 대한 Consumer 멱등성

컨슈머가 오프셋 커밋 후 비즈니스 로직 완료 전에 크래시하면 메시지를 두 번 처리할 수 있습니다. inventory-service의 경우:

  • OrderPlacedEvent 읽기
  • PostgreSQL에서 deductStock() (원자적으로)
  • InventoryReservedEvent 전송
  • 크래시
  • 오프셋 재읽기, deductStock() 반복 — 하지만 quantity 이미 감소, UPDATE가 작동하지 않음

이는 조건부 UPDATE를 통해 비즈니스 로직 수준에서 멱등성을 보장합니다.

이 접근법의 장점:

  • 추가 오프셋 저장소 불필요
  • DB ACID 트랜잭션 활용
  • WHERE quantity >= :quantity로 간단 구현

주요 포인트

  • Producer 멱등성: enable.idempotence=true + acks=all + retries>0 + max.in.flight≤5
  • ProducerEpoch: 재시작 시 좀비 producer 보호
  • Transactional.id: ProducerId 보존, initTransactions() 필요
  • Consumer: DB 조건부 UPDATE로 비즈니스 로직 멱등성 보장
  • Exactly-once 의미론: at-least-once + 멱등성, 문자 그대로의 exactly-once 아님

— Editorial Team

Advertisement 728x90

다음 읽기