SmartLifecycle을 통해 org.springframework.kafka.listener.ConcurrentMessageListenerContainer 를 start 시키는것을 보고
SmartLifecycle과 LifeCycle에 대해 조금 살펴보았습니다.
추후 이와같이 서버 기동시에 세팅하고 종료 시키는 Bean이 필요하면 써먹으면 어떨까 합니다.
기존에는 초기화 Bean 을 만들고 @PostConstruct를 달아 초기화 로직을 돌리고 종료는 알아서 하곤 했습니다.
org.springframework.context.SmartLifeCycle은 Lifecycle, Phased 인터페이스를 구현한 인터페이스 입니다.
SmarLifeCycle을 살펴보기 이전에 LifeCycle을 살펴보고 SmarLifeCycle 을 살펴보겠습니다.
LifeCycle
LifeCycle은 라이프 사이클을 컨트롤 하기위해 start(), stop(), isRunning() 메소드로 구성되어 있으며
주석을 살펴보니 LifeCycle 인터페이스는 자동실행은 담당하지 않고 자동실행을 하고 싶으면"SmartLifecycle"을 활용하라고 되어있습니다.
Bean이 LifeCycle 인터페이스를 구현한다면 ApplicationContext에서 Start와 Stop 시그널을 준다고 되어있고
JMX를 통해서도 제어 가능하다고 되어있습니다
그리고 주요한 부분중 하나는 Scope가 "싱글톤" 일때만 동작한다는 것 입니다.
(테스트 해보니 "프로토타입"의 경우 동작하지 않는것을 확인 했습니다.)
(서버 다운시 ShutDown Hook에서 동작하다 보니 프로토타입은 적합하지 않나봅니다)
(동작하려면 서버 종료때까지 Bean이 살아있어야 한다는 말이기 때문에..)
SmarLifeCycle
SmarLifeCycle은 위에 적은대로 Lifecycle,Phased 를 구현한 인터페이스이지만 LifeCycle과 동작이 조금 다릅니다.
종료시점에 isRunning을 체크하여 stop을 호출해준다는 부분은 같지만
앞에 설명대로 SmartLifeCycle은 자동 실행으로 기동시점에 start() 메소드를 호출해줍니다.
DefaultLifecycleProcessor:140에 startBeans를 보면, 두 가지 Bean 모두 lifecycleBean 맵에 존재 하지만 체크로직으로 SmarLifecycle Bean만 phases에 들어가는 것을 볼수 있고 하단에 start()를 호출해주는 부분을 볼수 있습니다.
그리고 종료(stop)하는 부분도 LifeCycle살짝 다르긴한데
DefaultLifecycleProcessor:221에 doStop을 보면, SmarLifecycle은 Callback을 넘겨받는 stop이 불리며
Lifecycle은 단순 stop만 호출하고 끝나는 것을 볼수 있습니다.
서버 다운시 돌고있는것을 정리만 해주기 원한다면 Lifecycle을
서버 부팅시 start, 종료시 stop 모두 서버와 Lifecycle을 함께 간다면 SmartLifecycle Bean을 고려해보는것도 좋을것 같습니다.
지난번 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로 실행도 가능하지만