티스토리 뷰

728x90

이번에 새로운 프로젝트를 진행하면서 필요한 기능 중 하나는 사용자로부터 반복 주기를 받아 그 때마다 특정 동작을 반복으로 실행시켜주어야 하는 기능이 있었다.

이를 위해  1️⃣스케쥴러가 동적으로 생성되어야 하고 2️⃣ 이중화 환경을 고려하여 스케쥴러는 여러개의 서버 중에 한대에서만 실행이 되어야한다.

 

사실 스케쥴러가 서버 실행 전에 정해져있다면 아래 처럼 shedlock 어노테이션으로 쉽게 스케쥴러 이중화를 할 수 있다.

그렇지만 동적으로 생성하는 스케쥴러에는..어노테이션을 붙일 수 가 없다.

@Component
@Slf4j
public class TestScheduler {
    @Scheduled(cron = "0 0 0 * * *")
    @SchedulerLock(name="registerScheduler", lockAtLeastFor = "30s", lockAtMostFor = "30s")
    public void registerScheduler() {
        System.out.println("registerScheduler실행");
    }
}

해당기능을 만들기 위한 개발 순서는 이렇다.

 

1️⃣ 동적 스케쥴러(uploadScheduler) 를 만드는 스케쥴러(registerScheduler) 를 생성한다.

2️⃣ 동적스케쥴러를 생성 시 shedlock의 LockProvider로 락을 직접 건다.

 

이렇게 정리하면 2 step의 간단한 일인데.. 구현하기 전까지는 어떻게 해야할지 고민이 많았다.

 

👉🏼스케쥴러(registerScheduler) 생성하기

- 매일 자정마다 실행 되어 동적 스케쥴러를 생성하는 역할

- 서버마다 동적 스케쥴러가 생성된다.

@Component
@Slf4j
public class TestScheduler {
    @Scheduled(cron = "0 0 0 * * *")
    public void registerScheduler() {
        System.out.println("registerScheduler실행");
    }
}

👉🏼동적스케쥴러 생성하는 방법

- 사용자가 지정한 시간에 동작하는 스케쥴러

- ThreadPoolTaskScheduler 를 사용해서 스케쥴러를 생성한다.

@Component
@RequiredArgsConstructor
public class TestScheduler {
    
    @Scheduled(cron = "0 0 0 * * *")
    public void registerScheduler() {

        //데이터를 DB에서 가져왔다고 가정
        List<Map<String,String>> dataList = new ArrayList<>();
        for (int i=0; i<10; i++) {
            Map<String,String> data = new HashMap<>();
            data.put("id", "id"+(i+1));
            String cron = "0 */1 * * * *";
            if (i%2 ==0) {
                cron = "0 */2 * * * *";
            }
            data.put("cron",cron);
            dataList.add(data);
        }

        //동적스케쥴러 생성
        dataList.forEach(
                (data)-> {

                    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
                    scheduler.initialize();
                    scheduler.schedule(() -> {
                          System.out.println("동적 스케쥴러 실행");
                     }, new CronTrigger(data.get("cron"))
                    );
                }
        );
    }
}

👉🏼동적스케쥴러에 LockProvider으로 중복 실행 방지 코드를 넣어보자

- 여러대의 서버가 동시에 실행 될 때 하나의 서버에서만 실행 될 수 있도록 하는 작업

@Component
@RequiredArgsConstructor
public class TestScheduler {
  
    private final LockProvider lockProvider;
    @Scheduled(cron = "0 0 */5 * * *")
    public void registerScheduler() {


        //데이터를 DB에서 가져왔다고 가정
        List<Map<String,String>> dataList = new ArrayList<>();
        for (int i=0; i<10; i++) {
            Map<String,String> data = new HashMap<>();
            data.put("id", "id"+(i+1));
            String cron = "0 */1 * * * *";
            if (i%2 ==0) {
                cron = "0 */2 * * * *";
            }
            data.put("cron",cron);
            dataList.add(data);
        }

        //동적스케쥴러 생성
        dataList.forEach(
                (data)-> {

                    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
                    scheduler.initialize();
                    scheduler.schedule(() -> {

                                //Lock 설정
                                LockConfiguration lockConfiguration = new LockConfiguration(
                                        Instant.now(),
                                        "Lock이름_"+data.get("id"),
                                        Duration.ofSeconds(30),
                                        Duration.ofSeconds(30)
                                );
                                //Lock 걸기
                                Optional<SimpleLock> lockOptional = lockProvider.lock(lockConfiguration);

                                if (lockOptional.isPresent()) {
                                        //Lock 획득 성공
                                        //동적 스케쥴로 인해 실행 될 비즈니스 로직 작성

                                        try {
                                            System.out.println(data.get("id") + ":: Lock 획득에 성공했습니다.");

                                        } finally {
                                            // Logic 실행 종료 후 Lock 해제를 해준다.
                                            lockOptional.get().unlock();
                                        }
                                    } else {
                                        //Lock 획득 실패
                                        System.out.println(data.get("id") + ":: Lock 획득에 실패했습니다.");
                                        System.out.println("다른 노드에서 실행 중입니다.");
                                    }
                            }, new CronTrigger(data.get("cron"))
                    );
                }
        );
    }
}

👉🏼 최종코드

- 동적스케쥴러를 생성하는 코드는 매일 자정 돌기 때문에, 동적스케쥴러를 생성하기 전에, 기존에 있는 스케쥴러를 삭제해주어야한다.

이를 위해, List를 생성하고, 동적 스케쥴러를 생성 한 후 List에 담아준다.

- 만약 운영 중 재배포를 해야하는 상황이 발생하는 경우에 생성된 스케쥴러는 사라진다.

저녁 8시에 돌아가는 동적 스케쥴러가 있고, 재배포를 저녁 6시에 했다고 가정하면,

동적스케쥴러를 생성해주는 스케쥴러는 12시에 실행되기 때문에, 저녁 8시에는 스케쥴러가 돌 수가 없다. 따라서, 서버 재시작을 대비해 @Postconstruct를 사용하여 서버가 실행 되어 DI 될 때 스케쥴러를 초기화 해준다.

@Component
@RequiredArgsConstructor
@Slf4j
public class TestScheduler {

    private List<ThreadPoolTaskScheduler> schedulerList = new ArrayList<>();
    private final LockProvider lockProvider;
    
	@PostConstruct
    public void initialize(){
        makeScheduler();
        log.info("initialize scheduler");
        log.info("scheduler size : " + schedulerList.size());
    }
    
    
    @Scheduled(cron = "0 0 */5 * * *")
    public void registerScheduler() {

        //전날에 생성 된 스케쥴러를 삭제한다.
        schedulerList.forEach((scheduler)-> {
            scheduler.shutdown();
            schedulerList.remove(scheduler);
        });
		makeScheduler();
       
    }
    
    private void makeScheduler() {
		 //데이터를 DB에서 가져왔다고 가정
        List<Map<String,String>> dataList = new ArrayList<>();
        for (int i=0; i<10; i++) {
            Map<String,String> data = new HashMap<>();
            data.put("id", "id"+(i+1));
            String cron = "0 */1 * * * *";
            if (i%2 ==0) {
                cron = "0 */2 * * * *";
            }
            data.put("cron",cron);
            dataList.add(data);
        }

        //동적스케쥴러 생성
        dataList.forEach(
                (data)-> {

                    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
                    scheduler.initialize();
                    scheduler.schedule(() -> {

                                //Lock 설정
                                LockConfiguration lockConfiguration = new LockConfiguration(
                                        Instant.now(),
                                        "Lock이름_"+data.get("id"),
                                        Duration.ofSeconds(30),
                                        Duration.ofSeconds(30)
                                );
                                //Lock 걸기
                                Optional<SimpleLock> lockOptional = lockProvider.lock(lockConfiguration);

                                if (lockOptional.isPresent()) {
                                    //Lock 획득 성공
                                    //동적 스케쥴로 인해 실행 될 비즈니스 로직 작성

                                    try {
                                        System.out.println(data.get("id") + ":: Lock 획득에 성공했습니다.");

                                    } finally {
                                        // Logic 실행 종료 후 Lock 해제를 해준다.
                                        lockOptional.get().unlock();
                                    }
                                } else {
                                    //Lock 획득 실패
                                    System.out.println(data.get("id") + ":: Lock 획득에 실패했습니다.");
                                    System.out.println("다른 노드에서 실행 중입니다.");
                                }
                            }, new CronTrigger(data.get("cron"))
                    );
                    //다음날 삭제 할 수 있도록 schedulerList에 저장한다.
                    schedulerList.add(scheduler);
                }
        );
    }
}

👉🏼 테스트 및 확인해보기

- 실행 된 로그를 보면 왼쪽 서버에서는 id10은 Lock 획득에 성공하고, 오른쪽 서버에서는 Lock 획득에 실패 한 것을 확인 할 수 있다.

DB에도 해당 스케쥴러가 잘 등록 된 것을 확인 할 수 있다.

 

인텔리제이에서 동일한 프로젝트를 여러개 실행 시키는 방법이 궁금하다면 아래 포스트를 참고해주세요😉

https://yes-admit.tistory.com/113

 

[인텔리제이] 멀티 서버 실행하기 / 동일한 springboot 서버 여러개 실행

shedlock을 테스트 하면서 서버를 동시에 실행시킬 일이 생겼다. 인텔리제이에서는 하나의 프로젝트에서 여러개의 서버를 실행시킬 수 있도록 해준다..갓인텔리제🥹 (아마 다른 IDE도 이런게 있지

yes-admit.tistory.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함