Java 에서의 스케쥴링, 어떻게 가능한 것인가?
우리는 Spring 에서 스케쥴링 잡을 수행하기 위해, @Scheduled
라는 어노테이션을 사용하곤 한다.
@Scheduled(cron = "0/5 * * * * ?")
public void scheduledCron() {
// do something...
}
@Scheduled(fixedDelay = 1000)
public void scheduledfixed() {
// do something...
}
그런데 상식적으로 생각을 해보면, 정말로 Java가 1ms 단위까지 세밀하게 스케쥴링을 할 수 있을까? 라는 의문이 들 수 있다.
Java 이야기는 아니지만 Redis/MongoDB 같은 데이터베이스의 TTL 또한 ms 단위로 넣을 수 있지만 실제로는 ms 단위로 데이터 체킹 및 삭제가 이뤄지지 않기도 한다. Redis는 기본적으로 사용자가 데이터 조회 시도 전 까지 삭제를 수행하지 않고, (주기적으로 샘플링하여 체킹하고 지우는 작업이 존재하지만, 이는 보조적인 역할이다.) MongoDB는 TTL Monitor 스레드가 주기적으로 데이터를 확인하여 제거한다. (ttlMonitorSleepSecs
파라미터에 따라 다르지만, 디폴트는 1분이다. 즉, 최대 59초까지는 데이터가 안 지워질 수 있 다.)
결국, 수많은 상용 기술들은 스케쥴링 처리가 우리의 의도 처럼 정확한 시간에 수행된다 라고 말하기 어려운데, Java는 어떨까?
Java가 스케쥴링을 지원하는 방법
가장 간단한 생각 - Thread.sleep()
Java의 Thread
는 대부분의 실제 동작을 OS에 위임하고 있다. 그렇기에, Thread.sleep(millis, nanos)
와 같은 메서드는 OS 스케쥴러에 이를 위임하게 되고, 자연스럽게 주기적인 작업을 수행하도록 유도할 수 있을 것이다.
try {
while (true) {
Thread.sleep(5000);
// do something...
}
} catch (InterruptedException e) {
e.printStackTrace();
}
다만, 이 방법이 스케쥴링 관점에서 효율적일까?
- 운영체제 관점에서, 인터럽트 된 스레드는 인터럽트 처리가 완료되어도 바로 실행되는게 아니라, 스케쥴링의 대상이 될 뿐이다. 즉, 5000ms 이후에 바로 실행됨이 보장되지 않는다.
- Real-Time 운영체제가 아닌 일반적인 운영체제의 경우, 정확한 Real Time 스케쥴링이 어렵다. (사실 이 부분 때문에라도, ms 단위 스케쥴링의 정확성을 보장하는 것은 거의 불가능하다.)
- 만약 우선순위를 설정했다면, (
Thread.setPriority()
등으로) sleep 으로 시간 지연한 스레드가 바로 수행됨이 보장되지 않는다. - 스케쥴링 해야 하는 작업이 2개 이상이라면 각각의 작업에 대해 별도의 스레드가 필요한데, 각각의 스레드가 Sleep 처리가 되면 효율성이 매우 떨어진다.
사실 다른 사항들은 약간의 오차를 감안하고 넘어갈 수 있다고 쳐도, 마지막이 가장 치명적일 것이다. 스레드는 결국 자원을 차지하고 있기 때문에, 블로킹된 스레드의 수가 많으면 많아질 수록 프로그램의 전반적인 효율은 떨어질 것이다.
그렇기에, Java에서는 일반적으로 여러 작업을 병렬적으로 수행하기 위해서 스레드풀을 사용하고 있고, Java에서는 가장 기본적인 ThreadPoolExecutor
를 상속한 ScheduledThreadPoolExecutor
를 통해 스레드풀을 사용하면서도 스케쥴링 잡을 수행할 수 있도록 구현하고 있다.
해당 도구의 구현을 좀 더 자세히 살펴보기 전, 위 글만 보면 생각할 수 있을 만한 “블로킹만 최소화 하면 좀 괜찮지 않을까?” 에 대한 해답을 알아보자.
방향 살짝 틀어보기 - Spin Lock
다음 코드를 보자.
var now = System.nanoTime();
while (true) {
var newTime = System.nanoTime();
if (newTime - now > 1_000) {
// do something
now = newTime;
}
}
이건 더 효율적인 솔루션일까?
결론만 말하자면 특정 상황에 따라 더 효율적일 수 있으나, Java 에선 대부분 비효율적이다.
- 이론적으로는 아주 정확한 타이밍을 모니터링 할 수 있지만, Java 언어 자체가 Hard Real Time 에 적합한 언어는 아니라, 완벽한 정확성을 보장할 수 없다.
- 참고: https://ko.wikipedia.org/wiki/%EC%8B%A4%EC%8B%9C%EA%B0%84_%EC%BB%B4%ED%93%A8%ED%8C%85
- 일반적인 Linux/Windows/MacOS 등의 OS는 RTOS (Real-Time OS) 가 아니라,
nanoTime()
을 호출했을 시점에 바로 값을 리턴할거란 보장을 할 수 없다. 즉, 실제 해당 작업이 OS 스케쥴러를 통해 선택되고, CPU에 향하는 시점을 완벽하게 통제할 수 없다.
- 그와 반비례하여 자원 소모량이 엄청나게 올라간다. 하나의 작업마다 저렇게 Spin Lock을 걸어버리면, OS 및 JVM 스케쥴러에 악영향을 주기 쉽다.