지난번 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로 실행도 가능하지만
getJobParametersIncrementer() : Job 실행시 새로운 파라미터를 만들경우(JobParametersIncrementer에 구현 내용은 실제로 JobParam을 받아 JobParam을 리턴하도록 되어있음)
isRestartable() : 재시작 여부
GroupAwareJob 배치명을 그룹명을 포함하여 활용할 경우, 발췌잡 -> 론뱅크.발췌잡 그룹명을 들고 있고, 메소드는 다 Job에 있는 것들을 delegate 하여 호출하는 방식
AbstractJob 추상 클래스이며, 공통 의존성들에 대해(JobRepository나 JobExecututionListener나..) 설정들에 대해 로직들이 기술되어있음 실제 대부분 모든 Job은 AbstractJob을 상속받아 구현한 구현체일것 Step 관련된 로직은 대부분 Abstract로 두어 구현한 Job에 따라 원하는 형태로 관리할 수 있도록 함
변수
JobRepository : Job의 정보(Param 이나 메타정보)들을 얻거나, 상태를 업데이트 하는 repo
CompositeJobExecutionListener : Job 선후에 불릴 JobExecutionListener들을 등록받아 일괄로 불러주는 컴포지트 클래스
StepHandler : step의 실제 처리 로직(실행 및 재처리에 따른 처리 및 상태처리 등)을 담고 있는 핸들러
메소드
[A] getStep(String) : 추상메소드로 둬서 Step을 어떻게 저장하고 관리할지는 위로 위임
[A] getStepNames() : 추상메소드로 둬서 Step을 어떻게 저장하고 관리할지는 위로 위임
[final] execute(JobExe) : Job의 메소드이며 핸들러 호출 및 Repo 호출 등에 대한 로직이 담겨 있음, final로 구현 내용을 바꾸지 못하도록 했고, 메소드에서는 doExecute()을 호출하도록 되어있기 때문에 구현 클래스에서는 Job실행은 doExecute()을 통해 구현
[final] handleStep(Step, JobExe) : step에 실행에 대한 메소드를 final로 내용을 변경하지 못하도록 했고, StepHandler를 변경함으로 내용을 변경할 수 있도록 제공
[A] doExecute(JobExe) : 구현 클래스에서 Job 실행 부분을 구현하도록 열어놓은 메소드
SimpleJob Step을 순차적으로 실행하는 Job의 구현체 Step이 실패시 Job이 실패하며, 모든 Step이 Complete 될경우 Job이 Complete 된다.
변수
ArrayList : Step을 ArrayList로 관리//Step이 그렇게 많지는 않겠지만, getStep은 O(n)
/**
* Given the current context in the form of a step contribution, do whatever
* is necessary to process this unit inside a transaction. Implementations
* return {@link RepeatStatus#FINISHED} if finished. If not they return
* {@link RepeatStatus#CONTINUABLE}. On failure throws an exception.
*
* @param contribution mutable state to be passed back to update the current
* step execution
* @param chunkContext attributes shared between invocations but not between
* restarts
* @return an {@link RepeatStatus} indicating whether processing is
* continuable. Returning {@code null} is interpreted as {@link RepeatStatus#FINISHED}
*
* @throws Exception thrown if error occurs during execution.
*/
@Nullable
RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception;
결국 리턴이 RepeatStatus.FINISHED(혹은 null) 로 리턴되면 마치게 되고 RepeatStatus.CONTINUABLE 의 경우에는 execute를 다시 호출하게 됩니다
(리턴값을 RepeatStatus.CONTINUABLE로 계속 반환하면 무한히 돌겠죠..)
Spring Batch에서 Tasklet을 imple하여 제공하고 있는 클래스 및 interface는 아래와 같습니다.
[I] Tasklet
[C] MethodInvokingTaskletAdapter : POJO 메소드 호출을 Step 시점에 호출하기위한 wraps 클래스
[C] CallableTaskletAdapter : Callable을 Step 시점에 호출하기 위한 wraps 클래스
[C] ChunkOrientedTasklet : ChunkProvider(SimpleChunkProvier 기준으로 ItemReader) , ChunkProcessor(SimpleChunkProcessor 기준으로 ItemProcessor, ItemWriter)를 활용하는 Tasklet(ETL)
[I] StoppableTasklet : (3.0부터) stop() 메소드를 구현하도록 하는 인터페이스며, 프레임워크에서 JobOperator.stop 시 해당 메소드를 불러줍니다.
[C] BatchletAdapter : (3.0부터) Batchlet을 지원하는 Tasklet Batchlet은 arg가 없는 process와 stop 메소드가 있는 interface 입니다.
[C] SystemCommandTasklet : Command(Java가 아닌 별도 프로그램을 실행한다던지)를 위한 Tasklet