본문 바로가기
Back-end/JAVA

[JAVA] Quartz 스케줄러 만들기 (4) - Step By Step

by 허도치 2020. 11. 11.
서론

  지난 포스트에서 Scheduler에 Listener를 적용했었는데, 로그를 출력하는것 외에는 활용하지 못했었다. 그러다가, Spring Batch의 Step처럼 하나의 작업이 끝나면 설정된 다음 작업으로 이어질 수 있도록 연결해보면 좋을것 같다는 생각이들었다.
  
  그래서, 이번 포스트에서는 JobListener를 이용하여 Job을 순차적으로 실행시켜 보도록하겠다.

 

 

 

개발환경

    - jdk-11.0.5
    - quartz-2.3.2

<!-- Scheduler -->
<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.3.2</version>
</dependency>
<!-- Scheduler -->
<!-- Logging -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.25</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>
<!-- Logging -->

 

 

 

소스코드
1. MainJob.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.dochi.quartz.step;
 
import java.util.Date;
 
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;
 
 
public class MainJob implements InterruptableJob {
    
    private Thread currentThread = null;
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String jobName = context.getJobDetail().getKey().getName();
        
        System.out.println(String.format("[%s][%s][%s] START", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
        
        // 현재 Thread 저장
        this.currentThread = Thread.currentThread();
        
        try {
            // 강제로 종료를 지연시키기
            for(int i=1; i<=5; i++) {
                System.out.println(String.format("[%s][%s][%s] 작업중...", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
                Thread.sleep(1*1000L);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(String.format("[%s][%s][%s] END", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
    }
 
    @Override
    public void interrupt() throws UnableToInterruptJobException {
        // interrupt 설정
        //   - 강제종료
        ifthis.currentThread != null ) {
            this.currentThread.interrupt();
        }
    }
}
 
cs

 

 

 

2. SubJob.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.dochi.quartz.step;
 
import java.util.Date;
 
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;
 
 
public class SubJob implements InterruptableJob {
    
    private Thread currentThread = null;
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String jobName = context.getJobDetail().getKey().getName();
        
        System.out.println(String.format("[%s][%s][%s] START", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
        
        // 현재 Thread 저장
        this.currentThread = Thread.currentThread();
        
        try {
            // 강제로 종료를 지연시키기
            for(int i=1; i<=5; i++) {
                System.out.println(String.format("[%s][%s][%s] 작업중...", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
                Thread.sleep(1*1000L);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(String.format("[%s][%s][%s] END", JobLauncher.TIMESTAMP_FMT.format(new Date()), this.getClass().getName(), jobName));
    }
 
    @Override
    public void interrupt() throws UnableToInterruptJobException {
        // interrupt 설정
        //   - 강제종료
        ifthis.currentThread != null ) {
            this.currentThread.interrupt();
        }
    }
}
 
cs

 

 

 

3. JobStepListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.dochi.quartz.step;
 
import java.util.Date;
import java.util.UUID;
 
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
 
 
public class JobStepListener implements JobListener {
    
    @Override
    public String getName() {
        return this.getClass().getName();
    }
 
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println(String.format("[%s][%s][jobToBeExecuted]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println(String.format("[%s][%s][jobExecutionVetoed]", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
 
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println(String.format("[%s][%s][jobWasExecuted] START", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
        
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        try {
            // JobDataMapp에 Next Step이 등록된 경우 스케줄 생성
            if( jobDataMap.containsKey(JobLauncher.NEXT_STEP_CLASS_NAME) ) {
                System.out.println("Add next step schedule...");
                addNextStepSchedule(context);
            }
        } catch (ClassNotFoundException | SchedulerException e) {
            e.printStackTrace();
        }
        
        System.out.println(String.format("[%s][%s][jobWasExecuted] END", JobLauncher.TIMESTAMP_FMT.format(new Date()), getName()));
    }
    
    // Next Step Schedule 등록 함수
    private void addNextStepSchedule(JobExecutionContext context) throws SchedulerException, ClassNotFoundException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        
        String mainStepJobName = context.getJobDetail().getKey().getName();
        String nextStepClassName = jobDataMap.getString(JobLauncher.NEXT_STEP_CLASS_NAME);
        String nextStepJobName = jobDataMap.getString(JobLauncher.NEXT_STEP_JOB_NAME);
        
        if( nextStepJobName == null ) {
            // 이름이 없는 경우
            //   - 중복 방지를 위해 UUID 생성
            nextStepJobName = UUID.randomUUID().toString();
        }
        
        // Next Step Class
        //   - 문자열로 Class 탐색
        Class<?> jobClass = Class.forName(nextStepClassName);
        
        // JobDetail 생성
        //   - NextStep에 MainStepName을 전달
        JobDetail jobDetail = JobBuilder.newJob((Class<extends Job>)jobClass)
                                .withIdentity(JobLauncher.PREFIX_NEXT_STEP_JOB_NAME+nextStepJobName)
                                .usingJobData(JobLauncher.MAIN_STEP_JOB_NAME, mainStepJobName)
                                .build();
        
        // Trigger 생성
        //   - 바로 실행
        Trigger trigger = TriggerBuilder.newTrigger()
                              .withIdentity(JobLauncher.PREFIX_NEXT_STEP_TRIGGER_NAME+nextStepJobName)
                              .startNow()
                              .forJob(jobDetail)
                              .build();
        
        // 스케줄 등록
        context.getScheduler().scheduleJob(jobDetail, trigger);
    }
}
 
cs

 

 

 

4. JobLauncher.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package com.dochi.quartz.step;
 
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Set;
 
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.UnableToInterruptJobException;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
 
public class JobLauncher {
    
    // 상수 설정
    //   - Prefix 설정
    public static final String PREFIX_STEP_JOB_NAME = "job_";
    public static final String PREFIX_STEP_TRIGGER_NAME = "trigger_";
    public static final String PREFIX_NEXT_STEP_JOB_NAME = "step_job_";
    public static final String PREFIX_NEXT_STEP_TRIGGER_NAME = "step_trigger_";
    
    //   - DateFormat 설정
    public static final SimpleDateFormat TIMESTAMP_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    public static final SimpleDateFormat DATETIME_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 
    //   - JobDataMap에서 사용할 Key 정의
    public static final String MAIN_STEP_JOB_NAME = "mainStepJobName";
    public static final String NEXT_STEP_CLASS_NAME = "nextStepClassName";
    public static final String NEXT_STEP_JOB_NAME = "nextStepJobName";
 
    // Scheduler 객체 생성
    private static SchedulerFactory factory = null;
    private static Scheduler scheduler = null;
    
    // Main 함수
    public static void main(String[] args) throws SchedulerException {
        // Scheduler 실행
        start();
        
        // Schedule 등록
        addSchedule("MainJob");
 
        try {
            System.out.println("아무키나 입력하면 종료됩니다...");
            System.in.read();
 
            // Scheduler 롱료
            stop();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // Scheduler 실행 함수
    public static void start() throws SchedulerException {
        // Scheduler 객체 정의
        factory = new StdSchedulerFactory();
        scheduler = factory.getScheduler();
        
        // Listener 설정
        scheduler.getListenerManager().addJobListener(new JobStepListener());
        
        // Scheduler 실행
        scheduler.start();
    }
    
    // Scheduler 종료 함수
    public static void stop() throws SchedulerException {
        try {
            System.out.println("스케줄러가 종료됩니다...");
            
            // Job Key 목록
            Set<JobKey> allJobKeys = scheduler.getJobKeys(GroupMatcher.anyGroup());
            
            // Job 강제 중단
            allJobKeys.forEach((jobKey)->{
                try {
                    scheduler.interrupt(jobKey);
                } catch (UnableToInterruptJobException e) {
                    e.printStackTrace();
                }
            });
            
            // Scheduler 중단
            //   - true : 모든 Job이  완료될 때까지 대기 후 종료
            //   - false: 즉시 종료
            scheduler.shutdown(true);
 
            System.out.println("스케줄러가 종료되었습니다.");
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
    
    // Schedule 등록 함수
    public static void addSchedule(String name) throws SchedulerException {
        // JobDataMap 설정
        //   - Step으로 실행시킬 Job Class 이름 설정
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put(NEXT_STEP_CLASS_NAME, "com.dochi.quartz.step.SubJob");
        
        // JobDetail 설정
        JobDetail jobDetail = JobBuilder.newJob(MainJob.class)
                                .withIdentity(PREFIX_STEP_JOB_NAME+name)
                                .setJobData(jobDataMap)
                                .build();
        
        // Trigger 설정
        Trigger trigger = TriggerBuilder.newTrigger()
                              .withIdentity(PREFIX_STEP_TRIGGER_NAME+name)
                              .startNow()
                              .forJob(jobDetail)
                              .build();
        
        // Schedule 등록
        scheduler.scheduleJob(jobDetail, trigger);
    }
}
 
cs

 

 

 

실행결과

    - MainJob이 완료된 후 SubJob이 수행되는 것을 확인할 수 있음.

 

 

 

마치며

    지금까지 JobListener를 통해 Step By Step으로 Job을 수행하는 방법에 대해서 알아보았다. 나중에 Crawler를 적용하였을 때, MainJob은 글목록에서 URL을 파싱하고, SubJob은 상세내용을 다시 크롤링하는 구조로 만들면 좋을것 같다.

 

댓글