시작하며
특정 시간에 주기적으로 해야 하는 일이 있습니다. 뉴스레터와 같은 이메일 발송, 주기적으로 데이터베이스 동기화, 이용시간이 적은 시간에 대량 로그 전송 등등의 일이 그 예시가 될 것입니다.
스프링 부트에서 이런 작업을 @Schedule
어노테이션을 사용하면 쉽게 할 수 있습니다. 본 글에서 @Schedule
활용을 살펴보겠습니다.
의존성 추가
@Schedule
은 org.springframework.scheduling 패키지에 있습니다. 다음과 같이 의존성을 추가하면 됩니다.
Gradle이라면 build.gradle에 다음과 같이 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-web'
메이븐이라면 pom.xml에 다음과같이 추가하면 됩니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
스케줄링 활성화
스케줄링 기능을 활성화하기 위해서는 configuration 클래스 혹은 지금처럼 애플리케이션 매인 클래스에 @EnableScheduling
를 추가합니다.
@EnableScheduling // 추가!!
@SpringBootApplication
public class ScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class, args);
}
}
이로써 스프링 부트에서 스케줄을 사용하기 위한 모든 준비를 마쳤습니다.
작업 메서드를 구현할 클래스
@Slf4j // 로그를 위해서
@Component // 빈 등록
public class ScheduledTasks {
private static final DateTimeFormatter formatter
= DateTimeFormatter.ofPattern("mm:ss:SSS");
}
주기적으로 실행할 작업을 구현하기 위한 클래스를 위와 같이 구성하였습니다. 스케줄링할 작업 메서드는 두 가지 특징을 갖습니다.
- 메서드의 리턴값은 void여야 한다. (그러나 리턴값이 있더라도 동작합니다.)
- 메서드의 인자는 없어야 한다. (인자가 있으면 컴파일시 BeanCreationException 발생)
Fixed Rate
고정된 간격으로 실행하는 작업을 하나 만들어보겠습니다.
@Scheduled(fixedRate = 3000)
public void fixedRate() {
log.info("fixedRate: 현재시간 - {}", formatter.format(LocalDateTime.now()));
}
fixedRate의 값은 ms 단위입니다. 3000ms = 3초, 즉 매 3초 주기로 작업을 실행합니다.
현재시간 - 00:03:000
현재시간 - 00:06:000
현재시간 - 00:09:000
Fixed Delay
fixedRate와 마찬가지로 fixedDelay는 일정 간격으로 실행됩니다. 그러나 fixedDelay는 이전 작업 완료 이후 시간부터 측정됩니다.
@Scheduled(fixedDelay = 1000)
public void fixedDelay() throws InterruptedException {
log.info("시작시간 - {}", formatter.format(LocalDateTime.now()));
TimeUnit.SECONDS.sleep(5);
log.info("종료시간 - {}", formatter.format(LocalDateTime.now()));
}
TimeUnit을 사용하여 5초 테스크를 수행하는데 5초가 걸리게 하였습니다.
시작시간 - 00:29:000
종료시간 - 00:34:000
시작시간 - 00:35:000
종료시간 - 00:40:000
시작시간 - 00:41:000
종료시간 - 00:46:000
시작시간 - 00:47:000
종료시간 - 00:52:000
fixedDelay는 작업 종료 후 1초 뒤 주기적으로 실행되는 것을 확인할 수 있습니다. 작업 수행 시작시점부터 delay 후에 재시작하는 fixedRate와 차이가 있습니다.
Initial Delay
첫 작업이 실행되기 전까지 대기할 시간을 지정할 수 있습니다.
initialDelay는 fixedRate, fixedDelay 모두 사용할 수 있습니다.
@Scheduled(fixedRate = 3000, initialDelay = 5000)
public void fixedRateAndInitialDelay() {
log.info("현재시간 - {}", formatter.format(LocalDateTime.now()));
}
// 서버는 00:25:000 시작
현재시간 - 00:30:000
현재시간 - 00:33:000
현재시간 - 00:36:000
00:25:000에 서버가 시작되었다면 5초 후 첫 작업이 시작하여 3초 간격으로 실행됩니다.
크론 표현식
그러나 현실은 ms로 지정하는 fixedRate, fixedDelay보다 더 구체적인 조건을 주어야 합니다. 매일 아침 8시 뉴스레터를 보내기, 매일 새벽 03시 00분 로그전송 등등일 것입니다. 이럴 때는 크론 표현식이 유용합니다.
다만 crontab.guru를 볼 때 조심해야 할 점은 맨 앞글자인 초 단위가 없는 5글자이기에 실제 스프링부트에서 크론식에 작성할때는 앞에 초 글자까지 포함해야합니다. 5글자만 넣으면 BeanCreationException이 발생합니다.
* * * * * *
초(0-59) 분(0-59) 시간(0-23) 일(1-31) 월(1-12) 요일(0-7)
- 총 6글자 (초/분/시간/일/월/요일)
- 요일의 경우 숫자 뿐만 아니라 MON 문자 표현 가능합니다.
@Scheduled(cron = "0 0 7 * * ?")
public void cronExpression() {
log.info("현재시간 - {}", formatter.format(LocalDateTime.now()));
}
0 0 7 * * ?
크론식의 의미대로 매일 아침 7시 로그가 찍힙니다.
쓰레드 풀 설정
기본적으로 @EnableScheduling
어노테이션을 사용 시 작업을 실행할 스케줄링을 위해서 스레드가 하나만 있는 스레드 풀을 만듭니다. @Scheduled 작업은 대기열에 쌓이게 되며 단일 스레드에 의해서만 실행됩니다.
여러 작업을 제시간에 맞추어서 실행하기 위해서는 스레드를 늘릴 필요가 있을 것입니다. SchedulingConfigurer 인터페이스를 구현하여 설정을 변경할 수 있습니다.
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Value("${thread.pool.size}")
private int POOL_SIZE;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(POOL_SIZE);
scheduler.setThreadNamePrefix("현재 쓰레드-");
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
setThreadNamePrefix
를 이용하여 로그로 남길시 현재 실행하는 쓰레드 명 Prefix를 붙일 수 있습니다.
thread.pool.size는 application.yml 파일을 통하여 3개로 늘려줍니다.
thread:
pool:
size: 3
[ 현재 쓰레드-2] com.covenant.schedule.ScheduledTasks : 현재시간 - 50:12:760
[ 현재 쓰레드-3] com.covenant.schedule.ScheduledTasks : 현재시간 - 50:13:763
[ 현재 쓰레드-1] com.covenant.schedule.ScheduledTasks : 현재시간 - 50:13:763
[ 현재 쓰레드-1] com.covenant.schedule.ScheduledTasks : 현재시간 - 50:13:766
setThreadNamePrefix 지정한 이름으로 현재 쓰레드라는 쓰레드 명이 추가되었으며 쓰레드 3개로 작업을 수행하는 것을 확인할 수 있습니다.
마치며
본 글에서 사용한 플레이그라운드 코드는 Github. Tistory-Covenant-Code에서 확인할 수 있습니다.
본 글 작성을 위해서 참고한 글 링크를 남깁니다.
'Computer Science > Java-Spring' 카테고리의 다른 글
완벽정리! Junit5로 예외 테스트하는 방법 (3) | 2021.11.12 |
---|---|
완벽정리! LocalDateTime을 살펴보자 (7) | 2021.11.08 |
Mybatis의 where 1=1 사용 대신 trim으로 해결해보자 (8) | 2021.11.06 |
Spring Cloud Gateway를 이용하여 게이트웨이 구성 및 유레카 서버 연동 (3) | 2021.10.18 |
넷플릭스 유레카를 이용한 서비스 디스커버리, 등록 구현(with 스프링부트, GO, 플라스크) (0) | 2021.10.11 |