From 8766b905c6368c5f464d5839a7614eca19459de1 Mon Sep 17 00:00:00 2001 From: yoon6yo Date: Sun, 5 Jul 2026 21:42:24 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Slack=20=ED=81=90=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=A0=84=EC=86=A1=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=A9=94=EC=BB=A4=EB=8B=88?= =?UTF-8?q?=EC=A6=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [H-10] SlackNotificationWorker: 전송 실패 시 최대 3회 재시도 후 폐기 - 일시적 장애(네트워크, Slack API 다운): requeue → 재시도 - BusinessException(유저 없음 등 영구 실패): 즉시 폐기 - SlackMessageDto: retryCount 필드 추가 (Jackson 직렬화 호환) 역직렬화 실패 시 명시적 로그 후 폐기하여 큐 blocking 방지 Closes #249 Co-Authored-By: Claude Sonnet 4.6 --- .../domain/alarm/dto/SlackMessageDto.java | 11 ++++-- .../scheduler/SlackNotificationWorker.java | 35 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) 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 +}