본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 10장: Exactly-once 보장과 신뢰성
2026년 2월 28일·데이터·

10장: Exactly-once 보장과 신뢰성

At-least-once/At-most-once/Exactly-once 비교, Kafka 트랜잭션과 Flink 체크포인트의 조합, 멱등성 설계, DLQ, 백프레셔, 장애 복구 전략까지 프로덕션 신뢰성을 학습합니다.

19분554자9개 섹션
streamingdata-engineering
공유
realtime-pipeline10 / 11
1234567891011
이전9장: 스키마 레지스트리와 데이터 계약다음11장: 프로덕션 모니터링과 운영

학습 목표

  • 세 가지 전달 보장 수준의 구현 원리를 깊이 이해합니다.
  • Kafka 트랜잭션과 Flink 체크포인트를 결합한 종단간 Exactly-once를 파악합니다.
  • 멱등성(Idempotency) 설계 원칙과 패턴을 학습합니다.
  • Backpressure(백프레셔) 메커니즘과 대응 전략을 이해합니다.
  • 장애 시나리오별 복구 전략을 수립합니다.

전달 보장 수준 심층 분석

4장에서 세 가지 전달 보장 수준을 간략히 소개했습니다. 이번 장에서는 각 수준의 구현 원리와 트레이드오프를 깊이 분석합니다.

At-most-once (최대 한 번)

메시지를 처리하기 전에 오프셋을 커밋합니다. 처리 중 장애가 발생하면 해당 메시지는 다시 처리되지 않습니다.

at-most-once-flow
text
1. 메시지 수신 (offset 10)
2. 오프셋 커밋 (offset 11) ← 처리 전에 커밋
3. 메시지 처리 ← 여기서 장애 발생!
4. 재시작 후 offset 11부터 읽기 → offset 10 메시지 유실

적합한 경우: 일부 데이터 유실이 허용되는 메트릭 수집, 로그 처리

At-least-once (최소 한 번)

메시지를 처리한 후에 오프셋을 커밋합니다. 처리는 완료했지만 커밋 전에 장애가 발생하면, 재시작 후 동일 메시지를 다시 처리합니다.

at-least-once-flow
text
1. 메시지 수신 (offset 10)
2. 메시지 처리 ← 성공
3. 오프셋 커밋 (offset 11) ← 여기서 장애 발생!
4. 재시작 후 offset 10부터 읽기 → offset 10 메시지 중복 처리

적합한 경우: 중복이 허용되거나 멱등 처리가 가능한 경우 (대부분의 시스템)

Exactly-once (정확히 한 번)

메시지 처리와 오프셋 커밋을 원자적으로 수행합니다. 유실도 중복도 발생하지 않습니다.

Info

엄밀히 말하면 "Exactly-once delivery"는 분산 시스템에서 불가능합니다. 실제로 구현되는 것은 "Effectively-once processing" — 내부적으로는 At-least-once 전달을 수행하되, 중복을 감지하고 제거하여 결과적으로 한 번만 처리된 것과 동일한 효과를 만들어냅니다.


종단간 Exactly-once

진정한 Exactly-once는 파이프라인의 모든 단계에서 보장되어야 합니다.

Kafka 내부: 트랜잭션

Kafka 내부의 read-process-write 패턴에서는 4장에서 학습한 트랜잭셔널 프로듀서로 Exactly-once를 달성합니다. 읽기(오프셋 커밋)와 쓰기를 하나의 트랜잭션으로 묶습니다.

Flink + Kafka: 체크포인트 + 2PC

Flink가 Kafka에서 읽어 처리하고 다시 Kafka에 쓰는 경우, Flink의 체크포인트와 Kafka의 트랜잭션을 결합하여 Exactly-once를 달성합니다.

이 과정은 Two-Phase Commit(2PC, 2단계 커밋) 프로토콜의 변형입니다.

1단계 (Pre-commit): 체크포인트 배리어가 도달하면 각 오퍼레이터가 상태를 스냅샷하고, Kafka Sink는 트랜잭션을 pre-commit 상태로 둡니다.

2단계 (Commit): 모든 오퍼레이터의 스냅샷이 성공하면 JobManager가 체크포인트 완료를 알리고, Kafka Sink가 트랜잭션을 최종 커밋합니다.

FlinkExactlyOnce.java
java
StreamExecutionEnvironment env =
    StreamExecutionEnvironment.getExecutionEnvironment();
 
// 체크포인트 활성화
env.enableCheckpointing(30_000, CheckpointingMode.EXACTLY_ONCE);
 
// Kafka Source (Exactly-once 읽기)
KafkaSource<String> source = KafkaSource.<String>builder()
    .setBootstrapServers("kafka:9092")
    .setTopics("orders")
    .setGroupId("flink-processor")
    .setStartingOffsets(OffsetsInitializer.committedOffsets())
    .setValueOnlyDeserializer(new SimpleStringSchema())
    .build();
 
// Kafka Sink (Exactly-once 쓰기)
KafkaSink<String> sink = KafkaSink.<String>builder()
    .setBootstrapServers("kafka:9092")
    .setRecordSerializer(
        KafkaRecordSerializationSchema.builder()
            .setTopic("orders-processed")
            .setValueSerializationSchema(new SimpleStringSchema())
            .build())
    .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE) // Exactly-once 보장
    .setTransactionalIdPrefix("flink-orders")              // 트랜잭션 ID 접두사
    .build();
 
DataStream<String> stream = env.fromSource(source, WatermarkStrategy.noWatermarks(), "Kafka");
stream.map(this::processOrder).sinkTo(sink);

외부 시스템에 대한 Exactly-once

Kafka가 아닌 외부 시스템(데이터베이스, API 등)에 결과를 쓸 때는 추가적인 설계가 필요합니다.

방법 1: 멱등 싱크

외부 시스템이 멱등 쓰기를 지원하면, At-least-once 전달 + 멱등 싱크로 Effectively-once를 달성합니다.

IdempotentSink.java
java
// Elasticsearch: 문서 ID 기반 upsert → 자연스러운 멱등 쓰기
// Redis: SET 명령 → 동일 키에 대한 반복 쓰기는 멱등
// DB: INSERT ... ON CONFLICT DO UPDATE → 멱등 upsert

방법 2: 트랜잭셔널 싱크 (2PC)

Flink는 일부 싱크에 대해 2PC 기반 Exactly-once를 지원합니다. TwoPhaseCommitSinkFunction을 구현하여, Flink 체크포인트와 외부 시스템의 트랜잭션을 연동합니다.

Warning

2PC 기반 트랜잭셔널 싱크는 외부 시스템이 트랜잭션을 지원해야 하며, 구현 복잡도가 높습니다. 가능하다면 멱등 싱크 방식을 우선 검토하세요. 대부분의 데이터베이스와 검색 엔진은 upsert를 지원하므로 멱등 설계가 가능합니다.


멱등성 설계

멱등성이란

**Idempotency(멱등성)**은 동일한 작업을 여러 번 수행해도 결과가 한 번 수행한 것과 동일한 성질입니다. At-least-once 전달에서 중복 메시지를 안전하게 처리하기 위한 핵심 설계 원칙입니다.

멱등 설계 패턴

1. 자연 키 기반 Upsert

idempotent-upsert.sql
sql
-- PostgreSQL: 주문 ID를 기준으로 upsert
INSERT INTO order_summary (order_id, total_amount, status, updated_at)
VALUES ('O-001', 50000, 'completed', NOW())
ON CONFLICT (order_id)
DO UPDATE SET
    total_amount = EXCLUDED.total_amount,
    status = EXCLUDED.status,
    updated_at = EXCLUDED.updated_at;

2. 이벤트 ID 기반 중복 방지

dedup_with_event_id.py
python
def process_event(event):
    event_id = event["eventId"]
 
    # 이미 처리된 이벤트인지 확인
    if redis_client.sismember("processed_events", event_id):
        return  # 중복 → 무시
 
    # 처리 수행
    execute_business_logic(event)
 
    # 처리 완료 기록 (TTL 설정으로 무한 증가 방지)
    redis_client.sadd("processed_events", event_id)
    redis_client.expire("processed_events", 86400)  # 24시간 보존

3. 버전 기반 낙관적 동시성 제어

optimistic-locking.sql
sql
-- 현재 버전보다 높은 경우에만 업데이트
UPDATE orders
SET status = 'shipped', version = 5
WHERE order_id = 'O-001' AND version = 4;
-- 영향받은 행이 0이면 이미 처리됨 (중복 또는 경합)
Tip

멱등 설계의 핵심은 결정론적 식별자입니다. 이벤트 ID, 엔티티의 자연 키, 또는 이벤트 내용의 해시를 활용하여 동일한 처리를 식별할 수 있어야 합니다. 타임스탬프나 자동 증가 ID를 식별자로 사용하면 멱등성이 깨집니다.


백프레셔

Backpressure란

**Backpressure(백프레셔)**는 하류 시스템이 상류 시스템의 데이터 생산 속도를 따라가지 못할 때 발생하는 현상입니다. 적절한 백프레셔 메커니즘이 없으면 메모리 초과, 데이터 유실, 시스템 장애로 이어집니다.

backpressure-scenario
text
프로듀서: 10,000 msg/sec →
Kafka: 충분한 용량 →
처리 엔진: 8,000 msg/sec (병목) →
싱크: 5,000 msg/sec (더 큰 병목)
 
→ 처리 엔진과 싱크에서 백프레셔 발생
→ 처리되지 않은 메시지가 계속 쌓임

Kafka의 백프레셔

Kafka 자체는 내장 백프레셔가 없습니다. 대신 컨슈머가 poll() 호출 빈도와 max.poll.records를 조절하여 소비 속도를 제어합니다. 처리가 느려지면 컨슈머 랙이 증가합니다.

KafkaBackpressure.java
java
Properties props = new Properties();
props.put("max.poll.records", "100");    // poll당 최대 레코드 수 제한
props.put("max.poll.interval.ms", "300000"); // 처리 시간 한도 (5분)
props.put("fetch.max.bytes", "1048576"); // fetch당 최대 바이트 (1MB)

Flink의 백프레셔

Flink는 크레딧 기반(credit-based) 백프레셔 메커니즘을 내장하고 있습니다. 하류 오퍼레이터의 버퍼가 가득 차면 상류 오퍼레이터에 "크레딧이 없다"고 알려, 데이터 전송을 자연스럽게 제한합니다.

flink-backpressure
text
Source → [buffer: 8/10] → Map → [buffer: 10/10 FULL] → Aggregate → Sink
                                      ↑
                            백프레셔 신호 역전파
                     Source의 전송 속도가 자동으로 감소

백프레셔 대응 전략

  1. 병목 식별: Flink 웹 UI에서 백프레셔가 발생하는 오퍼레이터를 확인
  2. 수평 확장: 병목 오퍼레이터의 병렬도를 높임
  3. 리소스 튜닝: TaskManager 메모리, 네트워크 버퍼 크기 조정
  4. 처리 최적화: 불필요한 직렬화/역직렬화 제거, 캐시 활용
  5. 외부 시스템 최적화: 싱크(DB, API)의 처리 능력 향상

Dead Letter Queue 심화

4장에서 기본적인 DLQ 패턴을 소개했습니다. 여기서는 프로덕션 수준의 DLQ 운영을 다룹니다.

DLQ 메시지 분류

DLQ에 메시지가 도착하면 실패 유형에 따라 대응이 달라집니다.

유형예시대응
일시적 오류네트워크 타임아웃, DB 과부하지수 백오프 후 재시도
스키마 오류잘못된 JSON, 필수 필드 누락프로듀서 수정 후 재발행
비즈니스 규칙 위반음수 금액, 유효하지 않은 상태데이터 보정 후 재처리
독성 메시지 (Poison Pill)파싱 불가한 바이너리폐기 (로그 보존)

DLQ 자동화

dlq_processor.py
python
class DLQProcessor:
    def __init__(self, max_retries=3, retry_delay_base=60):
        self.max_retries = max_retries
        self.retry_delay_base = retry_delay_base
 
    def process_dlq_message(self, message):
        retry_count = int(message.headers.get("retry-count", 0))
        failure_reason = message.headers.get("failure-reason", "")
 
        # 재시도 가능한 오류인지 판단
        if self.is_retriable(failure_reason) and retry_count < self.max_retries:
            delay = self.retry_delay_base * (2 ** retry_count)  # 지수 백오프
            self.schedule_retry(message, delay, retry_count + 1)
        else:
            # 재시도 불가 또는 한도 초과 → 영구 실패 큐로 이동
            self.move_to_permanent_dlq(message)
            self.alert_ops_team(message)
 
    def is_retriable(self, reason):
        retriable_patterns = [
            "timeout", "connection refused",
            "too many requests", "service unavailable"
        ]
        return any(p in reason.lower() for p in retriable_patterns)
Info

DLQ 모니터링은 파이프라인 건강 상태의 핵심 지표입니다. DLQ 메시지 수의 급증은 업스트림의 문제(스키마 변경, 데이터 품질 저하)를 조기에 감지하는 신호입니다. 프로덕션에서는 DLQ 메시지 증가율에 대한 알림을 반드시 설정하세요.


장애 복구 전략

장애 시나리오와 복구

장애 유형영향복구 전략
단일 브로커 장애일부 파티션 리더 이전자동 복구 (ISR 기반 리더 선출)
전체 Kafka 클러스터 장애모든 스트림 중단이벤트 소스에서 재전송, 오프셋 기반 재개
Flink TaskManager 장애태스크 일부 중단자동 재시작 + 체크포인트 복구
Flink JobManager 장애전체 작업 중단HA 설정(ZooKeeper/K8s) → 대기 JobManager 승격
컨슈머 그룹 전체 장애소비 중단, 랙 증가재시작 후 마지막 커밋 오프셋부터 재개
싱크 시스템 장애결과 미반영백프레셔 → 싱크 복구 후 자동 재개

Flink의 자동 복구 설정

FlinkRestartStrategy.java
java
StreamExecutionEnvironment env =
    StreamExecutionEnvironment.getExecutionEnvironment();
 
// 고정 지연 재시작: 최대 3회, 10초 간격
env.setRestartStrategy(
    RestartStrategies.fixedDelayRestart(3, Time.seconds(10)));
 
// 실패율 기반 재시작: 5분 내 3회 실패까지 허용
env.setRestartStrategy(
    RestartStrategies.failureRateRestart(
        3,                     // 허용 실패 횟수
        Time.minutes(5),       // 측정 기간
        Time.seconds(10)       // 재시작 간 대기
    ));
 
// 지수 백오프 재시작
env.setRestartStrategy(
    RestartStrategies.exponentialDelayRestart(
        Time.seconds(1),       // 초기 대기
        Time.minutes(5),       // 최대 대기
        2.0,                   // 배수
        Time.hours(1),         // 리셋 기간
        0.1                    // 지터
    ));

재처리 전략

장애로 인해 데이터를 재처리해야 하는 상황에 대비한 전략입니다.

reprocessing-strategies
text
1. 오프셋 리셋
   - 특정 시점으로 오프셋을 되돌려 해당 시점부터 재처리
   - 멱등 싱크 필수
 
2. Flink Savepoint
   - Savepoint에서 복원하여 해당 시점부터 재처리
   - 작업 수정 후 재배포에 유용
 
3. 새 컨슈머 그룹
   - 새 그룹 ID로 처음부터 읽기
   - 전체 재처리가 필요한 경우

정리

이번 장에서는 실시간 파이프라인의 신뢰성 확보 방법을 학습했습니다.

  • 전달 보장 수준: At-most-once, At-least-once, Exactly-once의 구현 원리와 트레이드오프를 이해했습니다.
  • 종단간 Exactly-once: Flink 체크포인트 + Kafka 트랜잭션(2PC)으로 구현됩니다.
  • 멱등성 설계: 자연 키 upsert, 이벤트 ID 중복 방지, 버전 기반 제어 등의 패턴을 학습했습니다.
  • 백프레셔: Flink의 크레딧 기반 백프레셔와 대응 전략을 파악했습니다.
  • 장애 복구: 시나리오별 복구 전략과 Flink의 자동 재시작 설정을 학습했습니다.

다음 장 미리보기

11장(마지막)에서는 프로덕션 모니터링과 운영을 다룹니다. 브로커/프로듀서/컨슈머의 핵심 메트릭, Prometheus/Grafana 대시보드 구성, 알림 설계, 용량 계획, 비용 최적화까지 실시간 파이프라인의 안정적 운영에 필요한 모든 것을 학습합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#streaming#data-engineering

관련 글

데이터

11장: 프로덕션 모니터링과 운영

브로커/프로듀서/컨슈머 핵심 메트릭, Prometheus/Grafana 대시보드, 알림 설계, 용량 계획, 비용 최적화, 실전 아키텍처까지 실시간 파이프라인의 프로덕션 운영을 학습합니다.

2026년 3월 2일·23분
데이터

9장: 스키마 레지스트리와 데이터 계약

스키마 진화의 필요성, Confluent Schema Registry, Avro/Protobuf/JSON Schema 비교, 호환성 규칙, 데이터 계약 개념까지 스키마 관리 전략을 체계적으로 학습합니다.

2026년 2월 26일·18분
데이터

8장: CDC(Change Data Capture)

CDC의 원리와 WAL 기반 변경 캡처, Debezium 아키텍처, PostgreSQL/MySQL CDC 실습, Flink CDC 3.6, 아웃박스 패턴, 이벤추얼 컨시스턴시까지 데이터 통합의 핵심을 학습합니다.

2026년 2월 24일·18분
이전 글9장: 스키마 레지스트리와 데이터 계약
다음 글11장: 프로덕션 모니터링과 운영

댓글

목차

약 19분 남음
  • 학습 목표
  • 전달 보장 수준 심층 분석
    • At-most-once (최대 한 번)
    • At-least-once (최소 한 번)
    • Exactly-once (정확히 한 번)
  • 종단간 Exactly-once
    • Kafka 내부: 트랜잭션
    • Flink + Kafka: 체크포인트 + 2PC
    • 외부 시스템에 대한 Exactly-once
  • 멱등성 설계
    • 멱등성이란
    • 멱등 설계 패턴
  • 백프레셔
    • Backpressure란
    • Kafka의 백프레셔
    • Flink의 백프레셔
    • 백프레셔 대응 전략
  • Dead Letter Queue 심화
    • DLQ 메시지 분류
    • DLQ 자동화
  • 장애 복구 전략
    • 장애 시나리오와 복구
    • Flink의 자동 복구 설정
    • 재처리 전략
  • 정리
  • 다음 장 미리보기