스케줄러 시스템 기술적 의사결정 - fitpassTeam/fitpass GitHub Wiki
스케줄러 시스템 기술적 의사결정
문제 : 시간 기반 비즈니스 로직의 수동 관리 한계
스케줄러 도입 배경
- 수동 상태 관리의 한계: 예약 완료, 멤버십 만료, 목표 기간 종료 등을 관리자가 직접 처리해야 하는 비효율성
- 사용자 경험 저하: 만료 알림 부재로 갑작스러운 서비스 중단 경험
- 데이터 일관성 문제: 시간이 지났음에도 상태가 업데이트되지 않는 레거시 데이터 누적
- 운영 비용 증가: 반복적인 수동 작업으로 인한 인력 낭비
의사결정 배경 및 요구사항
비즈니스 요구사항
- 자동화: 시간 기반 상태 전이를 시스템이 자동 처리
- 사용자 경험 향상: 단계적 알림으로 갑작스러운 서비스 중단 방지
- 운영 효율성: 수동 관리 업무 최소화
기술적 요구사항
- 실시간성: 중요한 상태 변경은 즉시 반영
- 안정성: 배치 작업 실패가 메인 서비스에 영향을 주지 않아야 함
- 확장성: 새로운 도메인의 자동화 요구사항을 쉽게 추가할 수 있어야 함
Spring Scheduler + 실시간
// 배치 처리 + 실시간 처리 조합
@Scheduled(cron = "0 0 0 * * *")// 백그라운드 일괄 처리
+
goal.checkAndUpdateExpiredStatus();// 조회 시 실시간 체크
장점: 성능 + 정확성 균형
단점: 구현 복잡도 약간 증가
선택한 이유 :
"배치 처리로 효율성을 확보하고, 실시간 체크로 정확성을 보장하는 이중 안전장치”
시스템 플로우
핵심 구현 설계 결정사항
- 도메인별 스케줄러 분리
// 도메인별 책임 분리로 유지보수성 향상
ReservationScheduler → 예약 관련 자동화
MembershipScheduler → 멤버십 관련 자동화
FitnessGoalScheduler → 목표 관련 자동화
의사결정 이유: 단일 책임 원칙, 코드 가독성, 확장 용이성
- 점진적 정보 수집 전략
// 시스템 부하 분산을 위한 시간대별 분리
00:00 (자정) → 목표 만료 처리 (하루 단위 정리)
01:00 (새벽) → 장기 대기 예약 취소 (정리 작업)
06:00 (새벽) → 멤버십 자동 활성화 (사용 전 준비)
09:00 (오전) → 만료 알림 발송 (사용자 활동 시간)
매시간 00분 → 예약 상태 변경 + 2시간 전 알림
의사결정 이유:
- 서버 리소스 집중 방지
- 사용자 알림 최적 시간대 고려
- 비즈니스 로직에 맞는 처리 순서
- 하이브리드 상태 관리 패턴
// 실시간 + 배치 처리 조합
@Entity
public class FitnessGoal {
// 실시간 체크 (조회 시마다)
public void checkAndUpdateExpiredStatus() {
if (goalStatus == GoalStatus.ACTIVE && LocalDate.now().isAfter(endDate)) {
this.goalStatus = GoalStatus.EXPIRED;
}
}
}
@Scheduled(cron = "0 0 0 * * *") // 배치 처리 (백그라운드)
public void updateExpiredGoals() {
List<FitnessGoal> activeGoals = repository.findByGoalStatus(ACTIVE);
activeGoals.forEach(FitnessGoal::checkAndUpdateExpiredStatus);
}
의사결정 이유: 즉시성, 효율성, 안정성
- 사용자 경험 최적화 알림 전략
// 단계적 알림으로 사용자 이탈 방지
멤버십 만료: 3일 전 → 1일 전 → 당일
예약 알림: 하루 전 → 2시간 전
목표 처리: 자정 일괄 (사용자 수면 시간)
의사결정 이유: 사용자 준비 시간 확보, 이탈률 감소
구현 성과 및 효과
자동화된 비즈니스 프로세스
// Before: 수동 관리
- 관리자가 직접 상태 변경
- 사용자 문의 증가
- 데이터 불일치 가능성
// After: 자동화
- 무인 상태 관리
- 사용자 만족도 향상
- 데이터 일관성 보장
실제 자동화 현황
도메인 | 작업 내용 | 실행 주기 | 기대 효과 |
---|---|---|---|
예약 | 만료된 예약 → 자동 완료 처리 | 매시간 | 100% 자동 상태 관리 |
예약 | 24시간 대기 예약 → 자동 취소 | 매일 새벽 1시 | 장기 대기 해소 |
예약 | 예약 2시간 전 알림 발송 | 매시간 | No-show 30% 감소 |
멤버십 | 3단계 만료 알림 전송 | 매일 오전 9시 | 갱신률 25% 향상 |
멤버십 | 기간 만료 후 자동 비활성화 | 매일 오전 6시 | 사용자 편의성 증대 |
목표 | 만료된 목표 자동 정리 | 매일 자정 | 데이터 일관성 확보 |
안정성 보장 설계
1. 실패 격리
// 각 스케줄러는 독립적으로 동작
ReservationScheduler 실패 → MembershipScheduler 정상 동작
예약 스케줄러가 무너져도 다른 스케줄러는 살아있기에 독립적으로 동작한다.
2. 멱등성 보장
// 같은 작업을 여러 번 실행해도 결과 동일
if (goalStatus == GoalStatus.ACTIVE && LocalDate.now().isAfter(endDate)) {
this.goalStatus = GoalStatus.EXPIRED; // 이미 EXPIRED면 변경 없음
}
3. 상세 로깅 및 모니터링
log.info("만료된 예약 {}개를 COMPLETED로 변경합니다.", expiredReservations.size());
log.info("사용자 ID: {}, 멤버십: {} 자동 활성화 완료", userId, membershipName);