지난번 SpringBoot에서 SpringBatch를 동작시키는데, External Class들을 잡는데 생기는 문제점들이 있어서
지난번 글에 MANIFEST.MF에 org.springframework.boot.loader.JarLauncher 가 Main-Class 에 적혀 있고
실제 개발한 코드의 메인은 Main-Class에 적힌 JarLauncher가 호출해준다는 부분을 언급했습니다.
JarLauncher 코드를 보며 클래스로더를 봤을때는 "클래스로더로 커스텀으로 구성하던지 해야겠구나" 로 생각했는데
검색해보니 역시나 해결방법은 있었습니다.
org.springframework.boot.loader.JarLauncher 말고도 빌드된 jar파일을 보면 다른 클래스들이 함께 들어있는데
다른 여러 클래스들이 있지만 Launcher 구현체는 WarLauncher와 PropertiesLauncher 두가지가 추가로 있습니다.
org.springframework.boot.loader.PropertiesLauncher 소스를 열어보니 처음 시도하던 -cp로 잡은 클래스패스와 BOOT-INF를 같은 클래스로더로 구성하는 방식은 아니지만 클래스패스 경로를 지원하는 설정을 통해 BOOT-INF와 별도로 설정한 클래스 패스 경로를 설정할 수 있도록 작성되어있습니다
그설정은 바로 "loader.path" 입니다.
저는 실행시 -Dload.path=userLib/ 형식으로 주거나 별도 설정 파일로 영역을 열어줄 예정입니다.
"loader.path"의 특이사항으로는 해당 영역의 클래스와 jar 그리고 리소스를 모두 클래스 패스에 한번에 잡아준다는 부분이며 해당 클래스패스에 있는 Configuration 클래스들도 Bean으로 잘 등록되는것을 확인했습니다.
물론 이 부분이 해결됬다고 하더라도 추가적으로 해야할 부분은 많이 남았습니다.
외부 클래스들을 클래스 패스로 제공하면 현재는 패키지가 동일한 클래스를 테스트 했지만
다른 패키지라면 componentScan을 어떻게 제공할 것인지
xml 설정은 어떻게 커버할 것인지 등.. 하나씩 시도해보고
막히는 부분이나 기록할 부분이 있으면 기록을 남길 예정입니다.
(작성하다보니 이건 SpringBatch 라기보다는 SpringBoot 활용 관련 이슈군요..)
아 그렇군 그럼 "java -cp boot.jar:app.jar com.meteor.AppMain" 가 아니라
"java -cp boot.jar:app.jar org.springframework.boot.loader.JarLauncher" 를 부르면 되겠군!
이라고 생각했지만 AppMain은 불리지만 문제가 있었습니다.
app.jar에 포함되어있는 내용은 job의 선언부 즉, configuration인데 이 클래스에 Bean들을 등록하다가
org.springframework.batch.core.Step 클래스를 못찾는 문제가 발생했습니다
"응? org.springframework.batch.core.Step은 그냥 boot.jar안에 lib에 Spring-batch쪽에 있는..건데..? 왜 못찾지?"
사실 위에 힌트가 있습니다
클래스 못찾는 문제니 당연히 클래스 로더 이슈(클래스로더가 잘못됬다는건 아닙니다, 대부분 사람이 문제죠) 였습니다.
현재 Configuration클래스는 위에 -cp로 잡았기 때문에 시스템 클래스로더에 물려있습니다.
그리고 org.springframework.batch.core.Step는 아까 JarLauncher가 만든 LaunchedURLClassLoader에 물려있습니다.
그리고 서로의 관계는 LaunchedURLClassLoader의 부모로 시스템 클래스 로더로 구성되어있죠
일반적인 클래스로더 구조 보통 상위 클래스 로더에 있는지 찾고 내려오는 구조입니다.
(일반적으로 상위 클래스 로더는 child를 알진 못합니다.)
즉 Thread에 ContextClassLoader는 LaunchedURLClassLoader로 설정 했으니
LaunchedURLClassLoader를 통해 Step이든 ConfigurationClass든 다 볼수 있는건 맞지만
Step만 로딩 한다면, 시스템 클래스로더에는 없으니 내려와서 LaunchedURLClassLoader 에서 찾고
ConfigurationClass를 로드 할때는 시스템 클래스로더에 있으니 바로 로딩했을겁니다.
그런데 ConfigurationClass를 막상 돌려보니 안에 Step.class가 있고 ConfigurationClass를 로딩한 시스템 클래스로더로 Step.class를 로드하니 없어서 에러가 난것으로 보입니다.
쉽게 말하면 parent클래스 로더에서 parent클래스 로더에 있는 클래스(child의 클래스를 바라보는)가 하위를 바라보니 시스템 클래스로더는 없는 클래스니 에러난거죠, JobConfig에서 Step을 참조할때 LaunchedURLClassLoader 를 쓰면 되겠지만, 에러가나는 코드는 빈등록하다 에러난거라..
Map<String, ? extends Job>//String은 Bean네임 로 인젝션을 받으면
Bean으로 등록된 Job들을 받을수 있었습니다.
두번째 방법인 ListableJobLocator는 getJobNames()로 Job의 Name List를 가져오거나
getJob(name)을 통해 Job 객체를 가져올수 있습니다.
하지만, 저처럼 별다른 설정을 하지 않으면 위와 같은 에러를 만날수 있습니다.
@Bean
public Job longJob() throws DuplicateJobException {
return jobBuilderFactory.get("longJob").start(longStep()).build();
}
에러가 발생하는 이유는 JobRegistry에 등록을 하지 않았기 때문인데요
해결하는 방법도 다양하겠지만 두가지를 소개 하려고합니다.
첫번째 방법 Job을 Bean으로 등록하기전 JobRegistry에 등록
두번째 방법 JobRegistryBeanPostProcessor를 Bean으로 등록하여 Registry에 등록
첫번째 방법인 JobRegistry에 등록하기 위해서는 JobRegistry에 register(JobFactory) 메소드를 이용하여 등록할 수 있는데, JobFactory는 ApplicationContextJobFactory 혹은 ReferrenceJobFactory를 통해 등록 하면 됩니다.
ApplicationContextJobFactory은 JobName과 ApplicationContextFactory를 받아 Bean으로 등록된 Job을 생성하는 Factory이며(Job을 Bean으로 등록하는 와중에 ApplicationContextJobFactory를 활용한 등록은 불가능 합니다.)
ReferrenceJobFactory는 Job Instance를 받아 그대로 돌려주는 방식으로 동작하는 Factory로 Bean을 등록하는 와중에 활용 할 수 있습니다.
@Bean
public Job longJob() throws DuplicateJobException {
Job job = jobBuilderFactory.get("longJob").start(longStep()).build();
ReferenceJobFactory factory = new ReferenceJobFactory(job);
jobRegistry.register(factory);
return job;
}
다만 이 방법은 Job Bean 선언 마다 register를 불러야 하는 단점과 원하는 Job만 등록할 수 있다는 장점이 있겠습니다.
두번째 방법은 JobRegistryBeanPostProcessor를 활용하는 방법이 있습니다.
@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();
jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry);
return jobRegistryBeanPostProcessor;
}
네 끝입니다.
Job마다 등록할 필요 없이 Job Type이 빈으로 등록될때 후처리로 자동으로 JobRegistry에 등록하게 됩니다.
등록을 하지 않아도 Job Type을 Injection으로 찾아 JobLauncher로 실행도 가능하지만