= Quartz Clustering with JDBC-JobStore == JDBC-JobStore DB 초기화 quartz db schema (배포된 sdl-appendix 내 참조) 를 사용하는 DBMS에 맞는 SQL을 실행해 JDBC-JobStore를 사용하기 위한 Table을 생성한다. CAUTION: Tibero DB는 미지원 == application.properties properties 파일 내용 중 Quartz JDBC-JobStore 관련 설정이다. [source, properties] ---- # Spring Quartz 설정 spring.quartz.job-store-type=jdbc spring.quartz.jdbc.initialize-schema=always spring.quartz.datasource.driver-class-name=org.postgresql.Driver spring.quartz.datasource.jdbcUrl=jdbc:postgresql://10.40.87.189:5445/SDL spring.quartz.datasource.username=sdl5 spring.quartz.datasource.password=dlatl#123 spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate <1> spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000 spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=15000 spring.quartz.properties.org.quartz.scheduler.instanceName=sdl-prod <2> spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO <3> spring.quartz.properties.org.quartz.scheduler.rmi.export=false spring.quartz.properties.org.quartz.scheduler.rmi.proxy=false spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool spring.quartz.properties.org.quartz.threadPool.threadCount=10 <4> spring.quartz.properties.org.quartz.threadPool.threadPriority=5 spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true spring.quartz.properties.org.quartz.scheduler-name=QuartzScheduler ---- <1> DB 에 따라 Delegate 를 변경한다. + org.quartz.impl.jdbcjobstore.oracle.OracleDelegate, + org.quartz.impl.jdbcjobstore.MSSQLDelegate, + org.quartz.impl.jdbcjobstore.PostgreSQLDelegate <2> Quartz Scheduler 를 구별하는 Property 값으로 Clustering 할 서버에서는 같은 값을 세팅한다. <3> Unique 한 값이어야 하며 AUTO 일 경우 자동 생성된다. <4> Job 을 실행하기 위한 Thread 수 기타 자세한 Property 값은 http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/ConfigJDBCJobStoreClustering.html[Quartz 메뉴얼, window=_blank]을 참고 한다. == QuartzClusteringConfig QuartzClusteringConfig 파일은 Spring 에 등록된 CronTriggerFactoryBean 을 실행 하도록 SchedulerFactoryBean이 정의되어 있다. [source, java] ---- @Bean public SchedulerFactoryBean setSchedulerFactoryBean(ApplicationContext applicationContext) { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); AutoWiringSpringBeanJobFactory jobFactory = new AutoWiringSpringBeanJobFactory(); jobFactory.setApplicationContext(applicationContext); schedulerFactoryBean.setJobFactory(jobFactory); <1> schedulerFactoryBean.setDataSource(quartzDataSource()); <2> schedulerFactoryBean.setApplicationContext(applicationContext); schedulerFactoryBean.setTransactionManager(quartzTransactionManager()); <3> schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true); schedulerFactoryBean.setStartupDelay(60); schedulerFactoryBean.setOverwriteExistingJobs(true); Properties properties = new Properties(); properties.putAll(quartzProperties.getProperties()); <4> schedulerFactoryBean.setQuartzProperties(properties); schedulerFactoryBean.setTriggers( knoxSyncBatchConfig.knoxApprovalSyncTrigger().getObject(), userBatchConfig.batchUserLongTermCheckTrigger().getObject(), userBatchConfig.batchUserAuthExpiredTrigger().getObject(), sysUseLogBatchConfig.batchSysUseLogTrigger().getObject(), sysUseLogBatchConfig.batchMenuUseHistoryTrigger().getObject() ); <5> return schedulerFactoryBean; } @Bean @ConfigurationProperties(prefix = "spring.quartz.datasource") @QuartzDataSource <2> public DataSource quartzDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean @QuartzTransactionManager <3> public DataSourceTransactionManager quartzTransactionManager() { return new DataSourceTransactionManager(quartzDataSource()); } ---- <1> Spring Bean을 AutoWiring <2> Job 정보를 조회, 저회 저장할 때 사용하는 Datasource (resources-dev, resources-prod/application.properties 참조) <3> Quartz 전용 TransactionManager <4> Quartz 설정 파일 세팅 <5> SchedulerFactoryBean 에서 실행 할 Trigger == JobDetail 및 Trigger Bean 등록 아래는 KnoxSyncBatchConfig에 수행할 Job 클래스를 설정한 JobDetail Bean과 Trigger Bean을 등록한 예이다. .KnoxSyncBatchConfig.java [source, java] ---- @Value("${knox.approval.sync.cron}") private String knoxApprovalSyncCron; @Bean public JobDetail knoxApprovalSyncJobDetail() { return JobBuilder .newJob(KnoxSyncBatchExecutor.class) <1> .withIdentity("knoxApprovalSyncJob") .withDescription("Knox Approval Sync Batch") .storeDurably(true) .build(); } @Bean public CronTriggerFactoryBean knoxApprovalSyncTrigger() { CronTriggerFactoryBean approvalTrigger = new CronTriggerFactoryBean(); approvalTrigger.setJobDetail(knoxApprovalSyncJobDetail()); approvalTrigger.setCronExpression(knoxApprovalSyncCron); return approvalTrigger; } ---- <1> 배치잡 실행시 QuartzJobBean을 상속 받은 KnoxSyncBatchExecutor의 executeInternal 메소드가 실행된다. == Job 구현 QuartzJobBean은 Spring Bean을 DI Job의 구현체에서 사용할 수 있도록 Job을 구현한 추상 클래스이며, QuartzJobBean을 상속 받아 executeInternal 메소드를 오버라이드하여 실행할 배치 Service의 메소드를 호출한다. .KnoxSyncBatchExecutor.java [source,java] ---- @Component @Log4j2 public class KnoxSyncBatchExecutor extends QuartzJobBean { @Value("${node-id}") private String nodeId; @Autowired private KnoxApprovalSyncService knoxApprovalSyncService; @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { JobDetail jobDetail = jobExecutionContext.getJobDetail(); log.info(new StringBuilder() .append("nodeId : ").append(nodeId).append(", ") .append("jobName : ").append(jobDetail.getKey().getName()).append(", ") .append("description : ").append(jobDetail.getDescription()) ); knoxApprovalSyncService.synchronizeKnox(); } } ---- CAUTION: UserBatchExecutor의 경우 JobKey(JobBuilder.withIdentity 설정 값)를 이용해 분기 처리 하고 있으므로 Job 등록시 Name 명 지정에 주의하도록 한다. .UserBatchConfig.java [source, java] ---- /** * 장기 미사용자 관리 Job */ @Bean public JobDetail batchUserLongTermCheckJob() { return JobBuilder .newJob(UserBatchExecutor.class) .withIdentity("batchUserLongTermCheck") .withDescription("User LongTerm Check Batch") .storeDurably(true) .build(); } /** * 만료 권한 삭제 Job */ @Bean public JobDetail batchUserAuthExpiredJob() { return JobBuilder .newJob(UserBatchExecutor.class) .withIdentity("batchUserAuthExpired") .withDescription("User Auth Expired Batch") .storeDurably(true) .build(); } ---- .UserBatchExecutor.java [source, java] ---- @Override protected void executeInternal(JobExecutionContext jobExecutionContext) { JobDetail jobDetail = jobExecutionContext.getJobDetail(); String jobName = jobDetail.getKey().getName(); log.info(new StringBuilder() .append("nodeId : ").append(nodeId).append(", ") .append("jobName : ").append(jobName).append(", ") .append("description : ").append(jobDetail.getDescription()) ); switch (jobName) { case "batchUserLongTermCheck" -> userBatchService.execUserLongTermCheck(); case "batchUserAuthExpired" -> userBatchService.execUserAuthValidCheck(); case "batchUserAuthExpiredMailing" -> { try { userBatchService.execUserAuthValidAlarmMail(); } catch (IOException | MessagingException e) { log.error(e.getMessage(), e); } } default -> { } } } ----