그림에서 보듯 실행 흐름 관점에서 보면 Scheduler가 Job을 사용하며 Job에 의존하고 있다. 하지만 Scheduler는 사실 상 Quartz 프레임워크 그 자체로서 안정적인 모듈이고, Job은 Scheduler에 의해 실행될 실제 작업에 대한 로직이 포함돼 있어 변경 빈도가 더 높고, Job 자체가 추가/삭제되는 일도 빈번하므로 불안정하다. 안정적인 모듈이 불안정한 모듈에 의존하는 것은 좋지 않다. Quartz 개발자들이 이런 사실을 모를 리 없다.
그래서 Job은 사실 인터페이스다. 그리고 Scheduler에 의해 실행되는 실제 작업들은 Job 인터페이스의 구현체 들이다. 즉 Quartz 자체는 Scheduler와 Job 구현체를 분리할 수 있게 잘 설계돼 있다.
그런데 이렇게 분리할 수 있음에도 불구하고 Scheduler와 Job 인터페이스의 구현체를 같은 모듈 안에 두면서 문제가 시작된다. 작업을 추가하려면 스케줄러까지 재배포를 해야 되며, 스케줄러 재배포는 스케줄러의 중단을 의미하므로 실행되는 작업이 없을 때만 가능하며, 많은 작업이 스케줄링 돼 있다면 재배포 타이밍을 잡기가 어려워진다. 따라서 스케줄러 본체인 Scheduler와 작업 클래스인 Job 구현체를 다음과 같이 분리해서 별도로 배포할 수 있으면 이 문제를 해결할 수 있다.
하지만 인터넷에서 찾을 수 있는 대부분의 Quartz 사용법이나 예제는 Scheduler와 Job 구현체를 동일한 서버 인스턴스에 일체형으로 묶어둔 아키텍처를 기준으로 설명하고 있다. 아마도 Quartz의 사용법 자체에 중점을 두기 때문이겠지만, 이유야 어찌됐든 자료가 그러하므로 결국 실제 프로젝트에 적용할 때도 일체형으로 구성하는 곳이 많은 것 같다.
이제 스케줄러와 작업 클래스를 분리해서 별도로 배포할 수 있게 만들고 클린 아키텍처에 한 걸음 다가가보자. 3가지 고개를 넘어야 한다.
클래스로딩
스케줄러가 작업을 스케줄하려면 작업 클래스를 로딩하고 실행 주기를 지정해야 한다. 대략 다음과 같다.
스케줄러에 의해 실행되는 SimpleJob은 단순하게 JobExecutionContext에 담겨 있는 키-밸류를 출력한다.
(1)과 같이 JobDetail과 Trigger를 Scheduler에 전달해주면 스케줄링 된다. 실행될 작업의 클래스는 (2)와 같이 클래스 리터럴 형태로 JobDetail에 지정된다. Trigger는 편의상 (3)과 같이 바로 실행되는 방식으로 지정했지만 Cron Expression 등 다양한 방식으로 지정할 수 있다.
실행해보면 다음과 같이 SimpleJob이 실행된다.
커스텀 클래스로더
아키텍처 관점에서 중요한 지점은 (2)다. 일체형일 때는 위와 같이 직접 SimpleJob.class로 지정해주면 되지만, 분리돼 있다면 해당 클래스를 외부에서 읽어와야 한다. 따라서 다음과 같이 URLClassLoader를 활용해서 JOB_REPO로 지정된 jar 파일에 있는 작업 클래스를 로딩할 수 있는 커스텀 클래스로더가 필요하다.
private ClassLoader getClassLoader(){ try { returnnew URLClassLoader( new URL[] { new File(JOB_REPO).toURI().toURL() }, // URLClassLoader 설정 시 parent를 webAppClassLoader로 지정해줘야 // org.quartz.Job 등 내부 의존 클래스 로딩 가능 this.getClass().getClassLoader() ); } catch (MalformedURLException e) { thrownew RuntimeException(e); } } }
이제 다시 애플리케이션을 실행해보면 다음과 같이 분리된 별도의 jar 파일에서 작업 클래스를 로딩해서 실행하는 것을 확인할 수 있다.
스케줄러쪽에서 io.homo_efficio.quartz.job.RemoteSimpleJob와 같이 외부 jar에 있는 작업 클래스 위치를 문자열로 직접 참조하고 있어서 마치 스케줄러 모듈(quartz-scheduler)이 작업 모듈(quartz-job)에 의존하는 것처럼 보이지만, 실무에서는 작업 클래스 위치나 실행 주기 정보를 DB에서 읽어오므로 실제 환경에서는 스케줄러 모듈은 작업 클래스 모듈을 모른다. 따라서 이제부터는 RemoteJob2, RemoteJob3 등을 추가하더라도 quartz-job.jar만 빌드/배포하면 되며, 스케줄러는 재배포할 필요가 없는 구조가 만들어졌다.
이렇게 해서 스케줄러와 작업 클래스를 분리하는데 성공했다. 어렵지 않다.
그런데 실무에서 사용하는 작업 클래스들이 RemoteSimpleJob처럼 단순할리는 없다. DB 작업도 있을 것이고 하둡 인프라와 관련한 작업도 있을 것이다. 이런 것들이 가능하려면 스케줄러 쪽에 있는 컴포넌트를 @Autowire로 주입 받아야 하는데, 지금처럼 런타임에 로딩되는 방식에서도 가능할까?
첫번째 고개만으로도 양이 제법되니 일단 여기서 1탄을 마무리하고 @Autowire는 2탄에서 다룬다.
정리
스케줄링을 담당하는 스케줄러와 실행되는 작업은 변경 주기가 다르다. 그런데 이를 한 곳에 모아 일체형으로 구성하면 운영이 매우 불편해진다.
Quartz는 스케줄러와 작업을 분리할 수 있도록 설계되어 있다.
작업 클래스를 별도의 jar로 묶고, 스케줄러 쪽에서 URLClassLoader를 사용해서 작업 클래스를 로딩하도록 개선하면 스케줄러와 작업 클래스의 배포를 분리할 수 있어 운영 부담을 대폭 줄일 수 있다.