-
Notifications
You must be signed in to change notification settings - Fork 0
Labels
Description
🔗 관련 이슈
💡 질문 / 논의 내용
기존 Outbox 패턴 구현에서는 스케줄러가 Outbox 테이블의 미처리 이벤트를 조회하여 Discord/Email 알림을 전송하는 구조였으나, 실질적인 재시도가 이루어지지 않는 문제가 있었다.
📝 추가 설명(선택)
1. 무한 재시도 문제
- 재시도 횟수 제한이 없어 영구적으로 실패하는 이벤트가 무한 반복됨
- 외부 서비스(Discord/Email)가 영구 장애 시, 시스템 리소스 낭비
Scheduler (10초마다)
└─ OutboxEventProcessorModule.processEvent()
└─ notificationPort.sendProjectNotification()
└─ NotificationAdapter.sendProjectXxxNotification()
├─ discordWebhookModule.send() ← try-catch로 삼킴 ✅/❌
└─ emailNotificationModule.send() ← try-catch로 삼킴 ✅/❌- NotificationAdapter (핵심 문제)
- Discord/Email 예외를 내부에서 catch하고 log만 남기기에, 예외가 위로 전파되지 않는 문제 존재
- OutboxEventProcessorModule
- 예외가 오면 markAsProcessed() 않고 throw
- 예외를 받아야 재시도 가능한데 못 받기에 재시도를 하지 않는다.
즉, NotificationAdapter가 예외를 삼켜버려서 Processor는 성공으로 인식 → markAsProcessed() 호출 → processed = true 로 저장된다.
알림이 실패했어도 다시 시도되지 않는 구조
2. 재시도 추적 부재
OutboxEventEntity에 재시도 횟수(retryCount) 필드가 없음- 몇 번 재시도했는지 알 수 없어 모니터링 및 디버깅 어려움
- 이는 이메일 주소가 잘못되거나, 특정 문제로 인해 영원히 해결되지 않더라도, 이를 계속해서 재시도 할 수 있습니다.
3. 고정된 재시도 간격
- 현재 10초마다 고정 간격으로 재시도
- 지수 백오프(exponential backoff) 전략 미적용
- 일시적 장애 시 불필요한 재시도로 외부 서비스 부하 증가
📊 현재 코드 분석
OutboxEventProcessorModule (현재)
/**
* 프로젝트 요청 생성 이벤트 처리
*/
private void handleProjectRequestCreated(OutboxEventEntity event) throws Exception {
ProjectRequestEvent projectEvent = objectMapper.readValue(
event.getPayload(),
ProjectRequestEvent.class
);
notificationPort.sendProjectNotification(new ProjectRequestCreatedNotification(
projectEvent.projectRequestId(),
projectEvent.projectName(),
projectEvent.projectDescription(),
projectEvent.requesterId(),
projectEvent.email()
));
event.markAsProcessed();
outboxMessageRepositoryPort.save(event);
}NotificationAdapter (현재)
/**
* 프로젝트 요청 생성 알림
*/
private void sendProjectRequestCreatedNotification(ProjectRequestCreatedNotification notification) {
// Discord 알림 전송
try {
discordWebhookModule.sendProjectRequestCreatedNotification(
notification.requesterName(),
notification.requesterEmail(),
notification.projectName(),
notification.projectRequestId(),
notification.projectDescription()
);
log.info("Discord notification sent for PROJECT_REQUEST_CREATED: projectRequestId={}", notification.projectRequestId());
} catch (Exception e) {
log.error("Failed to send Discord notification for PROJECT_REQUEST_CREATED: projectRequestId={}", notification.projectRequestId(), e);
}
// Email 알림 전송
try {
emailNotificationModule.sendProjectRequestCreatedNotification(
notification.requesterName(),
notification.requesterEmail(),
notification.projectName(),
notification.projectRequestId(),
notification.projectDescription()
);
log.info("Email notification sent for PROJECT_REQUEST_CREATED: projectRequestId={}", notification.projectRequestId());
} catch (Exception e) {
log.error("Failed to send Email notification for PROJECT_REQUEST_CREATED: projectRequestId={}", notification.projectRequestId(), e);
}
}OutboxEventEntity (현재)
@Entity
@Table(name = "outbox_events")
public class OutboxEventEntity {
@Id @GeneratedValue(strategy = GenerationType.UUID)
private String eventId;
private Boolean processed = false;
private LocalDateTime createdAt;
private LocalDateTime processedAt;
// ❌ 재시도 관련 필드 없음
// - retryCount
// - maxRetryCount
// - lastErrorMessage
// - nextRetryAt
}OutboxEventSchedulerModule (현재)
@Scheduled(fixedDelay = 10000) // 고정 10초
@SchedulerLock(name = "OutboxEventProcessor_processUnprocessedEvents")
public void processUnprocessedEvents() {
List<OutboxEventEntity> unprocessedEvents =
outboxMessageRepositoryPort.findByProcessedFalse();
for (OutboxEventEntity event : unprocessedEvents) {
try {
outboxEventProcessorModule.processEvent(event);
} catch (Exception e) {
// ❌ 재시도 횟수 증가 없음
// ❌ 에러 정보 저장 없음
// ❌ 최대 재시도 체크 없음
log.error("Failed to process outbox event", e);
}
}
}📚 참고 자료(선택)
Reactions are currently unavailable