@ImportResource({ "classpath:scheduler/context-scheduler.xml" }) 나 @Scheduled 어노테이션을 사용하지 않고 스케줄을 동적으로 등록하는 두가지 방법을 알아보겠습니다.
1. ThreadPoolTaskScheduler
꼭 아래 예시와 같이 사용할 필욘 없지만, Map에 scheduler를 저장해놓고 destroy 할 수 있습니다.
@Service
public class SchedulerServiceImpl implements SchedulerService {
private final Map<String, ThreadPoolTaskScheduler> schedulerMap = new ConcurrentHashMap<>();
public SchedulerServiceImpl() {
Set<String> typeList = ...;
for (String type: typeList) {
this.startScheduler(type);
}
}
/**
* 스케줄 시작
*
* @param type
*/
@Override
public void startScheduler(String type) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.initialize();
scheduler.schedule(() -> this.execute(type), new PeriodicTrigger(1, TimeUnit.SECONDS));
schedulerMap.put(type, scheduler);
}
/**
* 스케줄 종료
*
* @param type
*/
@Override
public void destroyScheduler(String type) {
ThreadPoolTaskScheduler scheduler = schedulerMap.get(type);
scheduler.shutdown();
schedulerMap.remove(type);
}
/**
* 스케줄
*
*/
private void execute(String type) {
// ...
}
2. SchedulingConfigurer
WAS가 여러 개일 때 WAS 마다 다른 스케줄을 실행시키려고 합니다.
기존에는 context-scheduler.xml를 이용하여 각 WAS마다 다른 xml파일로 스케줄을 실행했었는데,
- @ImportResource({ "classpath:scheduler/context-scheduler.xml" })
이 방법은 WAS 개수만큼 build 해야 하기 때문에 원빌드 후 배포 시 VM Option으로 서버 번호를 넘겨 그 번호에 해당하는 스케줄 Job만 실행되게 동적 스케줄을 구현해보려고 합니다.
- -jar -Dserver.number=0 {프로젝트명}.jar
처음에는 원하는 context-scheduler.xml 파일을 불러오려고 아래와 같이 구현했는데, bean이 컨테이너에 제대로 올라가지 않아서 실패하였습니다.
@Autowired
private ResourceLoader resourceLoader;
@PostConstruct
public void init() {
ApplicationContext context = new ClassPathXmlApplicationContext("scheduler/context-scheduler.xml");
for (String beanDefinitionNames : context.getBeanDefinitionNames()) {
System.out.println(beanDefinitionNames);
}
final XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(new GenericApplicationContext());
Resource resource = resourceLoader.getResource("classpath:scheduler/context-scheduler.xml");
if (resource.exists()) {
xmlBeanDefinitionReader.loadBeanDefinitions(resource);
}
}
@Scheduled 어노테이션을 사용하면 모든 WAS에서 스케줄이 실행됩니다.
그리고 @Scheduled 어노테이션은 Spring context startup시 한 번만 resolved와 초기화됩니다.
따라서 Spring에서 @Scheduled 어노테이션을 사용하면 runtime에 fixedDelay 또는 fixedRate, cron 값을 변경할 수 없습니다.
그래서 스케줄러를 동적으로 등록하는 다른 방법을 찾아보았습니다.
Spring의 SchedulingConfigurer를 사용하면 fixedDelay 또는 fixedRate, cron 값을 동적으로 설정할 수 있습니다.
SchedulingConfigurer 인터페이스를 구현하기에 앞서, 우선 실행할 Job을 일괄 등록할 수 있게 Job Service에서 상속받아 공통으로 사용할 인터페이스(DoExecuteJob.java)를 생성합니다.
public interface DoExecuteJob {
void execute();
}
실행할 Job은 DoExecuteJob.java를 상속받아 단일 메서드(execute())에 실행할 로직을 추가합니다.
단, 클래스명은 JobImpl로 끝나야 합니다.
@Service
public class AuditLogJobImpl implements DoExecuteJob {
// ...
@Override
public void execute() {
// 실행 내용
}
}
@Service
public class DestructionJobImpl implements DoExecuteJob {
// ...
@Override
public void execute() {
// 실행 내용
}
}
@Service
public class UserJobImpl implements DoExecuteJob {
// ...
@Override
public void execute() {
// 실행 내용
}
}
Job의 cron 표현식을 정의할 scheduler.properties를 생성합니다.
key 패턴은 {*jobImpl의 bean name}.{serverNumber}.cron.expression로 이루어지게 합니다.
# 스케줄 실행 cron 정의
# *JobImpl.{serverNumber}.cron.expression={cron}
# 0번 서버에서 실행되는 Job
auditLogJobImpl.0.cron.expression=0 30 1 * * ?
# 1번 서버에서 실행되는 Job
destructionJobImpl.1.cron.expression=0 0 1 * * ?
userJobImpl.1.cron.expression=0 0 7 * * ?
@PropertySource(value = {
"classpath:properties/scheduler.properties"
}, encoding = "UTF-8")
@EnableTransactionManagement
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Exception {
// ...
}
}
DynamicSchedulingConfig.java를 생성하고 SchedulingConfigurer 인터페이스를 구현합니다.
@Slf4j
@Configuration
@EnableScheduling
@RequiredArgsConstructor
public class DynamicSchedulingConfig implements SchedulingConfigurer {
private final GenericWebApplicationContext context;
private final Environment env;
@Value("${server.number:0}")
private Integer serverNumber;
@Bean
public ScheduledExecutorService poolScheduler() {
return Executors.newScheduledThreadPool(50);
}
// @Bean
// public TaskScheduler poolScheduler() {
// ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
// threadPoolTaskScheduler.setPoolSize(50);
// threadPoolTaskScheduler.setThreadNamePrefix("thread-scheduler-task-");
// return threadPoolTaskScheduler;
// }
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
log.info("########################################################");
log.info("##### {}번 서버", serverNumber);
taskRegistrar.setScheduler(poolScheduler());
String[] beanDefinitionNames = context.getBeanDefinitionNames();
for (String beanName : beanDefinitionNames) {
if (beanName.endsWith("JobImpl")) {
try {
String cronExpr = env.getRequiredProperty(beanName + SEPERATOR_DOT + serverNumber + ".cron.expression");
DoExecuteJob scheduleJob = context.getBean(beanName, DoExecuteJob.class);
taskRegistrar.addTriggerTask(scheduleJob::execute, new CronTrigger(cronExpr));
log.info("## execute schedule job : {} - {}", beanName, cronExpr);
} catch (Exception e) {
log.info("## unexecuted schedule job : {}", beanName);
}
}
}
log.info("########################################################");
}
}
(1) JopImpl로 끝나는 bean 이름을 필터링합니다.
(2) properties파일에 정의해둔 cron 표현식을 조회하는데, 존재하지 않으면 실행하지 않는 Job으로 판단합니다.
(3) Job Service에서 상속받아 공통으로 사용 중인 인터페이스(DoExecuteJob.java)로 스케줄을 등록합니다.
-jar -Dserver.number=1 {프로젝트명}.jar 실행 결과 로그
########################################################
##### 1번 서버
## unexecuted schedule job : auditLogJobImpl
## execute schedule job : destructionJobImpl - 0 0 1 * * ?
## execute schedule job : userJobImpl -0 0 7 * * ?
########################################################
[Reference]
댓글