준하 파트
- [RedisOutboxRetryScheduler.java]에서 REFILL 성공 후 markSuccess를 먼저 찍고, 바로 뒤의 clearIdempotency 예외는 바깥 catch[RedisOutboxRetryScheduler.java]가 다시 FAIL로 되돌립니다. 그 상태로 재시도 상한을 넘기면 [RedisOutboxRetryScheduler.java]에서 DB 원천 잔량까지 복구해 버려, 이미 Redis에 반영된 refill과 중복 크레딧이 생길 수 있습니다.
- 개선
- [TrafficPolicyBootstrapService.java]에서 주기 reconcile이 사실상 제거됐습니다. 그런데 새 Lua self-heal은 키가 “없을 때만” GLOBAL_POLICY_HYDRATE를 내보내고[deduct_indiv.lua], stale value는 고치지 않습니다. 그래서 write-through/outbox가 끝내 실패하면 Redis에 남은 오래된 정책값이 무기한 유지될 수 있습니다.
- [TrafficHydrateRefillAdapterService.java]와 [TrafficHydrateRefillAdapterService.java]에서 hydrate 재시도 소진 시 ERROR로 승격하지 않고 GLOBAL_POLICY_HYDRATE/HYDRATE를 그대로 반환합니다. 그런데 오케스트레이터는 [TrafficDeductOrchestratorService.java]에서 ERROR만 FAILED로 보기 때문에, 실제로는 캐시 복구 실패인 요청이 PARTIAL_SUCCESS로 기록됩니다.
- 개선
피드백
1. 두 이슈의 관계 및 통합 분석
두 사안은 모두 "성공적인 리필 이후의 부가 단계에서 발생하는 장애 처리 미흡"으로 인해 데이터 정합성이 깨지는 동일한 성격의 문제입니다.
| 구분 |
이슈 A (중복 차감) |
이슈 B (중복 충전) |
| 발생 지점 |
요청-응답 스레드 (Adapter) |
백그라운드 스케줄러 (Outbox) |
| 장애 시점 |
리필 성공 후 실제 잔량 차감 시 |
리필 성공 후 멱등성 키 제거 시 |
| 잘못된 복구 |
Redis 실패 시 DB Fallback 차감 실행 |
멱등키 제거 실패 시 DB 잔량 반납(Restore) |
| 결과 |
Double-Decrement (DB 잔량 2회 삭감) |
Double-Credit (DB 잔량 복구 + Redis 충전됨) |
| 해결 핵심 |
리필 후 재시도 시 DB Fallback 금지 |
멱등키 제거 실패 시 무시 및 성공 유지 |
2. 제안된 수정 계획 검토 결과 (RedisOutboxRetryScheduler)
제안하신 수정 계획을 코드로 검증한 결과, 데이터 정합성을 위해 반드시 필요한 조치임을 확인했습니다.
- 코드 상의 문제 확인:
RedisOutboxRetryScheduler.java의 88~91라인에서 markSuccess를 먼저 호출하더라도, 91라인의 clearIdempotency에서 예외가 발생하면 66라인의 catch 블록으로 넘어가 상태가 다시 FAIL로 역행(markFailWithRetryIncrement)하게 됩니다.
- 보상 로직의 부작용: 상태가
FAIL로 유지되다가 maxRetryCount에 도달하면 compensateRefillOnce가 호출되는데, 이때 DB 잔량이 복구됩니다. 하지만 Redis에는 이미 리필이 반영된 상태이므로 사용자에게 중복으로 잔량이 지급되는 정합성 오류가 발생합니다.
- 수정의 안전성: 멱등성 키(
idempotencyKey)는 600초 후 자동 만료되도록 설계되어 있으므로(TTL), 제거(clear) 작업이 실패하더라도 로그만 남기고 성공 처리하는 것이 시스템 전체 정합성 측면에서 훨씬 안전합니다.
3. 최종 종합 의견
검토하신 두 가지 수정 사항은 서로 보완적이며 트래픽 서비스의 데이터 신뢰도를 높이는 데 필수적입니다.
- 실시간 경로(Issue A): 리필 후 Redis 장애 시 DB Fallback을 막아 DB 잔량의 과도한 소멸을 방지합니다.
- 배치 경로(Issue B): 부차적인 스크립트 실패 시 DB 환불을 막아 DB 잔량의 부당한 생성을 방지합니다.