2022. 6. 23. 23:59ㆍSpring
이번 포스팅에서는 Spring Batch를 제작하면서 마주했던 Error를 기록하고자 한다.
새로운 프로젝트로 Multi Module를 새로 만들어 기존에 작성된 Spring Batch를 멀티 모듈 내부로 옮기는 작업을 진행했다. 레거시 코드도 많았고, 의존성 버전도 낮은 것들이 많았는데 배치까지 잘 모르니 도전의식 엄청 생겼다. ㅎㅎ..
작업 과정 중 마주했던 에러가 정말 많았는데, 그 중 몇 가지 정리해보았다.
ISSUE1.
Job 실행 후 Applicaton이 종료되지 않는 문제
코드 중 JobLauncher를 직접 호출해서 실행시키는 내용이 있었는데, Spring MVC 에 Batch를 사용하고자 하는 코드를 복붙한 것 같아 제거하는 작업을 진행했다.
문제
Job 실행 후 Applicaion이 제대로 종료되지 않는 문제 발생가 발생했다.
해결 시도로는 Spring batch is not shutdown after job 으로 검색해보며 문제를 찾았는데, 몇가지의 해결 방안을 찾아 볼 수 있었다. 몇 가지는 TaskExecutor를 종료시키는 내용이나 System.exit()으로 ApplicationContext의 종료 신호를 받아오면서 종료하라는 것이었다.
TRY 1. ThreadPoolTaskExecutor Shutdown
실행되고 있던 ThreadPoolTaskExecutor를 종료시킨다.
threadPoolTaskExecutor.shutdown();
해결되지 않는다. 무엇보다, 이 TaskExecutor가 종료되어도 어쨌든 Job은 종료됐으니, 프로세스가 종료되어야 할텐데 수동으로 끄는 건 이 문제의 원인이 아니라 TaskExecutor 정도만 학습하는 계기로 마무리했다.
TRY 2. System.exit
System.exit()으로 ApplicationContext에서 받아오는 종료 시그널을 전달한다.
이렇게 설정하면 Jenkins에서도 종료 신호를 받을 수 있다고 한다.
하지만 'TRY 1'과 마찬가지로 근본 원인은 아니다.
Cause & Solution
실행된 Thread 중 아래와 같이 Tomcat Thread가 살아있었다.
- http-nio-8080-Acceptor
- http-nio-8080-Poller
Web thread로 Application이 종료되지 않았던 것이었다.
이전 프로젝트 진행자가 Application Web 기반에 Batch를 추가하는 프로젝트를 복붙해서 추가된 Dependency 이며, 현재는 순수한 Spring Batch 프로젝트이기 때문에 불필요한 Spring Boot Web Dependendy가 존재했다.
기존 프로젝트에 설정되어 있던 아래의 dependency 를 제거하자.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
하면 되는 줄 알았는데 역시 아직도 안된다.
다시 한 번 살아있는 Thread를 보니까 reactor-http-nio-1 이 실행중이었다.
Webflux에서 기본으로 사용되는 스레드이다.
불필요한 의존성을 다 없애자.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.6.3</version>
</dependency>
Result
정상 종료가 되었으며, Web 용 Thread 더 이상 뜨지 않는 걸 알 수 있다.
2022-06-23 14:40:31 [AD01759157] [INFO ] o.s.b.c.l.support.SimpleJobLauncher:152 - Job: [FlowJob: [name=DB_HEALTH_CHECK_JOB]] completed with the following parameters: [{date=1655950962601, dirRemainYn=Y, run.id=26, targetMonth=202203}] and the following status: [COMPLETED] in 187ms
2022-06-23 14:40:31 [AD01759157] [INFO ] com.zaxxer.hikari.HikariDataSource:350 - ...batch-hikari-pool - Shutdown initiated...
2022-06-23 14:40:31 [AD01759157] [INFO ] com.zaxxer.hikari.HikariDataSource:352 - ...batch-hikari-pool - Shutdown completed.
2022-06-23 14:40:31 [AD01759157] [INFO ] com.zaxxer.hikari.HikariDataSource:350 - ...batch-hikari-pool - Shutdown initiated...
2022-06-23 14:40:31 [AD01759157] [INFO ] com.zaxxer.hikari.HikariDataSource:352 - ...batch-hikari-pool - Shutdown completed.
Disconnected from the target VM, address: '127.0.0.1:64976', transport: 'socket'
Process finished with exit code 0
배운 점은 아래와 같다.
- IntelliJ에서 Debug Tool로 살아있는 Thread를 확인할 수 있다.
- TaskExecutor에 대한 대략적인 이해를 할 수 있었다.
- 의존성 관리를 잘하자 ..ㅎㅎ
ISSUE 2.
JobExecutionContext is null
문제
아래와 같이 ContextExecutor를 불러와서 사용하고자 했는데, 해당 객체가 불러와지지 않았다.
// Before Step
ExecutorContext jobExecutionContext = jobExecution().getExecutionContext();
jobExecutionContext.put("targetMonth", "202204");
// and Next!
@Bean
public FlatFileItemReader itemReader(
@Value("#{jobExecutionContext['targetMonth']}") String targetMonth) {
// ...
}
targetMonth를 확인해보니 null로 들어오고 있었다.
Cause & Solution
해당 Bean이 생성될 시점에는 jobExecution에 ExecutionContext에 데이터가 없다.
로드되는 시점의 객체를 주입하기 때문이다.
그래서 Spring Batch는 @JobScope, @StepScope를 지원하여 초기화 시점을 늦춰 Late Binding을 가능하도록 지원한다.
실제 Annotation을 보면 @Scope(Spring Bean의 Singleton)을 가지고 있다.
그렇다면 @JobScope, @StepScope 중에 어떤 것을 사용해야 할까?
각각의 Scope의미는 아래와 같다.
- @JobScope : 해당 Annotation이 등록된 Job이 실행되는 시점에 해당 Bean을 생성
- @StepScope: 해당 Annotation이 등록된 Step이 실행되는 시점에 해당 Bean을 생성
본 내용은 다음 포스팅에서 ExecutionContext, JobParameter를 다룰 때 조금 더 자세히 다루고 싶다.
따라서, 아래와 같이 Scope을 지정하자.
@Bean
@StepScope
public FlatFileItemReader itemReader(
@Value("#{jobExecutionContext['targetMonth']}") String targetMonth) {
// ...
}
이전 Step을 실행하고 난 후 생성되어 주입받을 수 있어야 하기 때문에 StepScope을 적용시켰다.
Result
데이터 들어온당
ISSUE 3.
getVirtualServerName() did not exist
ServletConetxt의 getVirtualServerName()을 찾지 못한다.
***************************
APPLICATION FAILED TO START
***************************
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
org.apache.catalina.authenticator.AuthenticatorBase.startInternal(AuthenticatorBase.java:1318)
The following method did not exist:
'java.lang.String javax.servlet.ServletContext.getVirtualServerName()'
The calling method's class, org.apache.catalina.authenticator.AuthenticatorBase, was loaded from the following location:
jar:file:/C:/Users/gngsn/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.56/tomcat-embed-core-9.0.56.jar!/org/apache/catalina/authenticator/AuthenticatorBase.class
The called method's class, javax.servlet.ServletContext, is available from the following locations:
jar:file:/C:/Users/gngsn/.m2/repository/org/mortbay/jetty/servlet-api/2.5-20081211/servlet-api-2.5-20081211.jar!/javax/servlet/ServletContext.class
jar:file:/C:/Users/gngsn/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.56/tomcat-embed-core-9.0.56.jar!/javax/servlet/ServletContext.class
The called method's class hierarchy was loaded from the following locations:
javax.servlet.ServletContext: file:/C:/Users/gngsn/.m2/repository/org/mortbay/jetty/servlet-api/2.5-20081211/servlet-api-2.5-20081211.jar
Action:
Correct the classpath of your application so that it contains compatible versions of the classes org.apache.catalina.authenticator.AuthenticatorBase and javax.servlet.ServletContext
Disconnected from the target VM, address: '127.0.0.1:57230', transport: 'socket'
Process finished with exit code 5
Cause & Solution
기존에 사용하던 javax.servlet / servlet-api 의 패키지의 버전(2.5)이 너무 낮아서 발생하는 예외였다.
역시 StackOverflow에서 이미 나온 Issue였다.
해당 package를 사용하지 않고 있기 때문에 제외 시켜준다는 솔루션을 줘서 실행해보니 해결된다.
<!-- exclude servlet-api 2.3 jar-->
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client-jetty</artifactId>
<version>1.23.0</version>
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>org.mortbay.jetty</groupId>
</exclusion>
</exclusions>
</dependency>
근데, 이는 문제의 원인이 아니라 에러 막기 용인 것 같다.
버전이 너무 낮은거면 버전업이 장기적으로 좋을 듯하다.
Result
아래와 같이 version up을 시켜주고, 버전업되면서 분리된 dependency를 따로 추가했다.
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client-jetty</artifactId>
<version>${google.project.oauth.version}</version>
</dependency>
<!-- google-http-client-jackson2 추가 -->
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-jackson2</artifactId>
<version>1.42.0</version>
</dependency>
<properties>
<!-- ... -->
<!-- version up 1.23.0 => 1.34.1 -->
<google.project.oauth.version>1.34.1</google.project.oauth.version>
<google.project.api.version>1.35.0</google.project.api.version>
</properties>
ISSUE 4.
@StepScope on @Bean method...
WARN 로그로 아래와 같은 메세지가 뜬다.
org.springframework.batch.item.ItemWriter is an interface.
The implementing class will not be queried for annotation based listener configurations.
If using @StepScope on a @Bean method, be sure to return the implementing class so listener annotations can be used.
해당 메세지의 원인이 되는 코드는 아래와 같다.
@Bean
@StepScope
public ItemWriter<Transaction> staxEventItemWriterBuilder(
Marshaller marshaller,
@Value("#{jobParameters[opFileName]}") String filename) {
return new StaxEventItemWriterBuilder<Transaction>()
.name("staxEventItemWriterBuilder")
.marshaller(marshaller)
.rootTagName("transactionRecord")
.resource(new ClassPathResource("xml/" + filename))
.build();
}
그리고 아래와 같은 오류가 뜬다.
org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to
Writer가 Write를 수행하기 전에 Open되어야 한다는 내용이다.
Cause & Solution
위의 에러를 해석해보면 아래와 같다.
org.springframework.Item.ItemWriter는 인터페이스이며, 해당 인터페이스의 구현체가 Listener 설정에 대한 질의를 못할 수도 있다. @Bean 메서드에서 @StepScope를 사용하는 경우에는 Listener Annotation 사용할 수 있도록 타입 명시를 제대로 해줘야 한다.
즉, 인터페이스말고 구현체를 return에 명시하라는 의미이다. ItemWriter에 @StepScope를 달았는데, @StepScope로 지정된 @Bean 의 반환 타입이 Interface라면 위와 같은 경고메시지가 발생한다.
이렇게 되면 구현체를 직접 return 받아 불러오면 문제가 없겠지만,
자동 주입을 사용할 경우에는 문제가 생길 수 있기 때문이었다.
@Bean
@StepScope
public StaxEventItemWriter<Transaction> staxEventItemWriterBuilder(
Marshaller marshaller,
@Value("#{jobParameters[opFileName]}") String filename) {
return new StaxEventItemWriterBuilder<Transaction>()
.name("staxEventItemWriterBuilder")
.marshaller(marshaller)
.rootTagName("transactionRecord")
.resource(new ClassPathResource("xml/" + filename))
.build();
}
위와 같이 StaxEventItemWriter로 명시해주어 해결할 수 있다.
Result
반환 타입 명시를 제대로 해주거나, 직접 값을 return 받아오면 된다.
'Spring' 카테고리의 다른 글
Spring Batch, 제대로 이해하기 (4) - 데이터 처리 활용 (2) | 2022.07.01 |
---|---|
JdbcBatchItemWriter VS MyBatisBatchItemWriter (0) | 2022.06.26 |
Spring Batch, 제대로 이해하기 (3) - Meta Data (0) | 2022.06.22 |
Spring Batch, 제대로 이해하기 (2) - 동작원리 (4) | 2022.06.19 |
Spring Batch, 제대로 이해하기 (1) - 개념이해 (0) | 2022.06.18 |
Backend Software Engineer
𝐒𝐮𝐧 · 𝙂𝙮𝙚𝙤𝙣𝙜𝙨𝙪𝙣 𝙋𝙖𝙧𝙠