티스토리 뷰

들어가며

일정 주기마다 DB를 체크해 에러가 나면 문자로 알려주는 모니터링 시스템을 구현하게 되었다.

이전에 @Scheduled + cron 표현식을 활용해 정해진 시간마다 데이터를 주고 받는 로직은 운영해본적이 있지만,

직접 구현은 처음이라 여러가지 방법을 모색해봤다.

1) 오라클 Job : 잡을 등록하고 DB에서는 주기마다 데이터를 처리하겠지만, 이상이 생기면 자바에서 이를 인지하고 메시지를 날릴 수 있을지가 의문이었다. 그래서 일단 패스!

2) Quartz(쿼츠) : 외부 인터넷은 접속이 안되고 xml에 추가하려면 꽤나 복잡한 프로젝트여서 이것도 논외!

3) ThreadPoolTaskScheduler (동적 스케줄링) : Bean만 활용하면 되서 어노테이션으로 충분히 커버가 가능하다고 생각했고, 찾다보니 생각보다 잘 정리된 자료들이 많아서 이걸로 결정했다.

 

ThreadPoolTaskScheduler Bean 설정

1) XML

1
<task:scheduler id="scheduler" pool-size="10" />

2) 어노테이션 (Annotation)

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ThreadPoolTaskSchedulerConfig {
 
    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(10);
        taskScheduler.setThreadNamePrefix("timeSchedule");
        taskScheduler.initialize();
        return taskScheduler;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter

@Configuration - Bean을 구성하는 클래스라고 IOC Container에게 알려줌. (Bean 설정)

@Bean - 해당 메소드를 Bean으로 등록 (name 지정 안하면 메소드명으로 자동 네이밍)

.setPoolSize() - Thread Pool Size 지정

.setThreadnamePrefix() - Thread 이름 앞에 붙일거

.initialize() - ExecutorService를 준비하겠다고 알림

Thread는 생성/소멸이 많아질 경우 성능 저하가 생길 수 있어서 Thread Pool을 통해 Thread를 관리

Thread Pool : 작업 Queue에 작업(Task)이 들어옴 -> 미리 생성해놓은 Thread를 작업마다 하나씩 내어줌

(미리 만들어놔서 노는 Thread가 생길 수 있고, 많이 만들어 놓으면 메모리만 차지할 수 있다)

 

Scheduler Service 구현
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
@Component
public class SchedulerService {
 
    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
 
    @Autowired
    private SqlSession sqlSession;
 
    private Map<String, ScheduledFuture<?>> scheduledMap = new ConcurrentHashMap<>();
 
    //스케줄 등록
    public void registerScheduler(ScheduleVO schedule) {
 
        ScheduledFuture<?> task = threadPoolTaskScheduler.schedule(
 
                () -> {
                    List<ScheduleVO> scheduleList = sqlSession.selectList("com.test.mapper.scheduleList");
 
 
                    System.out.println(new Date()+" Error Message :: "+ schedule.getMessage() + " Board Count ::" + boardCnt()
                            +" on thread "+Thread.currentThread().getName());
                }, periodicTrigger(Integer.parseInt(schedule.getTimeTerm()))
 
        );
    }
 
    //스케줄 정지
    public void stopScheduler(ScheduleVO scheduleVO) {
    }
 
    //작업 주기 Trigger
    private PeriodicTrigger periodicTrigger(int timeTerm) {
        return new PeriodicTrigger(timeTerm, TimeUnit.SECONDS);
    }
 
    
    private int boardCnt() {
        return sqlSession.selectOne("com.test.mapper.boardCnt");
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter

Bean으로 등록한 ThreadPoolTaskScheduler를 불러와 스케줄러를 관리하는 클래스를 만들었다.

> 등록 (registerScheduler)

ScheduledFuture<V>를 통해 ThreadPoolTaskScheduler의 Thread에 작업(Task)을 할당해준다.

이 작업들을 Map으로 관리하여 Key값으로 Thread를 정지할 수 있도록 구현하였다.

ThreadPoolTaskScheduler에는 여러 메소드들이 있지만 여기서는 schedule(Runnable task, Trigger trigger)를 사용하였는데, task에는 Runnable 인터페이스를 활용한 작업들이 Trigger에는 Trigger 인터페이스를 활용한 작업 주기를 설정한다.

여기서 Runnable을 인터페이스로 받는 클래스를 새로 만들려고 했는데, (class RunnableTask implements Runnable)

스케줄 등록할 때 해당 클래스를 계속 새로 할당하다보니 DB연결하는 부분이(여기선 sqlSession) 계속 안되서 NullPointerException이 발생했다.

원인을 찾다가 (거의 2주동안...나의 주말...) Bean은 WAS가 실행될 때 ApplicationContext에 등록이 되고 이를 가져오는데, Bean으로 관리되지 않는 클래스에서 new로 생성한 instance의 경우 Bean 주입이 일어나지 않기 때문이라는 사실을 알게 되었다.

(관련 stackoverflow 질문)

따라서 세가지의 해결책을 찾게 되었는데,

1) ApplicationContext를 자바로 가져와서 Autowired 대신 Bean을 주입시키는 방법

2) @Async로 작업할 Task를 구현하는 방법

3) Task를 Bean에 주입해 관리하는 방법

여기서 1)은 시도했지만 왜인지 모르겠지만 Bean을 주입받지 못했고 똑같이 null이 발생했다.

시간이 없는 관계로 이거에 대한 문제는 다음에 공부하며 다시 해결해보기로 남겨두고 패스... ㅠ

2)는 @Async의 경우 해당 어노테이션을 통해 Runnable.run()을 자동으로 실행시켜주는거 같긴한데,

나는 파라미터로 Runnable을 넘겨줘야하는 상황이어서 이것도 패스...

따라서 나머지 하나인 3)을 활용했고, Task까지 스케줄 관리하는 곳에서 다같이 관리하도록 일단 정했다.

> 정지 (stopScheduler)

ScheduledFuture<V> Docs를 보면 cancel(boolean) method를 확인할 수 있는데,

cancle(true)를 통해 지정된 Thread를 정지시킬 수 있다.

Map은 따로 관리용으로 생성한거라 Map에서 까지 지워주려면 remove를 활용해 해당 작업을 취소한다.

> 작업 주기 (Trigger)

대표적으로 PeriodicTrigger, CronTrigger를 활용하면 되는거 같은데 조금 더 연구하고 내용을 보강해야겠다.

현재는 Periodic Trigger를 활용했는데, TimeUnit은 최대 DAY까지만 표현이 가능해서 매달,매년에 대한 고민도 해야한다.

Trigger에 대해서 조금 더 찾아보고 추가해야겠다.

추가)

Trigger 인터페이스를 받아서 CronTrigger, PeriodicTrigger를 동시에 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
    private Trigger trigger(int timeTerm) {
 
        //CronTrigger 사용
        if(timeTerm == 0) {
            return new CronTrigger("0 0 0 * 0 0");
        }
        //한달 주기
        if(timeTerm == 1) {
        }
 
        return new PeriodicTrigger(timeTerm, TimeUnit.SECONDS);
    }
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter

이처럼 입맛에 따라 CronTrigger와 PeriodicTrigger로 설정이 가능하다.

한달 주기로 설정할 경우, 스케줄이 등록된 달의 마지막 날짜를 더하면 되므로 LocalDateTime과 TemporalAdjusters를 활용했다. (이 둘은 JAVA8부터 추가된거 같은데, 차차 정리해나가야겠다)

 

마치며

그동안 기본기가 중요하지! 하며 자바만 신나게 봤는데 스프링에 대한 기본기가 많이 부족하다는 것을 깨닫게 되는 작업이었다... 관련 책도 샀으니 이젠 스프링 위주로 좀 더 공부를 해야겠다

Bean, ApplicationContext, IoC Container 등등 대략적으로만 아는데 조금 더 구체적으로 알아야 이런 스케줄 작업에 있어서도 덜 헤매지 않을까 생각한다

솔직히 너무 헤맸다... 어디서부터 잘못된건지 감도 안와서 삽질삽질 무한삽질 명륜진사삽질

배울게 아직도 많다

 

ps. 서버 이중화일 때 해당 코드가 올라간 WAS는 전부 스케줄이 돈다...

나름 고민 끝의 해결법은

1) 스케줄 관련 코드는 따로 빼서 하나의 WAS에만 올린다

2) Spring profile을 설정해서 Value("$spring.profiles.active") 로 받아서 처리한다

 

관련 링크 (찾아본 링크들)

https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#scheduling-annotation-support-async (스프링 Docs 스케줄 관련 링크)

https://okky.kr/article/320326 (okky : Thread에서 Bean 주입 실패 관련 질문)

https://okky.kr/article/341541 (okky : Thread 관련 질문)

https://www.baeldung.com/spring-task-scheduler (ThreadPoolTaskScheduler 예제와 함께 친절한 설명)

https://blog.outsider.ne.kr/1066 (Task 실행과 스케줄링)

https://jsonobject.tistory.com/233 (@Async 활용한 TaskExecutor 구현)

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.html#schedule-java.lang.Runnable-org.springframework.scheduling.Trigger- (ThreadPoolTaskScheduler Spring Docs)

http://dveamer.github.io/java/SpringAsync.html (@Async 비동기 처리 구현)

댓글