SpringBatch에 external Configuration Class 검토-2

JAVA/SpringBatch|2020. 4. 23. 19:38

지난번 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 활용 관련 이슈군요..)

댓글()

SpringBatch에 external Configuration Class 검토

JAVA/SpringBatch|2020. 4. 10. 20:12

업무상

SpringBoot 베이스로 SpringBatch 위에 조금 레이어를 추가할 계획을 가지고 있는데

 

제품 base jar에(SpringBoot 자체에 레이어를 추가한 jar) 에

클래스 패스로 추가적인 클래스(외부 개발자 혹은 고객이 개발 및 선언한 configuration class나 기타등등..)

를 더해서 돌려보면 어떨까 싶어 테스트를 시도하고 있습니다.

 

의도 했던 부분은 base jar에 추가적인 클래스를 심어 돌리면 되겠지만

관리상 base jar는 그대로 두고 추가적인 클래스는 jar로 묶거나 클래스 자체로 두고

배치 실행시 클래스패스에 base jar와 추가적인 클래스 jar를 잡아 실행 시키면 되지 않을까?

를 생각했지만 의도처럼 동작하지 않았습니다.

 

먼저 테스트로 추가해봤던 클래스는 job의 선언이 들어있는 Configuration Class 인데요

 

클래스 패스로 base와 추가된 클래스를 둘다 묶고 돌렸지만 몇가지 이슈들 만났습니다.

(일단 SpringBoot버전은 1.5.22와 2.2.5 그리고 1.3을 조금 살펴봤습니다. 1.3은 구조가 조금 달랐었습니다)

 

먼저 "java -jar boot.jar" 로 실행 했던것을 "java -cp boot.jar:app.jar com.meteor.AppMain" 방식으로 실행을 해봤는데

(실제 파일이나 옵션을 많이 생략했습니다)

 

boot.jar 내에 있는 com.meteor.AppMain 클래스를 못찾는 문제가 생겼습니다.

 

그래서 boot.jar 를 열어보니 클래스를 못찾는 이유를 알게됬는데, 디렉토리 구조가 일반적인 jar의 구조와는 많이 달랐습니다.

원래 boot.jar에 클래스가 있어야하는 바로 com 디렉토리가 보여야하는데

디렉토리 구조는

BOOT-INF

META-INF

org

였습니다.

 

META-INF는 흔한 디렉토리이고

org는 제가 생성하지 않은 클래스들이 들어있는 디렉토리 였습니다

BOOT-INF의 경우에는 하위에 classes와 lib이 있는데 classes에는

작성한 클래스들 lib에는 의존성이 걸려있는 lib들이 있었습니다.

 

위의 "java -cp boot.jar:app.jar com.meteor.AppMain" 처럼 실행했을때

com.meteor.AppMain을 찾으려면 org처럼 jar내에 바로 com 부터 있어야하는데 BOOT-INF/classes에 있으니

못찾는게 당연했습니다.

 

그럼 "java -jar boot.jar"로 호출했을때는 com.meteor.AppMain를 어떻게 찾고 있는건가?

를 살펴봤습니다.

 

일반적으로 execuexecutable jar는 META-INF의 MANIFEST.MF에 Main-Class에 있는 메인 클래스를 실행하게 됩니다.

 

그래서 boot.jar에 META-INF의 MANIFEST.MF에 Main-Class에 있는 클래스는

com.meteor.AppMain이라고 적혀있길 기대했지만

"org.springframework.boot.loader.JarLauncher" 라는 모르는 클래스가 적혀있었습니다.

그리고 "com.meteor.AppMain"는 Main-Class 에 적혀있지 않고 Start-Class 에 적혀있었습니다.

 

결국 "java-jar boot.jar" 를 호출한다면 com.meteor.AppMain이 호출되는것이 아니라 

org.springframework.boot.loader.JarLauncher것이 호출되는 것이고

내부 과정을 거쳐서 com.meteor.AppMain가 호출되는것을 예상 할수 있습니다.

 

그럼 "org.springframework.boot.loader.JarLauncher"는 무엇이고 어떻게 AppMain이 호출되는가?

를 조금 살펴봤습니다.

 

JarLauncher는 ExecutableArchiveLauncher를 상속 받고 있고 

ExecutableArchiveLauncher는 Launcher를 상속받고 있습니다.

 

당연히 JarLauncher에 main()이 있고, main 로직에는 launch(String[])를 호출하고 있고

launch(String[])은 상위에서 구현한 부분이 없기에 결국 Launcher에 launch(String[])가 불리고 코드는 하단과 같습니다.

protected void launch(String[] args) throws Exception {
    JarFile.registerUrlProtocolHandler();
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    launch(args, getMainClass(), classLoader);
  }

 

getClassPathArchives()에서는 BOOT-INF/classes/ 와 BOOT-INF/lib/의 경로를 넘기고

createClassLoader(url[])을 통해 LaunchedURLClassLoader를 만들도록 되어있습니다.

LaunchedURLClassLoader는 일단 URLClassLoader을 상속 받고 있고 당연히 상식적이게도 SystemClassLoader를 parent로 연결 시켜주는식으로 되어있습니다.

 

그후, getMainClass에서 MANIFEST.MF에 Start-Class 에 클래스를 얻어 넘기고 

launch에서는 start클래스에 메인 클래스를 부르는 MainMethodRunner를 통해 호출합니다.

(MainMethodRunner는 그냥 리플렉션으로 main(String[])를 부르는..클래스 입니다)

 

여기까지만 살펴봤을때 "JarLauncher가 결국에 Start-Class에 클래스를 리플랙션으로 부른다"

에 흐름 정도는 볼 수 있었습니다.

 

아 그렇군 그럼 "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 를 쓰면 되겠지만, 에러가나는 코드는 빈등록하다 에러난거라..

 

 

일단 현상황은 이정도 이며

 

해결 방법은 여러가지가 있을테지만 아직 어떤 방법이 좋을지는 고민중입니다.

BOOT-INF 안에 넣어서 한 클래스 로더에 물리는 방법이 있고

메인을 변경해서 클래스 로더를 Flat하게 커스텀 하는 방법도.. 있을것 같고..

 

추가로 정리할 내용이 생기면 업데이트 하도록 하겠습니다.

댓글()

spring boot log level 변경(actuator)

JAVA/Spring|2019. 12. 19. 19:50

로그레벨 동적 반영을 위하여

Controller를 하나 만들어서 처리할까 하다 이미 구현된 기능이 있을지 해서 검색해보니

이기 해당 기능이 actuator를 이용하여 처리가 가능하도록 되어있었습니다.

 

먼저 actuator를 사용하기 위해 pom.xml 에 추가합니다.(메이븐 기준으로)

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

 

설정 없이는 /actuator 로 확인할수 있는것들이 없지만 'management.endpoints.web.exposure.include' 에 항목들을 추가하여 추가적인 기능들을 사용할 수 있습니다.

management.endpoints.web.exposure.include=loggers

#management.endpoints.web.exposure.include=httptrace,loggers,health,info,metrics

 

logger 설정 및 확인을 위해 필요한 내용은 loggers입니다.

 

위와같이 설정 후 기동하게되면

'/actuator/loggers' 를 통해 모든 로거들의 로그레벨들을 확인할 수 있습니다.

 

 

'/actuator/loggers/로거명' 을 GET으로 요청하면 해당 로거만 나타나게 됩니다.

 

이제 목표인 로그레벨 동적 변경을 위해서는

'/actuator/loggers/로거명' 에 POST로 설정할 값을 보내주면 됩니다.

{"configuredLevel":null,"effectiveLevel":"INFO"} 를

{"configuredLevel":"ERROR","effectiveLevel":"ERROR"} 로 변경하고자 하면

 

헤더는 컨텐츠 타입을 application/json에 body에 {"configuredLevel":"ERROR","effectiveLevel":"ERROR"} 을 넣어 요청하도록 하면 됩니다.

 

 

 

'JAVA > Spring' 카테고리의 다른 글

[MSA] sidecar 패턴  (0) 2020.01.13
[SpringConfig] Properties 암호화  (0) 2019.12.24
reactor Schedulers  (0) 2019.12.16
스프링 부트 2.2 릴리즈노트  (0) 2019.11.01
[Spring] zuul 사용시 Eureka client로 분배가 안되는경우  (0) 2019.08.16

댓글()

[Azure] SpringBoot, AppService로 배포

JAVA/Spring|2019. 8. 2. 18:51

개인 Toy 프로젝트를 라즈베리파이를 통해 개발하다 요즘 MS Azure를 활용하여 개발중입니다.

AWS를 쓰다 무료기간이 지난후 과금으로 인해 Cloud를 사용하지 않고 있다 살펴보니 좋더군요..

 

구글도 아마존도 MS 도 다 비슷하게 1년동안 무료로 제공하고 있으니 

꼭 Azure를 사용할 필요는 없습니다.

 

ms azure는 한달 22만원에 크레딧을 제공하고 있습니다

https://azure.microsoft.com/ko-kr/free/

처음에는 1년에 22만원을 제공하나? 해서 조금씩 써야겠다고 생각했지만.. 매달 지원해주고 있습니다.

 

'Virtual machines'로 우분투 서버를 에 수동으로 App을 배포해서 사용해도 되겠지만

'App Services' 서비스에 배포 하는 방법을 알아보았습니다.

 

이런저런 방법들을 시도해도 잘 되지 않아 괜히 고생했지만 결국 현재 사용하고 있는 방법을 담고 있는 링크는

https://docs.microsoft.com/ko-kr/java/azure/spring-framework/deploy-spring-boot-java-app-with-maven-plugin?view=azure-java-stable

 

를 참고하였습니다.

 

SpringBoot App을 개발후 'Azure App Service'에 배포하기 위해 필요한 내용은 사실

"Azure App Service용 Maven 플러그인 구성" 부터 보면 됩니다.

 

0. azure cli 설치 및 로그인(https://docs.microsoft.com/ko-kr/cli/azure/?view=azure-cli-latest)

0.1 배포시 az 인증이 필요하기 때문에 az login 커맨드를 통해 로그인

 

az cli 설치 하지 않고 배포하는방법은 추후 발견하게되면 첨부 하겠습니다..

 

1. pom.xml 에 azure 플러그인을 추가

<plugin>
 <groupId>com.microsoft.azure</groupId>
 <artifactId>azure-webapp-maven-plugin</artifactId>
 <version>1.6.0</version>
</plugin>

2. mvn azure-webapp:config 수행

2.1 배포할 환경 설정하게 됩니다.(OS, javaVersion 등)

2.2 위 설정 이후 pom.xml에 <configuration> 항목에 설정한 내용들이 추가 되는데, 여기서 <region>이 미국으로 설정되던데 한국으로 바꾸고 싶다면 <region>koreacentral</region> 으로 바꾸시면 됩니다.

2.3 pom.xml에 appName과 resourceGroup 모두 적힌 그대로 배포되기 때문에 필요하면 배포전 수정하시기 바랍니다.

 

3. port 설정을 위해 <appSettings>에 JAVA_OPTS ,-Dserver.port=80 설정

3.1 저는 추가로 profile 설정이 필요해서 SPRING_OPTS 로 설정을 시도해보았지만 안되어 JAVA_OPTS에 -Dserver.port=80 -Dspring.profiles.active=run 설정하여 정상적으로 동작했습니다.

 

 

4. 설정파일은 다 됬고 이제 배포

4.1 mvn clean package

4.2 mvn azure-webapp:deploy

4.3 4.2을 하다가 인증 에러 발생시 0.1 에 로그인 다시 진행

 

5. 배포 완료 

5.1 이제 포탈에서 적혀있는 url로 접속하여 정상적으로 접속 되는지 확인하면 됩니다.

 

Azure 에 'App Services'는 'Virtual machines' 과 달리 

Development Tools 이나 Scale out 과  같은 기능들도 지원되고 있습니다.

 

 

아직은 기본적으로 각 AppService만 띄워 사용중인데 추후 기회가 된다면

제공하고 있는 기능들을 활용 후 정리해두도록 하겠습니다.

 

댓글()

Spring Boot Toy 프로젝트 세션 클러스터링-1

JAVA/Spring|2019. 7. 4. 12:30

현재 라즈베리 파이에 올려놓은 Toy Project는 

앞에는 Nginx(web서버) 뒤에는 SpringBoot(WAS라 해야하나 WebApp이라고 해야하나..) 그리고 mysql로 구성되어있습니다.

 

nginx를 둔 이유는 정적 리소스 처리도 있지만, 버전 업을 위해 Boot를 배포해야하는 경우

Boot를 바로 내리기보다 먼저 신규 Boot를 배포하고 nginx의 설정을 기존 boot에서 신규 boot로 reload하여

DownTime을 제거하는 것이 가장 큰 이유였습니다.

 

현재 Toy프로젝트는 로그인 방식이 아니고, 쿠키를 활용하여 동작하기 때문에 위와같이 사용하더라도

사용자에게 문제가 생길 부분은 없지만, 추후 로그인을 위해 그리고 세션에 데이터를 활용하게 된다면

신규버전 배포시에 기존 boot에서 로그인 한 사용자들은 로그아웃이 되는 예상치 상황을 만날수 있음을 인지했습니다.

 

바로 적용을 할지는 의문이지만, 먼저 찾아보았을때 boot(톰캣)도 세션도 클러스터링이 가능하다는 내용들을 발견하여

추후 적용을 할때 빠르게 접근하기 위해 조금씩 내용을 정리할 예정입니다.

(필요할때는 왜 세션서버와 클러스터링이 생각이 떠오르지 않았는지.. 참..--;;)

 

일단 잠시 찾아봤을때

세션 클러스터링 방법

  1. 세션을 여러 서버들이 접근할 수 있는 곳에 저장한다(File, SQL Server, no SQL Server)
  2. 인스턴스 끼리 클러스터링 한다.
  3. 다른 인스턴스에 공유해둔다.

mysql을 사용중이니 mysql을 활용해도 좋을것 같고,

다른 분들을 봤을때 redis에도 저장해서 활용하시는것 같습니다(성능상 redis가 좋긴 하겠지요).

 

다음 글은 해당 내용 적용기를 작성하는걸로!

댓글()

[SpringBoot] Embedded Tomcat, NioEndpoint

JAVA/Spring|2019. 6. 11. 19:22

SpringBoot Async 처리를 다시 학습 중에

어떻게 Embedded Tomcat에서는 요청을 처리하고 있는지 파악하기 위해 조금 코드를 살펴봤다.

 

 

일단 크게 별다른 설정을 하지 않고 디버그 모드로 붙었을 때 아래와 같이 스레드가 떠있다.

대략 오늘 살펴보려는 스레드는 4가지 정도다

 

  • http-nio-8080-Acceptor-0
    • Http Port accept 하는 스레드(ServerSocket.accept())
    • 관련 클래스는 org.apache.tomcat.util.net.Acceptor
    • accept 되면 getpoller() 후 채널을 regist 한다
      • getpoller는 poller가 여러 개의 경우 rr로 동작한다.
  • http-nio-8080-ClientPoller-0~1
    • Accept 된 Socket을 Selector에 OP_READ를 등록하고 Selector를 epoll 한다.
    • 관련 클래스는 org.apache.tomcat.util.net.NioEndpoint$Poller
    • 요청이 들어오면(read가 불리면), ThreadPoolExecutor.execute를 부르게 되고, 결국에 TaskQueue에 offer()를 부르게 된다.
    • poller가 하나가 아니라서 어떤 기준으로 동작 하나 살펴보니 아래와 같이 값이 설정되어있으며
      • polle는 많아도 2개
      • 기본 동작은 selector는 select()로 blocking이 아닌 select(1000), 1초마다 깨어 난다.

  • http-nio-8080-exec-1~10
    • 실제 ServletThread
    • TaskQueue(BlockingQueue)를 take 하며 대기, poller 혹은 다른 곳에서 TaskQueue에 이벤트를 넣어주면 동작
    • 선후 처리 및 Controller 코드가 보통 이 스레드를 통해 불리게 되며 Async 처리 이후 다시 돌아와(받은 스레드가 대기하여 다시 돌아온다는 이야기는 아니고, ServletThread->Async->ServletThread) 처리
    •  

  • MvcAsync
    • Controller에서 Async 처리(Callable이라던지 기타 등등..) 이후 응답을 직접 하려나? 해서 살펴보니
    • 마지막에 다시 TaskQueue에 offer를 하고 있었다.

 

 

전체 처리를 대략적으로 그리면 아래와 같이 될 것 같다.

 

 

댓글()