자바 백엔드 입문 55편. Phase 8 Integration 시작. @Scheduled 한 줄로 매분·매시간·매일 자동 실행되는 배치 작업을 출퇴근 자명종 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 55편이에요. 52~53편 테스팅 까지 다뤘으니, 이번 55편은 운영 백엔드의 반복 작업 — "매분·매시간·매일 자동 실행되는 백엔드 작업" 의 표준 패턴을 풀어 가요.
@Scheduled — 한 줄로 끝나는 스케줄링
매일 새벽 3시에 통계 집계, 매분 헬스체크, 매 30초 큐 폴링 같은 "반복 실행" 작업이 백엔드에 흔해요. @Scheduled 한 줄로 끝.
@Component
@RequiredArgsConstructor
public class ReportScheduler {
private final ReportService reportService;
@Scheduled(cron = "0 0 3 * * *") // 매일 새벽 3시
public void generateDailyReport() {
reportService.generate();
}
@Scheduled(fixedRate = 60000) // 60초마다
public void healthCheck() {
log.info("Health check at {}", LocalDateTime.now());
}
}
스케줄링 활성화는 @EnableScheduling 한 줄 — 보통 메인 클래스에.
@SpringBootApplication
@EnableScheduling
public class MyApp { ... }
Spring이 시작 시 @Scheduled 박힌 메서드를 찾아 등록 + 별도 스레드에서 주기적 실행.
3가지 스케줄링 방식
| 옵션 | 의미 | 사용 |
|---|---|---|
fixedRate |
직전 실행 시작 시점 기준 N ms 후 | 일정한 간격 (시간 정밀) |
fixedDelay |
직전 실행 종료 시점 기준 N ms 후 | 직전 작업 완료 후 안정 |
cron |
Cron 표현식 (특정 시각) | 매일·매주·매월 정해진 시각 |
@Scheduled(fixedRate = 60000) // 60초마다
@Scheduled(fixedDelay = 60000) // 직전 종료 + 60초
@Scheduled(cron = "0 0 3 * * *") // 매일 03:00
@Scheduled(cron = "0 0/30 9-18 * * MON-FRI") // 평일 9~18시, 30분마다
@Scheduled(cron = "0 0 1 1 * *") // 매월 1일 새벽 1시
가장 자주 만나는 = cron. Linux cron과 거의 같은 문법.
Cron 표현식 — 6자리
Spring @Scheduled cron은 6자리 (초·분·시·일·월·요일).
초 분 시 일 월 요일
0 0 3 * * * → 매일 03:00:00
0 */5 * * * * → 5분마다
0 0 9 * * MON → 매주 월요일 09:00
0 0 0 1 * * → 매월 1일 자정
특수 문자:
| 문자 | 의미 |
|---|---|
* |
모든 값 |
? |
지정 안 함 (일·요일 충돌 회피) |
/ |
단위 (0/5 = 0부터 5단위) |
- |
범위 (MON-FRI) |
, |
나열 (MON,WED,FRI) |
자동 생성기는 검색해서 "cron 표현식 생성기" 활용. 매번 손으로 짜는 거 어려워요.
fixedRate vs fixedDelay 차이
자주 헷갈리는 두 옵션.
fixedRate = 1000 (1초)
시점: 0 ──── 1 ──── 2 ──── 3 ──── 4
실행: [a] [b] [c] [d] [e]
(a가 1.5초 걸리면 b는 a 끝과 동시에 시작 — 누적 가능)
fixedDelay = 1000 (1초)
시점: 0 ──── 1.5 ──── 3 ──── 4.5
실행: [a] [b] [c] [d]
(a가 1.5초 걸려도 b는 a 끝난 후 1초 대기)
fixedRate= "시간을 정확히 맞추기" (조금 빠르거나 누적 가능)fixedDelay= "직전 작업이 끝나야 다음 시작" (안정)
길게 도는 작업은 fixedDelay 가 안전. 짧은 헬스체크 같은 건 fixedRate.
initialDelay — 시작 지연
서버 시동 직후 모든 스케줄러가 한꺼번에 도는 걸 막을 때.
@Scheduled(fixedRate = 60000, initialDelay = 30000)
public void monitor() { ... }
// 시작 30초 후 첫 실행, 그 후 60초마다
비동기 실행 — @Async
@Scheduled 는 기본적으로 단일 스레드에서 순차 실행. 여러 스케줄러가 동시에 도는 게 필요하면 @Async 조합.
@SpringBootApplication
@EnableScheduling
@EnableAsync // ← 비동기 활성화
public class MyApp { ... }
@Component
public class Tasks {
@Async
@Scheduled(fixedRate = 60000)
public void heavyTask() {
// 별도 스레드에서 비동기 실행
}
}
긴 작업이 다른 스케줄을 막지 않게.
분산 환경 함정 — 여러 서버에서 동시 실행
@Scheduled 가장 큰 함정 — 여러 인스턴스를 띄우면 각자 따로 실행. 새벽 3시 통계 집계가 — 서버 3대면 3번 동시 실행. DB 부담·중복 데이터·동시성 폭발.
해결법:
1. 한 인스턴스만 활성화 (간단)
@Scheduled(cron = "...")
@ConditionalOnProperty(name = "scheduler.enabled", havingValue = "true")
public void task() { ... }
여러 인스턴스 중 하나만 scheduler.enabled=true 박아 실행. 단순하지만 "그 인스턴스가 죽으면 스케줄 안 돔".
2. ShedLock — 분산 락
@SchedulerLock(name = "dailyReport", lockAtMostFor = "10m")
@Scheduled(cron = "0 0 3 * * *")
public void generateDailyReport() { ... }
ShedLock 라이브러리가 DB에 락을 박아 — 어느 한 인스턴스만 실행 보장. 한국 회사 시스템 표준 패턴.
3. 별도 배치 서버 분리
규모 큰 시스템은 — 웹 API 서버와 배치 서버를 분리. 배치 서버에만 @EnableScheduling. Spring Batch 같은 전용 도구도.
신규 프로젝트 초기 — 한 인스턴스라 @Scheduled 단순 사용 OK. 트래픽 증가로 인스턴스 늘릴 때 ShedLock 추가 가 표준. 초기에 너무 복잡하게 설계하지 말고, 필요 시점에 마이그레이션.
한 줄 정리 — @EnableScheduling + @Scheduled 한 줄로 매분·매시간·매일 자동 실행. cron 표현식(6자리)이 가장 자주. 여러 인스턴스 환경에서는 ShedLock으로 분산 락.
시험 직전 한 번 더 — @Scheduled 입문자가 매번 헷갈리는 것
@EnableScheduling= 메인 클래스에 박아 활성화@Scheduled= 메서드에 박아 주기 실행- 3가지 방식 =
fixedRate·fixedDelay·cron fixedRate= 실행 시작 시점 기준 (시간 정밀)fixedDelay= 종료 시점 기준 (안정)cron= Cron 표현식. 매일·매주·매월 정해진 시각- Cron = 6자리 (초·분·시·일·월·요일)
0 0 3 * * *= 매일 새벽 3시0 */5 * * * *= 5분마다0 0 9 * * MON-FRI= 평일 09시initialDelay= 시작 후 첫 실행 지연- 기본 = 단일 스레드 순차 실행
- 동시 실행 =
@EnableAsync+@Async조합 - 분산 환경 함정 = 인스턴스마다 따로 실행
- 해결 1 = 한 인스턴스만 활성화 (
@ConditionalOnProperty) - 해결 2 = ShedLock 분산 락 (한국 표준)
- 해결 3 = 별도 배치 서버 분리
- 큰 시스템 = Spring Batch 전용 도구
- 메서드는 반환 X, 매개변수 X (void 메서드만)
- 예외 발생 시 다음 스케줄에 영향 X (개별 실행)
- 로깅 + 알림 필수 — 실패 시 사일런트 위험
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 50편 — QueryDSL 타입 안전 동적 쿼리
- 51편 — 영속성 컨텍스트와 LazyLoading
- 52편 — @SpringBootTest 통합 테스트
- 53편 — MockMvc로 컨트롤러 테스트
- 54편 — Testcontainers 실제 DB 통합 테스트
다음 글: