포폴/일정관리앱
이메일 전송에 대한 후처리
slown
2025. 5. 17. 15:36
목차
1.문제점
2.해결 전략 – 후처리 아키텍처 도입
3.적용 구조 및 구현
4.후기
1.문제점
회원가입 시 인증 메일을 전송하거나, 각종 알림 메일을 발송하는 기능은 대부분의 서비스에서 필수입니다.
하지만 실제 운영 중 다음과 같은 문제가 발생했습니다:
- 메일 서버의 일시적인 장애
- SMTP 인증 실패
- 네트워크 지연 및 타임아웃
- `MessagingException` 등의 예외 발생
이런 예외가 발생했을 때, 시스템이 아무런 후처리를 하지 않는다면 중요한 이메일이 유실되고, 사용자 입장에서는 인증/알림을 받지 못해 혼란을 겪게 됩니다.
결국 이러한 구조는 시스템 신뢰도 저하와 사용자 불만으로 이어질 수 있습니다.
2.해결 전략 – 후처리 아키텍처 도입
이메일 실패를 무시하지 않고, 시스템이 자동으로 복구를 시도하고, 운영자가 추적할 수 있도록 저장하는 구조가 필요하다고 판단했습니다.
목표
- 실패 시 재시도
- 최종 실패 시 DB에 이력 저장
- 정기적으로 재시도 수행
- 성공 시 처리 완료 플래그 갱신
3.적용 구조 및 구현
구현하고자 하는 구조는 아래와 같습니다.
3-1. 메일 전송 + Retry 처리
@Retryable(
value = { MessagingException.class, RuntimeException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 2000)
)
public void sendHtmlEmail(String to, String subject, String htmlContent) throws MessagingException {
// 메일 전송 로직
}
3-2. 실패 후 DB 저장
@Recover
public void recover(MessagingException e, String to, String subject, String htmlContent) {
FailEmailModel fail = FailEmailModel.builder()
.toEmail(to)
.subject(subject)
.content(htmlContent)
.resolved(false)
.createdAt(LocalDateTime.now())
.build();
failEmailOutConnector.createFailEmail(fail);
}
3-3. 재처리 스케줄 처리.
@Scheduled(fixedDelay = 10_000)
public void retryFailedEmails() {
List<FailEmailModel> fails = failEmailOutConnector.findUnresolved();
for (FailEmailModel fail : fails) {
try {
emailService.sendHtmlEmail(fail.getToEmail(), fail.getSubject(), fail.getContent());
fail.markResolved();
} catch (Exception e) {
log.warn("재시도 실패 - id={}, reason={}", fail.getId(), e.getMessage());
}
}
failEmailOutConnector.saveAll(fails);
}
설계시 고려했던 포인트는 아래와 같습니다.
- resolved 플래그를 통해 무한 재시도 방지
- 후처리 로직은 @Recover로 분리 → 코드 복잡도 낮춤
- DB 이력 저장을 통해 장애 복구 후에도 재처리 가능
- OutConnector 패턴을 통해 DB 접근 로직을 모듈화
4.후기
단순히 "이메일을 보냈다" 수준에서 벗어나, 실패를 감지하고 추적/복구하는 아키텍처로 전환을 했고, Retry + 후처리 조합으로 운영 복원력(Resilience) 확보했습니다.
4-1.향후 계획
- 실패 이력을 관리자 페이지에서 시각화
- Slack 알림 연동
- Kafka 기반 메일 큐잉 구조 + Outbox 패턴으로 확장 예정입니다.