diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/dto/SlackMessageDto.java b/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/dto/SlackMessageDto.java index 84cf3ed6..38a9824d 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/dto/SlackMessageDto.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/alarm/dto/SlackMessageDto.java @@ -18,8 +18,8 @@ public enum MessageType { DM // 사용자 개인 DM } - private MessageType type; // 메시지 타입 구분 - private String message; // 보낼 메시지 내용 + private MessageType type; + private String message; // Webhook용 필드 private String webhookUrl; @@ -27,4 +27,11 @@ public enum MessageType { // DM용 필드 private String username; private String email; + + @Builder.Default + private int retryCount = 0; + + public void incrementRetryCount() { + this.retryCount++; + } } \ No newline at end of file diff --git a/src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/SlackNotificationWorker.java b/src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/SlackNotificationWorker.java index 63112a40..ed5e4582 100644 --- a/src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/SlackNotificationWorker.java +++ b/src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/SlackNotificationWorker.java @@ -13,6 +13,7 @@ /** * Consumer / Worker * Redis 큐에 쌓인 알림 요청을 하나씩 꺼내서 실제로 처리하는 Consumer입니다. + * 전송 실패 시 최대 MAX_RETRY_COUNT회 재시도합니다. */ @Component @RequiredArgsConstructor @@ -24,15 +25,22 @@ public class SlackNotificationWorker { private final ObjectMapper objectMapper; private static final String SLACK_QUEUE_KEY = "slack:notification:queue"; + private static final int MAX_RETRY_COUNT = 3; @Scheduled(fixedDelay = 1000) public void processSlackQueue() { - try { - Object messageObj = redisTemplate.opsForList().leftPop(SLACK_QUEUE_KEY); - if (messageObj == null) return; + Object messageObj = redisTemplate.opsForList().leftPop(SLACK_QUEUE_KEY); + if (messageObj == null) return; - SlackMessageDto dto = objectMapper.convertValue(messageObj, SlackMessageDto.class); + SlackMessageDto dto; + try { + dto = objectMapper.convertValue(messageObj, SlackMessageDto.class); + } catch (Exception e) { + log.error("Slack 큐 메시지 역직렬화 실패 (폐기): {}", e.getMessage()); + return; + } + try { if (dto.getType() == SlackMessageDto.MessageType.WEBHOOK) { slackApiService.sendWebhook(dto.getWebhookUrl(), dto.getMessage()); log.info("Slack Webhook 전송 성공 (Queue)"); @@ -43,10 +51,23 @@ public void processSlackQueue() { } } catch (BusinessException e) { - log.warn("Slack 알림 처리 실패 (Business): {}", e.getMessage()); + // 비즈니스 예외(유저 없음 등)는 재시도해도 동일하게 실패하므로 바로 폐기 + log.warn("Slack 알림 처리 실패 (Business, 폐기): {}", e.getMessage()); } catch (Exception e) { - log.error("Slack 큐 처리 중 시스템 오류 (재시도 필요 시 큐 복귀 고려)", e); + // 일시적 장애(네트워크, Slack API 다운 등)는 재시도 + requeue(dto, e); + } + } + + private void requeue(SlackMessageDto dto, Exception cause) { + if (dto.getRetryCount() < MAX_RETRY_COUNT) { + dto.incrementRetryCount(); + redisTemplate.opsForList().rightPush(SLACK_QUEUE_KEY, dto); + log.warn("Slack 메시지 재시도 예약 ({}/{}회): {}", dto.getRetryCount(), MAX_RETRY_COUNT, cause.getMessage()); + } else { + log.error("Slack 메시지 최대 재시도({}) 초과, 폐기: {} | 원인: {}", + MAX_RETRY_COUNT, dto.getMessage(), cause.getMessage()); } } -} \ No newline at end of file +}