Part 11. 장애 대응 아키텍처: Partial Failure, Poison Data, DLQ, 재시도, 멱등성
배치 장애를 회피가 아닌 복구 가능한 상태로 설계하기 위한 실패 분류, DLQ, 재시도, 멱등성 패턴을 정리한다.
Series: Spring Boot 배치 전략 완전 정복
총 12편 구성. 현재 11편을 보고 있습니다.
- 01Part 1. 배치의 본질과 분류: 스케줄, 이벤트, 수동, 대량, Near-real-time
- 02Part 2. @Scheduled 실전 운영: 단순함의 대가와 멀티 인스턴스 함정
- 03Part 3. Quartz 클러스터 아키텍처: JobStore, Misfire, 대규모 스케줄 관리
- 04Part 4. Spring Batch 핵심: Chunk, 트랜잭션 경계, Restart 가능한 Job 설계
- 05Part 5. Spring Batch 확장: Partition과 Multi-threaded Step의 트레이드오프
- 06Part 6. 수동 배치 전략: REST 트리거, Admin UI, 파라미터 재처리, 롤백
- 07Part 7. DB 대량 조회 전략: OFFSET/LIMIT 한계와 Keyset, ID Range, Covering Index
- 08Part 8. OpenSearch/Elasticsearch 배치 전략: Scroll, Search After, PIT, Bulk, Rollover
- 09Part 9. 분산 환경 배치: Leader Election, Kubernetes CronJob, 락 전략 비교
- 10Part 10. 성능 최적화: 배치 사이즈, 커밋 간격, JVM 메모리, Backpressure
- 11Part 11. 장애 대응 아키텍처: Partial Failure, Poison Data, DLQ, 재시도, 멱등성CURRENT
- 12Part 12. 통합 레퍼런스 아키텍처와 최종 선택 가이드

출처: Pexels - Dashboard warning light
버전 기준
- Java 21
- Spring Boot 3.3.x
- Spring Batch 5.2.x
- Quartz 2.3.x
- PostgreSQL 15
- OpenSearch 2.x
1) 문제 제기
운영 배치에서 실패는 예외가 아니라 정상이다. 네트워크 타임아웃, 외부 API 제한, 데이터 품질 오류는 반드시 발생한다. 실패를 "없애는" 대신 "격리하고 복구"하는 설계가 필요하다.
대부분의 사고는 아래 패턴으로 커진다.
- 재시도 정책이 모든 예외에 동일하게 적용된다.
- 한 건의 Poison Data 때문에 전체 잡이 중단된다.
- 중복 실행 방어가 없어 재시도 자체가 부작용을 만든다.
2) 핵심 개념 정리
실패 분류
| 실패 유형 | 예시 | 처리 방식 |
|---|---|---|
| 일시적 실패(Transient) | 타임아웃, 429, deadlock | 제한 재시도 + 백오프 |
| 영구적 실패(Permanent) | 스키마 불일치, 필수값 누락 | DLQ 격리 후 수동 처리 |
| 부분 실패(Partial) | 10,000건 중 120건 실패 | 성공 커밋 + 실패 별도 누적 |
Poison Data 처리
Poison Data는 같은 입력으로 반복 실행해도 계속 실패하는 데이터다. 핵심은 "빠른 격리"다.
- 최대 재시도 횟수 초과 시 즉시 DLQ 이동.
- 원본 payload, 오류 스택, 처리 시점, 실행 버전을 함께 저장.
- DLQ 재처리 경로는 별도 승인 흐름으로 분리.
실패 상태 전이 다이어그램

출처: Pexels - Road warning sign
3) 코드 예시
예시 A: 예외 유형별 재시도 정책
public boolean shouldRetry(Throwable t) {
return t instanceof SocketTimeoutException
|| t instanceof DeadlockLoserDataAccessException
|| t instanceof HttpServerErrorException;
}
public Duration backoff(int attempt) {
long seconds = Math.min((long) Math.pow(2, attempt), 60);
return Duration.ofSeconds(seconds);
}
예시 B: 멱등 Writer (중복 방지)
INSERT INTO payout_result (request_id, account_id, amount, status, processed_at)
VALUES (:request_id, :account_id, :amount, :status, NOW())
ON CONFLICT (request_id)
DO NOTHING;
예시 C: DLQ 테이블 설계
CREATE TABLE batch_dlq (
id BIGSERIAL PRIMARY KEY,
job_name VARCHAR(100) NOT NULL,
source_id BIGINT NOT NULL,
payload JSONB NOT NULL,
error_type VARCHAR(100) NOT NULL,
error_message TEXT NOT NULL,
retry_count INT NOT NULL,
failed_at TIMESTAMP NOT NULL DEFAULT NOW(),
reprocess_status VARCHAR(20) NOT NULL DEFAULT 'PENDING'
);
CREATE INDEX idx_batch_dlq_job_failed ON batch_dlq (job_name, failed_at DESC);
CREATE INDEX idx_batch_dlq_reprocess_id ON batch_dlq (reprocess_status, id);
예시 D: DLQ 재처리 Keyset 조회
SELECT id, source_id, payload
FROM batch_dlq
WHERE reprocess_status = 'PENDING'
AND id > :last_id
ORDER BY id ASC
LIMIT 200;
4) 실제 장애/운영 시나리오
상황: 외부 정산 API 장애(HTTP 500)로 4만 건 중 8천 건이 실패했다. 시스템은 무제한 재시도를 수행했고, 외부 API가 복구된 후에도 요청 폭주로 다시 장애가 반복됐다.
원인:
- 재시도 상한이 없고 지수 백오프가 없었다.
- 영구 실패(잘못된 계좌번호)와 일시 실패를 구분하지 않았다.
- 멱등 키 없이 재시도해 중복 출금 시도가 발생했다.
개선:
- 예외 분류 기반 재시도(최대 3회) + 지수 백오프 적용.
- 영구 실패는 즉시 DLQ 격리.
request_id유니크 인덱스로 중복 출금 방지.
5) 설계 체크리스트
- 실패를 일시적/영구적/부분 실패로 분류하는가?
- 재시도 최대 횟수와 백오프 정책이 정의되어 있는가?
- Poison Data를 DLQ로 격리하는 경로가 있는가?
- DLQ payload에 재현 가능한 정보(입력/오류/버전)가 저장되는가?
- 멱등 키와 유니크 인덱스로 중복 부작용을 차단하는가?
- DLQ 재처리 역시 감사 추적과 승인 흐름을 갖추는가?
6) 요약
장애 대응 설계의 목표는 "실패를 없애는 것"이 아니라 "실패를 제어 가능한 상태로 유지"하는 것이다. 재시도, DLQ, 멱등성을 함께 설계하면 대규모 장애에서도 복구 가능성을 확보할 수 있다. 특히 운영자는 실패 건수를 줄이는 것보다, 실패를 예측 가능한 시간 안에 복구할 수 있어야 한다.
7) 다음 편 예고
다음 편(최종편)에서는 지금까지의 내용을 통합해 참조 아키텍처와 의사결정 매트릭스를 제시한다. 어떤 상황에서 @Scheduled, Quartz, Spring Batch, 수동 배치를 조합해야 하는지 최종 선택 기준을 제공한다.
참고 링크
- Spring Batch Reference
- Quartz Scheduler Documentation
- PostgreSQL Transaction Isolation
- 블로그: Idempotency Key API 설계