At-most-once, At-least-once, Exactly-once
메시지 전달 보장(Delivery Semantics)은 분산 시스템에서 메시지가 어떻게 전달되는지를 정의하는 핵심 개념입니다.
메시지 전달 보장 개요
세 가지 전달 보장 수준
┌─────────────────────────────────────────────────────────────────┐
│ Message Delivery Semantics │
├─────────────────┬─────────────────┬─────────────────────────────┤
│ At-most-once │ At-least-once │ Exactly-once │
│ (최대 1회) │ (최소 1회) │ (정확히 1회) │
├─────────────────┼─────────────────┼─────────────────────────────┤
│ 0 or 1 번 전달 │ 1+ 번 전달 │ 정확히 1번 전달 │
│ 손실 가능 │ 중복 가능 │ 손실/중복 없음 │
│ 구현 간단 │ 구현 중간 │ 구현 복잡 │
└─────────────────┴─────────────────┴─────────────────────────────┘
비교 요약
| 특성 | At-most-once | At-least-once | Exactly-once |
|---|---|---|---|
| 메시지 손실 | 가능 | 없음 | 없음 |
| 메시지 중복 | 없음 | 가능 | 없음 |
| 성능 | 최고 | 높음 | 중간 |
| 구현 복잡도 | 낮음 | 중간 | 높음 |
| 사용 사례 | 로그, 메트릭 | 일반 비즈니스 | 금융, 결제 |
At-most-once (최대 1회 전달)
정의
메시지가 한 번 이하 전달됩니다. 메시지 손실이 발생할 수 있지만 중복은 없습니다.
동작 방식
Producer Broker Consumer
│ │ │
│──── send message ───────>│ │
│ (no ack wait) │ │
│ │──── deliver message ────>│
│ │ (commit before │
│ │ processing) │
│ │ │
메시지 손실 시나리오:
- Producer → Broker 전송 실패 (감지 못함)
- Consumer 커밋 후 처리 실패
Producer 구현
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "0"); // 브로커 응답 대기하지 않음
props.put("retries", "0"); // 재시도 없음
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// Fire-and-forget 전송
producer.send(new ProducerRecord<>("topic", "key", "value"));
// 결과 확인하지 않음Consumer 구현
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "at-most-once-group");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "100"); // 빠른 커밋
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
// 오프셋이 먼저 커밋됨 (auto commit)
for (ConsumerRecord<String, String> record : records) {
try {
processRecord(record); // 실패해도 이미 커밋됨
} catch (Exception e) {
log.error("Processing failed, message lost", e);
// 메시지 손실 발생
}
}
}사용 사례
- 로그 수집: 일부 로그 손실이 허용되는 경우
- 메트릭/통계: 통계적으로 의미 있는 데이터
- 실시간 모니터링: 최신 데이터가 중요한 경우
- 센서 데이터: 주기적으로 중복 수집되는 데이터
장단점
장점:
- 가장 빠른 성능
- 구현 간단
- 리소스 사용 최소
단점:
- 메시지 손실 가능
- 데이터 정확성 보장 불가
At-least-once (최소 1회 전달)
정의
메시지가 한 번 이상 전달됩니다. 손실은 없지만 중복이 발생할 수 있습니다.
동작 방식
Producer Broker Consumer
│ │ │
│──── send message ───────>│ │
│<──── ack ────────────────│ │
│ (retry if no ack) │ │
│ │──── deliver message ────>│
│ │ (process before │
│ │ commit) │
│ │<──── commit offset ──────│
│ │ │
중복 발생 시나리오:
- Producer: ack 손실 → 재전송 → 중복 저장
- Consumer: 처리 후 커밋 전 장애 → 재시작 → 중복 처리
Producer 구현
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all"); // 모든 ISR의 응답 대기
props.put("retries", Integer.MAX_VALUE); // 무한 재시도
props.put("retry.backoff.ms", "100");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// 동기 전송으로 전달 보장
try {
RecordMetadata metadata = producer.send(
new ProducerRecord<>("topic", "key", "value")
).get();
log.info("Message sent to partition {} offset {}",
metadata.partition(), metadata.offset());
} catch (Exception e) {
// 재시도 로직 또는 에러 처리
log.error("Failed to send message", e);
}Consumer 구현
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "at-least-once-group");
props.put("enable.auto.commit", "false"); // 수동 커밋
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
// 먼저 처리
processRecord(record);
}
// 처리 완료 후 커밋
consumer.commitSync();
}중복 처리 전략
1. 멱등성 처리 (Idempotent Processing)
public void processRecord(ConsumerRecord<String, String> record) {
String messageId = extractMessageId(record);
// 이미 처리된 메시지인지 확인
if (processedMessages.contains(messageId)) {
log.info("Duplicate message skipped: {}", messageId);
return;
}
// 실제 처리
doProcess(record);
// 처리 완료 기록
processedMessages.add(messageId);
}2. 유니크 제약 조건
-- 데이터베이스 레벨에서 중복 방지
CREATE TABLE orders (
order_id VARCHAR(50) PRIMARY KEY, -- 메시지 키를 PK로
...
);
-- INSERT 시 중복 무시
INSERT INTO orders (order_id, ...)
VALUES (?, ...)
ON CONFLICT (order_id) DO NOTHING;3. 버전 기반 업데이트
public void processUpdate(ConsumerRecord<String, String> record) {
Order order = parseOrder(record);
// 버전이 높은 경우에만 업데이트
int updated = jdbcTemplate.update(
"UPDATE orders SET data = ?, version = ? " +
"WHERE id = ? AND version < ?",
order.getData(), order.getVersion(),
order.getId(), order.getVersion()
);
if (updated == 0) {
log.info("Skipped older version: {}", order);
}
}사용 사례
- 대부분의 비즈니스 애플리케이션
- 이벤트 처리: 멱등성으로 중복 처리 가능
- 알림 시스템: 중복 알림이 손실보다 나은 경우
- 데이터 동기화: 최종 일관성 보장
장단점
장점:
- 메시지 손실 없음
- 구현 비교적 간단
- 성능과 신뢰성 균형
단점:
- 중복 처리 가능
- 멱등성 구현 필요
Exactly-once (정확히 1회 전달)
정의
메시지가 정확히 한 번만 전달됩니다. 손실도 중복도 없습니다.
구현 방식
Kafka에서 Exactly-once를 달성하는 방법:
┌─────────────────────────────────────────────────────────────────┐
│ Exactly-once 구현 방법 │
├─────────────────────────────────────────────────────────────────┤
│ 1. Idempotent Producer │
│ - Producer → Broker 간 중복 방지 │
│ - enable.idempotence = true │
├─────────────────────────────────────────────────────────────────┤
│ 2. Transactional Producer + Consumer │
│ - Producer + Consumer 전체 트랜잭션 │
│ - isolation.level = read_committed │
├─────────────────────────────────────────────────────────────────┤
│ 3. External System + Idempotent Consumer │
│ - 외부 시스템에서 멱등성 보장 │
│ - 오프셋과 데이터를 원자적으로 저장 │
└─────────────────────────────────────────────────────────────────┘
Idempotent Producer
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("enable.idempotence", "true"); // 멱등성 활성화
// 자동으로 설정됨: acks=all, retries=MAX, max.in.flight.requests.per.connection<=5
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// 재시도해도 중복 저장 없음
producer.send(new ProducerRecord<>("topic", "key", "value"));Transactional Producer
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("transactional.id", "my-transactional-id");
props.put("enable.idempotence", "true");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new ProducerRecord<>("topic1", "key", "value1"));
producer.send(new ProducerRecord<>("topic2", "key", "value2"));
producer.commitTransaction();
} catch (Exception e) {
producer.abortTransaction();
throw e;
}Exactly-once Consumer (Kafka Streams)
Properties props = new Properties();
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "exactly-once-app");
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG,
StreamsConfig.EXACTLY_ONCE_V2); // Exactly-once 보장
StreamsBuilder builder = new StreamsBuilder();
builder.stream("input-topic")
.mapValues(value -> processValue(value))
.to("output-topic");
KafkaStreams streams = new KafkaStreams(builder.build(), props);
streams.start();외부 시스템과 Exactly-once
// 트랜잭션으로 데이터와 오프셋을 함께 저장
public void processExactlyOnce(ConsumerRecords<String, String> records) {
for (ConsumerRecord<String, String> record : records) {
try {
database.beginTransaction();
// 1. 비즈니스 데이터 저장
database.saveData(record.key(), record.value());
// 2. 오프셋 저장 (같은 트랜잭션)
database.saveOffset(
record.topic(),
record.partition(),
record.offset()
);
database.commit();
} catch (Exception e) {
database.rollback();
throw e;
}
}
}사용 사례
- 금융 거래: 이중 결제 방지
- 재고 관리: 정확한 수량 관리
- 이벤트 소싱: 이벤트 정확성 보장
- Kafka Streams: 스트림 처리 파이프라인
장단점
장점:
- 완벽한 데이터 정확성
- 손실/중복 없음
단점:
- 구현 복잡
- 성능 오버헤드
- 추가 리소스 필요
전달 보장 선택 가이드
결정 플로우차트
메시지 손실이 허용되는가?
│
├── Yes → At-most-once
│ (로그, 메트릭, 센서 데이터)
│
└── No → 메시지 중복이 허용되는가?
│
├── Yes → At-least-once
│ (대부분의 비즈니스 로직)
│ + 멱등성 처리 권장
│
└── No → Exactly-once
(금융, 결제, 재고)
시나리오별 권장 설정
로그 수집 (At-most-once)
// Producer
props.put("acks", "0");
props.put("retries", "0");
// Consumer
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "100");주문 처리 (At-least-once + 멱등성)
// Producer
props.put("acks", "all");
props.put("retries", Integer.MAX_VALUE);
props.put("enable.idempotence", "true");
// Consumer
props.put("enable.auto.commit", "false");
// + 멱등성 처리 로직결제 처리 (Exactly-once)
// Producer
props.put("transactional.id", "payment-producer");
props.put("enable.idempotence", "true");
// Consumer
props.put("isolation.level", "read_committed");
props.put("enable.auto.commit", "false");End-to-End 전달 보장
전체 파이프라인 고려
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Producer │───>│ Kafka │───>│ Consumer │───>│ External │
│ │ │ Broker │ │ │ │ System │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
└───────────────┴───────────────┴───────────────┘
전체 파이프라인에서 보장 필요
각 구간별 보장
| 구간 | At-most-once | At-least-once | Exactly-once |
|---|---|---|---|
| Producer → Broker | acks=0 | acks=all + retries | Idempotent |
| Broker Storage | - | replication | replication |
| Broker → Consumer | auto-commit | manual commit | transaction |
| Consumer → External | - | idempotent | atomic write |
댓글 (0)