1078 words
5 minutes
[DE Design Pattern]03-1. Dead-Letter

1. Dead-Letter 패턴#

스트리밍/배치 파이프라인에서 일부 레코드가 처리 불가능(poison pill)할 때, fail-fast로 전체 잡을 멈출 것인가, 아니면 해당 레코드만 분리하고 계속 처리할 것인가?

01. 에러유형#

  • Transient error는 일시적 장애(DB 순간 다운 등)로 재시도하면 자연 복구됨
  • Non-transient error는 레코드 자체가 깨져있어 복구되지 않는 에러

Dead-Letter 패턴은 기본적으로 non-transient error를 어떻게 처리할 것인가의 문제

패턴 구조#

Dead-Letter 패턴의 아키텍처는 4개 컴포넌트로 구성됨

여기서 Figure 3-1을 참조하는데, 이는 Data processing job → Main dataset(정상)과 Dead-Letter storage(에러) 두 경로로 분기되고, Dead-Letter storage에 Monitoring & Alerting이 연결되며, 선택적으로 Data replay pipeline이 Dead-Letter에서 Main dataset으로 재투입하는 구조를 보여줍니다.

에러 식별 지점 파악안전 장치(try-catch 또는 if-else) 추가에러 레코드를 별도 출력으로 라우팅모니터링(선택) 리플레이 파이프라인

구현 방식 1: try-catch 기반#

  • 스트리밍에서는 레코드 단위로 처리하므로 개별 레코드를 dead-letter로 전송 가능
# Apache Flink - side output 기반 Dead-Letter
from pyflink.datastream import OutputTag
invalid_data_output = OutputTag('invalid_data')
def map_rows(self, json_payload: str) -> str:
try:
evt = json.loads(json_payload)
evt_time = int(datetime.datetime.fromisoformat(evt['event_time']).timestamp())
yield json.dumps({'visit_id': evt['visit_id'], 'page': evt['page']})
except Exception as e:
# 실패한 레코드를 side output으로 전송
yield self.invalid_data_output, _wrap_input_with_error(json_payload, e)
# 정상/에러 각각 다른 sink로 전송
visits.sink_to(kafka_sink_valid_data)
visits.get_side_output(invalid_data_output).sink_to(kafka_sink_invalid_data)

핵심: try 블록에서 정상 처리, except에서 side output으로 라우팅. 잡은 멈추지 않습니다.

구현 방식 2: error-safe 함수 기반 (배치/SQL)#

  • 입력은 있는데 출력이 null일 경우 null로 판단
# Spark SQL - error-safe 함수 기반 Dead-Letter
spark_session.sql('''
SELECT type, full_name, version, name_with_version,
CASE
WHEN (full_name IS NOT NULL OR version IS NOT NULL)
AND name_with_version IS NULL THEN false
ELSE true
END AS is_valid
FROM (
SELECT type, full_name, version,
CONCAT(full_name, '_', version) AS name_with_version
FROM devices_to_load
)
''')
# 캐싱 후 정상/에러 분리
devices_to_load_with_validity_flag.persist()
# 정상 레코드 → main output
devices_to_load_with_validity_flag.filter('is_valid = true') \
.drop('is_valid').write.mode('overwrite') \
.format('delta').save(f'{base_dir}/output/devices')
# 에러 레코드 → dead-letter output
devices_to_load_with_validity_flag.filter('is_valid = false') \
.drop('is_valid').write.mode('overwrite') \
.format('delta').save(f'{base_dir}/output/devices_dead_letter')

핵심: .persist()로 쿼리 중복 실행 방지 → is_valid 플래그로 분기 저장.

DLQ관련 Consequences#

  • Snowball backfilling effect: dead-letter 레코드를 리플레이하면 해당 파티션을 이미 처리한 downstream consumer들도 backfill해야 하기 때문에 연쇄 backfill이 발생함
  • Ordering 깨짐: 10:00, 10:01, 10:02 중 10:01이 dead-letter되었다가 리플레이될 경우 출력 순서가 10:00, 10:02, 10:01이 됨
  • Error vs Failure 구분: dead-letter가 에러를 숨기므로, 너무 많은 레코드가 drop되면 이는 에러가 아니라 시스템 장애일 수 있습니다. 반드시 모니터링 + 알림으로 임계치 초과 시 잡을 중단하는 로직필요
  • Dead-lettered records 식별: 리플레이된 레코드를 구분하기 위해 was_dead_lettered boolean 컬럼이나 job name/version/replay time 메타데이터를 추가

Concept

  • Dead-Letter 패턴 : 처리 불가능한 레코드를 별도 저장소에 분리하여 파이프라인 가동을 유지하는 에러 관리 패턴
  • Transient Error : 재시도로 자연 복구되는 일시적 에러 (예: DB 순간 다운, 네트워크 타임아웃)
  • Non-transient Error (Poison Pill) : 레코드 자체 결함으로 영원히 복구 불가능한 에러
  • Side Output : 스트리밍 프레임워크에서 메인 출력 외 추가 출력 경로. Flink의 OutputTag로 구현
  • Error-safe Function : 에러 시 예외 대신 NULL을 반환하는 함수 (예: SQL CONCAT). 에러를 숨기므로 Dead-Letter 구현이 더 복잡해짐
  • Replay Pipeline : Dead-Letter 저장소의 레코드를 메인 데이터 흐름에 재투입하는 파이프라인
  • Snowball Backfilling Effect : 리플레이로 인해 downstream consumer 체인 전체에 연쇄 backfill이 발생하는 현상
  • Fail-fast : 에러 발생 시 즉시 잡을 멈추는 전략. 단순하지만 스트리밍에는 부적합할 수 있음
  • Metadata Decorator 패턴 : 실패 레코드에 에러 메시지, 타임스탬프 등 메타데이터를 추가하여 사후 분석을 돕는 패턴

  • DLQ (Dead Letter Queue) in Kafka : Kafka에서의 dead-letter 구현 방식과 consumer group 관리
[DE Design Pattern]03-1. Dead-Letter
https://yjinheon.netlify.app/posts/02de/de-design-pattern/03-dead-letter/03-dl-01-dead-letter/
Author
Datamind
Published at
2025-01-31
License
CC BY-NC-SA 4.0