본문 바로가기

프로그래밍/Spring

[Spring] @Scheduler를 동적으로 대체하자! (Feat. ThreadPoolTaskScheduler)

서론

현재 사내에서 사용하고 있는 스케줄러를 개선하는 작업을 하고 있다.

기존에는 C# 환경에서 RestSharp + Thread를 활용해 스케줄링 작업을 하고 있었으나 내가 부서이동을 하는 바람에 마지막으로 C#을 Java로 변경하고 가라고 하셨다. 🥲 (C#과 Java는 비슷하니 금방 배우실 수 있을겁니다! 라고 말했으나... 하라고 하셨으니 만드는게 인지상정! 호다닥 만들러 슝~💻)

 

현재 스케줄러는 하단과 같은 json 구조에서 데이터를 파싱해 사용하고 있다.

[
  {
    "name": "ATrigger",
    "endPoint": "/ATrigger",
    "cronExpression": "0/10 * * * * *",
    "enable": "true"
  },
  {
    "name": "BTrigger",
    "endPoint": "/BTrigger",
    "cronExpression": "0/10 * * * * *",
    "enable": "true"
  },
  {
    "name": "CTrigger",
    "endPoint": "/CTrigger",
    "cronExpression": "0 1 * * * *",
    "enable": "false"
  }
]

 

처음 이것을 Java 프로그램을 만들어서 C#과 동일하게 Thread를 활용해 호출하려고 했으나 사수님이 Spring으로 만들어서 RestAPI를 Call하라고 하셨다.

 

이에 나는 Spring Scheduler 를 떠올렸고 하단과 같이 코드를 작성했다.

@Data
@NoArgsConstructor	// 기본 생성자가 없으면 parsing을 못 함.
@AllArgsConstructor
class BatchObject {
    private String name;
    private String endPoint;
    private String enabled;
    private String cronExpression;
}
@Slf4j
@Component
@EnableAsync
public class MySchedule {

    @Async
    @Scheduled(cron = "0/10 * * * * ?")
    public void ATrigger() throws InterruptedException {
        BatchObject batchObject = ConfigFactory.BatchObjectMap.get("ATrigger");
    	if(batchObject.getEnabled().equals("true") {
            log.info("ATrigger Called");
            get(batchObject.getEndPoint());
        }
    }
    
    @Async
    @Scheduled(cron = "0/10 * * * * ?")
    public void BTrigger() throws InterruptedException {
        BatchObject batchObject = ConfigFactory.BatchObjectMap.get("BTrigger");
    	if(batchObject.getEnabled().equals("true") {
            log.info("BTrigger Called");
            get(batchObject.getEndPoint());
        }
    }

    @Async
    @Scheduled(cron = "0/10 * * * * ?")
    public void CTrigger() throws InterruptedException {
        BatchObject batchObject = ConfigFactory.BatchObjectMap.get("CTrigger");
    	if(batchObject.getEnabled().equals("true") {
            log.info("CTrigger Called");
            get(batchObject.getEndPoint());
        }
    }
}

 

ConfigFactory에서 Parsing한 BatchObjectMap, Rest Call 하는 get 함수는 현재 필요하지 않으니 서술하지 않겠다.

 

아무튼 저렇게 만들고 정상적으로 돌아간 것을 확인 한 뒤 사수님께 보여드렸다.

Scheduled 데이터는 json이 아닌 .yaml로 작성한다면 하드코딩이 아닌 외부 파일로 관리할 수 있다고 말했다.

 

이걸 본 사수님은 유지보수가 힘들지 않을까? 라고 반응하셨다. 

 

그렇다. 지금은 예시로 3개의 스케줄러만 만들었지만 실제 사내에서 사용하는 스케줄러는 102개 였고 이를 하나하나 함수로 만드는 노가다를 했다.😱

 

구현 당시에는 이것이 최선이였다고 생각했지만 사수님은 떠나기 전 마지막 작품인데 조금 더 Creative하게 만들어보라고 하셨다.

본론

처음에는 Spring Scheduler를 1초마다 실행하게 하고 priority queue를 사용해서 (다음 호출 될 시간 < 현재 시간) 일 경우 pop하는 방식으로 구현하려고 했었다.

 

하지만 이 방법은 비동기 방식이 아닌 동기 방식이라 오래 걸리는 API의 경우 점차 스케줄링이 밀리는 단점이 있다.

 

구글링을 하던 도중 오늘의 히어로 ThreadPoolTaskScheduler를 찾게 되어 해결을 했다.

 

공식 문서

https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling

 

Integration

The Spring Framework provides abstractions for the asynchronous execution and scheduling of tasks with the TaskExecutor and TaskScheduler interfaces, respectively. Spring also features implementations of those interfaces that support thread pools or delega

docs.spring.io

@Slf4j
public class MyScheduler {
    private ThreadPoolTaskScheduler scheduler;
    private BatchObject batchObject;
    
    public MyScheduler(BatchObject batchObject) {
        this.batchObject = batchObject;
        if(this.batchObject.getEnabled().equals("true") {
            startScheduler();
        }
    }

    private void startScheduler() {
        scheduler = new ThreadPoolTaskScheduler();
        scheduler.initialize();
        scheduler.schedule(getRunnable(), getTrigger());
    }

    private Runnable getRunnable() {
        return () -> {
            log.info(batchObject.getName() + " Called");
            get(batchObject.getEndPoint());
        };
    }

    private Trigger getTrigger() {
        return new CronTrigger(batchObject.getCronExpression());
    }
}
public class ConfigFactory {
    private final List<BatchObject> batchObjectList = new ArrayList<>();
    public ConfigFactory(String serverInfo) {
        settingServer(serverInfo);
        settingBatchObjects();
        for(BatchObject batchObject : batchObjectList) {
            new MyScheduler(batchObject);
        }
    }
    // settingServer : serverList.json에서 서버 정보 파싱
    // settingBatchObjects : batchList.json에서 배치 정보 파싱
}

 

간단하게 설정하자면 MyScheduler 클래스에서 @Scheduler 어노테이션을 제거한 뒤 ThreadPoolTaskScheduler로 각각의 Schduler를 만들어 객체 생성 시 스케줄이 실행되도록 하였다.

 

결론

 

이번 작업을 하면서 얻은 이점은 @Scheduler를 사용하기 위해 만든 수 많은 함수들을 제거해 코드 가독성을 높였고 유지 보수 면에서 batchList.json의 값 변경 후 서버 재시작만 하면 된다는 점에서 큰 장점을 얻었다.