회사 서비스에는 외부 API와 연동되어 데이터를 동기화하는 작업이 있고, 이 작업은 스케줄러로 처리되고 있다.
그런데 문득, "이 스케줄러가 실패하면 디버깅이 어렵고, 우리가 그걸 인지하지 못하면 어떻게 하지?"라는 생각이 들었다.
사실 이 문제는 이미 가끔 발생 중이었고,
그래서 “이슈가 터지기 전에 알 수 있도록 경고 시스템을 넣자”는 결론에 도달했다.
가장 먼저 떠오른 해결책은 Slack 알림이었다.
회사 동료들이 작성한 레퍼런스를 참고해 Slack API를 기반으로 Webhook 연동을 구현했다.
구성한 내용은 아래와 같다
slack:
webhook:
url: "https://hooks.slack.com/services/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"
@Service
class SlackNotifier(
@Value("\${slack.webhook.url}")
private val webhookUrl: String,
) {
fun sendMessage(message: String) {
restTemplate.postForEntity(webhookUrl, requestEntity, String::class.java)
....
}
}
Slack 알림 외에도, 실패 케이스를 다각도로 대응하기 위해 여러 방안을 정리해보았다
이 중 내가 택한 방법은 실패 로그 저장 + 슬랙 알림이었다.
선택 이유는 다음과 같다
SchedulerFailedLog
로깅 관련 엔티티 추가 @Entity
@Table(name = "scheduler_failed_log")
@SQLDelete(sql = "UPDATE routing SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
class SchedulerFailedLog() : AutoIncrementIdEntity() {
constructor(
exceptionSource: String,
exceptionMessage: String,
requestedUrl: String? = null,
) : this() {
this.exceptionSource = exceptionSource
this.exceptionMessage = exceptionMessage
this.requestedUrl = requestedUrl
}
@Comment("예외 발생 이름")
@Column(name = "exception_source", nullable = false)
private var exceptionSource: String = ""
@Comment("예외 메시지")
@Column(name = "exception_message", nullable = false)
private var exceptionMessage: String = ""
@Comment("외부 요청 url")
@Column(name = "requested_url", nullable = true)
private var requestedUrl: String? = ""
}
SchedulerFailedHandler
를 구현했다@Component
class SchedulerFailedHandler(
private val schedulerFailedLogRepository: SchedulerFailedLogRepository,
private val schedulerFailedNotifier: SchedulerFailedNotifier,
) {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun httpFailed(failedLog: SchedulerFailedLog) {
log.info("동기화 스케줄러 실패 : {}", failedLog.getExceptionMessage())
schedulerFailedLogRepository.save(failedLog)
schedulerFailedNotifier.alert(failedLog.getExceptionMessage())
}
}
결과적으로, 슬랙 알림과 함께 실패 로그가 잘 쌓이도록 구성되었고,
향후 이 로그를 기반으로 모니터링 지표나 리포트 형태로 확장할 계획이다.
이메일 발송 실패는 스케줄러 실패보다 더 문제였다.
이유는 명확하다
“스케줄러는 내부의 실패지만, 이메일은 사용자와의 접점이다.”
현재 이메일은 다음과 같은 방식으로 처리되고 있었다:
비즈니스 로직 (예: 임시 비밀번호 발급)이 완료된 후
Spring Event
+ @Async
조합으로 이메일 발송
즉, 트랜잭션이 커밋된 후 비동기적으로 발송되는 구조였고,
이 구조에선 실패 시 대응이 어렵다.
검토한 방법들
Fallback
패턴 적용 @HystrixCommand(fallbackMethod = "sendEmailFallback")
이유는 다음과 같다
"비즈니스 로직은 성공했지만, 이메일 발송이 실패했다고 그걸 되돌리는 건 신중해야 한다."
다만, 발송 실패를 원자적 단위로 본다면, 오히려 비동기가 아닌 동일 트랜잭션에서 처리하는 게 맞지 않나? 라는 고민도 들었다.
이 부분은 추후 리팩토링에서 더 깊이 검토할 예정이다.
이번 작업은 단순히 실패를 잡는 것에서 끝나지 않았다.
오히려 이 과정을 통해 기존 아키텍처와 처리 방식이 적절했는지를 되돌아보게 해주는 계기가 되었다.
스케줄러와 이메일 발송처럼 실패 시점이 사용자 혹은 외부 시스템과 연결되는 경우엔,
단순한 로그 이상의 대응이 필요하다.
이번 개선으로 '문제가 발생하면 알게 된다' → '문제가 생기기 전에 인지할 수 있다'로 한 단계 나아갔다고 생각한다.
다음 리팩토링에선 모니터링, 시각화, 재시도 큐 구조 등을 포함해 더 개선해볼 예정이다.