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-Letterfrom 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-Letterspark_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 outputdevices_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 outputdevices_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_letteredboolean 컬럼이나 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/