Feature proposal · Spring Kafka · enhancement

Spring Kafka Issue #4523 — First-class DLT replay API

@RetryableTopic 파이프라인에서 DLT 에 들어간 레코드를 운영자가 직접 손대지 않고 framework가 자동으로 다시 retry chain으로 흘려주는 first-class API. 다시 실패하면 parking queue로 보내 human-only 검토 대상으로 격리. 전부 opt-in.

배경

@RetryableTopic chain이 retry 소진되면 레코드는 DLT 에 안착하고 거기서 멈춤. 그 다음 — "root cause 가 수정된 뒤 DLT 레코드를 다시 흘려보내기" — 는 현재 100% 운영자 수동 작업: 콘솔에 들어가서 레코드 persist, 직접 producer 호출, 헤더 맞추기, 빌어서 원본 토픽으로 재전송.

DLT 에 어떤 레코드가 자동 재처리 안전 대상이고 어떤 건 사람이 봐야 하는지 결정하는 정책, 그리고 재처리도 또 실패한 진짜 poison-pill 의 종착지(parking queue) 까지 — 모두 팀마다 ad-hoc 으로 다시 짜고 있음. 헤더 컨벤션 / cap / parking 시맨틱이 팀마다 일관되지 않음.

2022년 #2172 가 같은 주제로 열렸다가 "직접 DestinationTopicResolver 로 만드세요" 로 닫혔지만, 이후로도 동일 use case가 SO·Slack·discussions 에 반복 등장. framework 차원의 contract 가 필요한 시점.

흐름

Main Topic
   |
   v
 Consumer
   |
   +--> DLT (retries exhausted)
            |
            v
      DLT Reprocessor  (new — automated)
            |
   +--------+--------+
   |                 |
Success           Still Fail
   |                 |
Main Topic       Parking Queue
                     |
                     v
              human-only review
                  (terminal)

제안 요지 — 4 dimensions

  1. Replay entry pointDltReplayService 빈. ConsumerRecord 또는 (key, value, headers, originalTopic) tuple 받아서 DestinationTopicResolver 가 resolve한 retry chain entry로 republish.
  2. Eligibility policyDltReplayPolicy. 어떤 DLT 레코드가 auto-replay 대상인지 결정. 빌트인: ALL / EXCEPTION_ALLOWLIST / EXCEPTION_DENYLIST / HEADER_MARKER (운영자가 콘솔에서 kafka_dlt-replay-ready 마킹) / MANUAL_ONLY (기본) / 커스텀 Predicate.
  3. Cap + parking topicmax-replay-attempts (default 1) 카운터를 kafka_dlt-replay-attempt 헤더에 stamp. 초과 시 parkingTopic(default <original>-DLT-PARKED) 으로 라우팅. 종착지, human-review-only.
  4. Audit hookDltReplayAuditor SPI. onReplay / onParked / onReplayFailed. 기본은 INFO 로깅만, 사용자가 티켓팅·감사 시스템과 연동.

API sketch

public interface DltReplayService {
    CompletableFuture<SendResult<?, ?>> replay(ConsumerRecord<?, ?> dltRecord);
    CompletableFuture<SendResult<?, ?>> replay(byte[] key, byte[] value, Headers headers, String originalTopic);
}

public interface DltReplayPolicy {
    boolean isEligible(ConsumerRecord<?, ?> dltRecord);
}

public interface DltReplayAuditor {
    void onReplay(ConsumerRecord<?, ?> dltRecord, SendResult<?, ?> result);
    void onParked(ConsumerRecord<?, ?> dltRecord, int attempts);
    void onReplayFailed(ConsumerRecord<?, ?> dltRecord, Throwable cause);
}

대안과 비교

  • "DIY 패턴만 docs 화" — #2172의 결론. 팀마다 같은 plumbing 재구현, 헤더/cap/parking 시맨틱 비일관.
  • "DLT 자체에 타이머 두고 N분 지난 거 자동 replay" — root-cause 해결은 사람 신호이지 시계 신호가 아님. 시계 기반 replay = retry-storm 위험.
  • "cap 없이 무한 replay" — 진짜 poison-pill 이 영원히 순환, DLT 가 정착하지 않고, ops 가 transient/permanent 구분 불가.
  • "클러스터 공통 graveyard topic 하나" — per-listener topology 보장 깨짐. per-listener parking (DLT 패턴 그대로 미러) 가 단순.

구현 로드맵

  • Phase 1DltReplayService + DltReplayPolicy + DltReplayAuditor + 헤더 상수 + autoconfiguration 토글. parking 아직 없음 (cap=1 시 두 번째 실패는 그냥 다시 DLT, status quo).
  • Phase 2 — parking topic + onParked audit + auto-create (opt-in).
  • Phase 3 (별도 issue) — REST facade autoconfiguration (manual replay 용).

maintainer 협의 후 Phase 1 + 2 PR 진행 의향 있음. PR body에 #2172 링크 + 본 issue 링크 첨부 예정.

제출 현황

관련 자료