코드로 보실 분?
서문
배치작업에 대해 학습하라고 하신 팀장님의 명으로 열심히 알아보던 중... 그만 이직에 성공해버리고 말아 중간에 그만하게 되었는데... 많이는 아니지만! 지금까지 작성해 놓은 코드나 내용이 아까워서 그마저라도 적어놓자 싶어 작성하는 spring batch에 대한 간략 정리
(진짜 간략정리니까 심화 내용은 다른 블로그를 보자.)
batch란? => 한 번에 처리할 수 없는 양을 쪼개서 처리하는 행위
spring framework에서 지원하는 batch => spring batch
spring batch에서의 용어
job
- 처리하는 일, 여러 "step"으로 나눠서 처리하는 인터페이스
step
- job 하위의 단계별로 일을 처리하는 인터페이스
- step은 실제 일의 처리하는 tasklet / chunk 형식의 인터페이스를 인자로 받아 처리
a. tasklet
- 업무 단위, 쪼개진 일을 실제로 처리하는 인터페이스
- chunk와 다르게 데이터를 한 번에 처리, execute 함수를 구현해 사용
b. chunk
- 업무 단위, 쪼개진 일을 실제로 처리하는 인터페이스
- tasklet과 다르게 데이터를 읽고, 처리하고, 쓰는 인터페이스를 받아서 처리 (reader, processor, writer)
쉽게는 아래와 같이 구성된다고 생각하면 된다.
job
- 1번째 step
- tasklet
- 2번째 step
- chunk
- reader -> processor -> writer
- chunk
- ... N번째 step
사용하려면 pom.xml 에 해당 내용 추가
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring batch 세팅 시,
batch 프레임워크의 작동 상태, 작업 성공/실패 여부, 작업별 인자를 기록하는 table이 생성됨
자세한 내용은 구글에 "spring batch meta table" 검색!
위 적어놓은 사항을 코드로 살펴보자.
풀코드
package com.example.testDB.config;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableAutoConfiguration
@EnableBatchProcessing
public class PrintHelloConfig extends DefaultBatchConfigurer {
@Bean(name = "helloJob")
public Job printHelloJob() {
return new JobBuilder("helloJob")
.repository(getJobRepository())
.start(printHelloStep(getJobRepository(), getTransactionManager()))
.build();
}
@Bean
@JobScope
public Step printHelloStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("helloStep")
.repository(jobRepository)
.transactionManager(transactionManager)
.tasklet(helloTask())
.build();
}
@Bean
@StepScope
public Tasklet helloTask() {
return (stepContribution, chunkContext) -> {
System.out.println("hello!");
return RepeatStatus.FINISHED;
};
}
}
우선 tasklet 단위의 job
@Configuration
@EnableAutoConfiguration
@EnableBatchProcessing
public class PrintHelloConfig extends DefaultBatchConfigurer {
@Bean(name = "helloJob")
public Job printHelloJob() {
return new JobBuilder("helloJob")
.repository(getJobRepository())
.start(printHelloStep(getJobRepository(), getTransactionManager()))
.build();
}
...
}
Config 클래스를 구성하고 해당 클래스의 메서드로 job을 생성해 bean으로 등록하고 있다.
jobBuilder를 통해 repository, step을 설정해 job을 반환해 준다.
(※ JobRepository, transactionManager 등은 defaultBatchConfigurer를 상속받아 사용…
jobBuilderFactory 사용 대신 jobBuilder 사용 시 repository, transactionManager를 받아 사용해야 함…
repository와 transactionManager는 별다른 설정이 없을 경우, 사용자가 적은 application.yml 또는 application.properties 정보를 사용하는 걸로 알고 있음…)
job을 설정했으니 이제 하위의 step
@Bean
@JobScope
public Step printHelloStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("helloStep")
.repository(jobRepository)
.transactionManager(transactionManager)
.tasklet(helloTask())
.build();
}
step의 어노테이션을 보면 JobScope라고 적혀있는데 bean 어노테이션 설정 시 스프링 실행 시 프레임워크가 관리하는 singleton 객체로 관리되는데 이때, 실행시 띄우는 게 아니라 해당 step을 사용하는 Job 실행 시에만 띄운다는 의미이다. step 역시 tasklet이라는 작업 단위를 받아 동작한다.
step을 설정했으니 하위의 tasklet을 작성
@Bean
@StepScope
public Tasklet helloTask() {
return (stepContribution, chunkContext) -> {
System.out.println("hello!");
return RepeatStatus.FINISHED;
};
}
tasklet은 stepContribution과 chunkContext를 받아 상태값을 리턴한다.
- stepContribution => step의 현재 진행상황? 을 가지고 있는 객체
- chunkContext => 현재 실행 중인 chunk 정보(job, step에 대한 정보 포함)를 가지고 있는 객체
작성한 job을 테스트해보자.
@RunWith(SpringRunner.class)
@SpringBatchTest
@SpringBootTest(classes = {PrintHelloConfig.class})
@ContextConfiguration(classes = {PrintHelloConfig.class})
@TestPropertySource(locations = "classpath:application_test.properties")
class PrintHelloConfigTest {
@Autowired
JobLauncherTestUtils jobLauncherTestUtils;
@Autowired
@Qualifier("helloJob")
Job job;
@Test
public void helloTest() throws Exception {
jobLauncherTestUtils.setJob(job);
JobParameters jobParameters = this.jobLauncherTestUtils.getUniqueJobParameters();
JobExecution jobExecution = this.jobLauncherTestUtils.launchJob(jobParameters);
Assert.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
}
}
우선 어노테이션을 보자 (나는 아래와 같이 설정했는데… 틀린 게 있다면… 진짜로 사용할 때 고쳐야지..)
spring을 테스트하기 위해 RunWith, SpringBootTest
batch 테스트임을 알리고 적용하기 위해 SpringBatchTest
특정 job에 대해서만 실행하기 위해서 ContextConfiguration
test시 특정 properties 적용하기 위한 TestPropertySource
이렇게 설정해 준다.
@RunWith(SpringRunner.class)
@SpringBatchTest
@SpringBootTest(classes = {PrintHelloConfig.class})
@ContextConfiguration(classes = {PrintHelloConfig.class})
@TestPropertySource(locations = "classpath:application_test.properties")
@RunWith(SpringRunner.class)
@SpringBatchTest
@SpringBootTest(classes = {PrintHelloConfig.class})
@ContextConfiguration(classes = {PrintHelloConfig.class})
@TestPropertySource(locations = "classpath:application_test.properties")
class PrintHelloConfigTest {
@Autowired
JobLauncherTestUtils jobLauncherTestUtils;
@Autowired
@Qualifier("helloJob")
Job job;
@Test
public void helloTest() throws Exception {
jobLauncherTestUtils.setJob(job);
JobParameters jobParameters = this.jobLauncherTestUtils.getUniqueJobParameters();
JobExecution jobExecution = this.jobLauncherTestUtils.launchJob(jobParameters);
Assert.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
}
}
test 어노테이션이 붙은 메서드를 확인해 보면 jobLauncherTestUtils의 job을 설정해 주고
jobPrameter를 설정해 준다.
parameter를 설정해주지 않으면 한번 실행 후엔 동일한 Job이라 실행하지 않는다…
실행하면 아래와 같은 결과가 나온다.
2024-09-16 17:15:06.426 INFO 82504 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=helloJob]] launched with the following parameters: [{random=-5389004981438232706}]
2024-09-16 17:15:06.432 INFO 82504 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [helloStep]
hello!
2024-09-16 17:15:06.436 INFO 82504 --- [ main] o.s.batch.core.step.AbstractStep : Step: [helloStep] executed in 4ms
2024-09-16 17:15:06.438 INFO 82504 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=helloJob]] completed with the following parameters: [{random=-5389004981438232706}] and the following status: [COMPLETED] in 10ms
보통 batch 작업은 일괄처리를 위해 동작하고
나는 주로 DB read → process → DB write 작업을 할 예정이었기에 tasklet예제가 아니라 chunk예제를 구성했다.
기본적인 reader, processor, writer에 대한 틀은 아래와 같다.
@Configuration
@EnableAutoConfiguration
@EnableBatchProcessing
public class ChunkConfig extends DefaultBatchConfigurer{
@Bean(name="chunkJob")
public Job chunkJob() {
return new JobBuilder("chunkJobTest")
.repository(getJobRepository())
.start(chunkStep(getJobRepository(), getTransactionManager()))
.build();
}
@Bean
@JobScope
public Step chunkStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("chunkStepTest")
.repository(jobRepository)
.transactionManager(transactionManager)
.<String, String>chunk(1)
.reader(reader())
.processor(processor())
.writer(writer())
.build();
}
@Bean
@StepScope
public ItemReader<String> reader() {
return new ListItemReader<>(new ArrayList<>(Arrays.asList("apple","banana","carrot")));
}
@Bean
@StepScope
public ItemProcessor<String, String> processor() {
return new ItemProcessor<String, String>() {
@Override
public String process(String s) throws Exception {
return s.toUpperCase();
}
};
}
@Bean
@StepScope
public ItemWriter<String> writer() {
return new ItemWriter<String>() {
@Override
public void write(List<? extends String> list) throws Exception {
list.forEach(System.out::println);
}
};
}
}
위 tasklet방식과 차이가 보이는지…?
우선 step 부분을 자세히 보자.
@Bean
@JobScope
public Step chunkStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("chunkStepTest")
.repository(jobRepository)
.transactionManager(transactionManager)
.<String, String>chunk(1) // <- 1
.reader(reader()) // <- 2
.processor(processor()) // <- 2
.writer(writer()) // <- 2
.build();
}
- chunk 기반은 어떤 형식의 데이터가 어떤 형식의 데이터로 변환될지 명시 및 데이터 처리 단위를 명시하게 되어 있다.
(ex. String → String 형식으로 변할 거고, 처리 단위는 1개씩 해줘! 의 의미) - reader, processor, writer의 처리를 위해 각 인터페이스를 받고 있다.
그림으로 보면 아래와 같다.
예제를 위해 단순히 구현했지만 Reader → ItemReader → ListItermReader의 구현체로 reader를 구성했다.
@Bean
@StepScope
public ItemReader<String> reader() {
return new ListItemReader<>(new ArrayList<>(Arrays.asList("apple","banana","carrot")));
}
processor는 앞서 정의한 chunk에서 처럼 String → String 형식으로 변환하는 로직을 담당한다.
ItemProcessor 인터페이스를 구현하면 되는데 예제니까 단순히 String문자열을 대문자로 고쳐주는 로직을 구성했다.
@Bean
@StepScope
public ItemProcessor<String, String> processor() {
return new ItemProcessor<String, String>() {
@Override
public String process(String s) throws Exception {
return s.toUpperCase();
}
};
}
writer는 처리된 데이터를 소비하는 인터페이스를 구현해야 하고, 여기선 예제로 단순히 해당 문자열을 콘솔상에 출력하게끔 구현했다.
테스트하면 아래와 같이 출력된다.
@Bean
@StepScope
public ItemWriter<String> writer() {
return new ItemWriter<String>() {
@Override
public void write(List<? extends String> list) throws Exception {
list.forEach(System.out::println);
}
};
}
결과는 아래와 같다.
2024-09-16 19:19:41.426 INFO 83111 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=chunkJobTest]] launched with the following parameters: [{random=-1468195155979933575}]
2024-09-16 19:19:41.428 WARN 83111 --- [ main] o.s.b.c.l.AbstractListenerFactoryBean : org.springframework.batch.item.ItemReader 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.
2024-09-16 19:19:41.428 WARN 83111 --- [ main] o.s.b.c.l.AbstractListenerFactoryBean : 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.
2024-09-16 19:19:41.428 WARN 83111 --- [ main] o.s.b.c.l.AbstractListenerFactoryBean : org.springframework.batch.item.ItemProcessor 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.
2024-09-16 19:19:41.432 INFO 83111 --- [ main] o.s.batch.core.job.SimpleStepHandler : Executing step: [chunkStepTest]
APPLE
BANANA
CARROT
2024-09-16 19:19:41.440 INFO 83111 --- [ main] o.s.batch.core.step.AbstractStep : Step: [chunkStepTest] executed in 8ms
2024-09-16 19:19:41.444 INFO 83111 --- [ main] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=chunkJobTest]] completed with the following parameters: [{random=-1468195155979933575}] and the following status: [COMPLETED] in 15ms
이제 실제 DB를 읽고 쓰는 Job을 만들어보자.
우리 회사 (이젠 전회사가 된..)는 Jpa로 사용하는 게 아닌 MyBatis로 구현해 놨었어서 예제도 MyBatis로 구현했다.
로직은 앞서 만들어 놓은 String → String은 변함없고, 단지 DB에서 읽고, 쓰고 한다는 점만 변한다.
우선 전체 코드를 적고 부분 부분 살펴보자.
풀코드
@Configuration
@EnableAutoConfiguration
@EnableBatchProcessing
@RequiredArgsConstructor
public class DBChunkConfig extends DefaultBatchConfigurer{
private final int chunkSize = 3;
private final DataSource dataSource;
private final ApplicationContext applicationContext;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:sqlMap/**/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean(name="dbChunkJob")
public Job dbChunkJob() throws Exception {
return new JobBuilder("chunkJobTest")
.repository(getJobRepository())
.start(dbChunkStep(getJobRepository(), getTransactionManager()))
.build();
}
@Bean
@JobScope
public Step dbChunkStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) throws Exception {
return new StepBuilder("chunkStepTest")
.repository(jobRepository)
.transactionManager(transactionManager)
.<Map<String, Object>, String>chunk(chunkSize)
.reader(dbreader())
.processor(dbprocessor())
.writer(mydbwriter())
.build();
}
@Bean
@StepScope
public MyBatisPagingItemReader<Map<String, Object>> dbreader() throws Exception {
return new MyBatisPagingItemReaderBuilder<Map<String, Object>>()
.pageSize(chunkSize)
.sqlSessionFactory(sqlSessionFactory())
.queryId("com.example.testDB.mapper.PlainTextMapper.getPaging")
//.parameterValues(parameterValues)
.build();
}
@Bean
@StepScope
public ItemProcessor<Map<String, Object>, String> dbprocessor() {
return new ItemProcessor<Map<String, Object>, String>() {
@Override
public String process(Map<String, Object> item) throws Exception {
return item.containsKey("text") ? item.get("text").toString().toUpperCase() : "";
}
};
}
@Bean
@StepScope
public MyBatisBatchItemWriter<String> mydbwriter() throws Exception {
return new MyBatisBatchItemWriterBuilder<String>()
.sqlSessionFactory(sqlSessionFactory())
.statementId("com.example.testDB.mapper.ResultTextMapper.putText")
.build();
}
}
이번엔 선언되는 변수가 기존 것과 다르기에 앞에서부터 살펴보자면
public class DBChunkConfig extends DefaultBatchConfigurer{
private final int chunkSize = 3;
private final DataSource dataSource;
private final ApplicationContext applicationContext;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:sqlMap/**/*.xml"));
return sqlSessionFactoryBean.getObject();
}
...
}
Mybatis를 사용하려면 DataSource와 함께 SqlSessionFactory를 설정해줘야 한다. 나중에
MyBatisItemReader, MyBatisItemWriter를 만들 때 SqlSessionFactory를 요구하기 때문이다.
중요한 건 sql이 저장된 .xml 파일의 경로를 지정해 주는 것이다. (reader와 writer에서 해당 xml을 참조해 쿼리를 날리기 때문!)
chunkSize도 메서드 내부가 아니라 Config 변수로 선언해주고 있다.
Step에서 선언뿐 아니라 Reader에도 pageSize를 설정해줘야 하기 때문에 Final변수로 한 번에 정의해주고 있다.
reader를 살펴보자.
@Bean
@StepScope
public MyBatisPagingItemReader<Map<String, Object>> dbreader() throws Exception {
return new MyBatisPagingItemReaderBuilder<Map<String, Object>>() // <- 1
.pageSize(chunkSize) // <- 2
.sqlSessionFactory(sqlSessionFactory()) // <- 3
.queryId("com.example.testDB.mapper.PlainTextMapper.getPaging") // <-4
//.parameterValues(parameterValues)
.build();
}
- MybatisItemReaderBuilder를 선언하고 (물론 어떤 자료형인지 중요하겠쟈?)
- chunkSize에 맞춰 가져온다고 PageSize를 넣어주고,
- 앞서 Bean으로 구성한 SqlSessionFactory를 넣어준다.
- 이제 MyBatis에서 사용할 mapper의 sql을 세팅! (parameter 가 있다면 설정해 주고…)
참고로 sql은 아래와 같다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.testDB.mapper.PlainTextMapper">
<select id="getPaging" resultType="map">
SELECT * FROM plain_text WHERE 1=1 order by id LIMIT #{_skiprows}, #{_pagesize} ;
</select>
</mapper>
살펴보면 paging 처리를 위해 skiprows, pagesize를 입력받게 되어 있는데 이건 batch에서 알아서 넣어준다.
processor는 기존과 같은 로직이기 때문에 패스!
writer 역시 reader와 마찬가지로 sqlSessionFactory 설정, sql 설정해 주면 된다.
@Bean
@StepScope
public MyBatisBatchItemWriter<String> mydbwriter() throws Exception {
return new MyBatisBatchItemWriterBuilder<String>()
.sqlSessionFactory(sqlSessionFactory())
.statementId("com.example.testDB.mapper.ResultTextMapper.putText")
.build();
}
mapper xml은 아래와 같다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.testDB.mapper.ResultTextMapper">
<insert id="putText" parameterType="map">
INSERT INTO result_text (text) values (#{text}) ;
</insert>
</mapper>
결과는 DB -> DB 이기에 table 결과로 대신한다.
plain_text table
result_text table
대문자로 잘 변할걸 확인할 수 있다!
풀 코드는 아래 깃에서 확인하면 된다.
https://github.com/hoonzinope/springbatch-case-test
아래는 번외로 내가 구성하면서 만났던 오류들에 대한 처리 방법을 적었다.
1. 실제 사용 db와 test db 구분은 어떻게?
application.properties 와 application_test.properties로 구분 및
@Test 시 아래와 같이 어노테이션으로 명시
@TestPropertySource(locations = "classpath:application_test.properties")
2.eclipse 사용할 때 @SpringBootTest 만으로 테스트가 안 돌 때 아래 어노테이션 붙여서 사용
@RunWith(SpringRunner.class)
3.test시 or 구동 시 batch에서 사용되는 테이블이 없다고 나오는 오류시
(ex. batch_job_instance' doesn't exist)
application.properties에 아래 설정 추가
spring.batch.jdbc.initialize-schema=always
4.properties가 여러 개일때(연결해야 하는 DB가 여러개 일 때)
No qualifying bean of type 'javax.sql.DataSource' available 에러 발생
1) 첫 번째 해결방안
//XXX_jobConfig.java 에 아래 annotation 추가
@EnableAutoConfiguration
해당 방안으로 해결 시 application_test.properties를 참조하는 게 아니라서! test DB로 설정한 h2에 적재 x
2) 두 번째
h2를 in-memory로 사용할 경우, 추가해 줘야 하는 것이 2개 있다.
. properties 파일
#application_test.properties
spring.jpa.database=h2
spring.jpa.hibernate.ddl-auto=none
resource/schema.sql을 생성하고, 테이블 생성문을 적어준다.
3) 세 번째
DefaultBatchConfiguer를 상속해 준다.
public class DBChunkConfig extends DefaultBatchConfigurer{
private final int chunkSize = 3;
private final DataSource dataSource;
private final ApplicationContext applicationContext;
...
}
5.JobBuilderFactory가 autowired로 불러와지지 않는 경우, version 4 → 5로 바뀐 경우라
JobBuilderFactory → JobBuilder
StepBuilderFactory → StepBuilder로 변경해서 코드를 고쳐준다.
@Bean
public Job printHelloJob() {
return new JobBuilder("helloJob")
.start(printHelloStep())
.build();
}
@Bean
@JobScope
public Step printHelloStep() {
return new StepBuilder("helloStep")
.tasklet((stepContribution, chunkContext) -> {
System.out.println("hello!");
return RepeatStatus.FINISHED;
}).build();
}
6.springboot test시 test 이외에 job이 도는걸 콘솔창 확인 (?? 왜 한 번씩 더 돌지?)
이유는 스프링 실행 시 job이 돌게끔 기본으로 세팅되어 있기 때문에 아래의 설정이
application.properties에 명시되어 있어야 한다.
#application.properties
spring.batch.job.enabled=false
7.JobExecutionAlreadyRunningException: A job execution for this job is already running 에러
중간에 한번 에러가 나서 내가 실행을 중지시킨 적이 있는데 그렇게 종료되면 batch meta table에 종료되지 않고 실행 중이라고 적힌 상태로 끝나게 된다. 같은 job에 대해서 완료가 되지 않은 job에 대해 처리가 필요하다고 오류가 뜨게 되는데
batch_job_execution 테이블과 batch_step_execution 테이블의 Status 값을 변경해 줘서 해결했다.
UPDATE
BATCH_JOB_EXECUTION
SET END_TIME = now(),
STATUS = 'FAILED',
EXIT_CODE = 'FAILED'
WHERE JOB_EXECUTION_ID = ${id}
;
UPDATE
BATCH_STEP_EXECUTION
SET END_TIME = now(),
STATUS = 'FAILED',
EXIT_CODE = 'FAILED'
WHERE JOB_EXECUTION_ID = ${id}
;
해결법 출처 : https://yeonyeon.tistory.com/317
위에 적은 출처 이외에 내가 참고한 링크들은 아래와 같다.
ref)
- spring batch 이해하기 https://khj93.tistory.com/entry/Spring-Batch란-이해하고-사용하기
- MyBatisItemWriter 사용법 https://oingdaddy.tistory.com/362
- MyBatis paging 처리 https://m.blog.naver.com/zzzinnni_/221936296468
- batch test code 작성 https://velog.io/@joon6093/공부정리-Spring-Batch-5-에서-Job-테스트-코드-작성
- MyBatis + spring batch 설정 방법 https://atoz-develop.tistory.com/entry/Spring-Boot-MyBatis-설정-방법
'text > Java' 카테고리의 다른 글
http API server (w/o spring) (0) | 2024.09.28 |
---|---|
springboot 에 redis 세션 서버 사용 (1) | 2024.09.01 |
short url (0) | 2024.06.14 |
java - mybatis 사용 (no spring) (1) | 2024.05.30 |
검색 자동완성 with mysql function (5) | 2024.04.07 |
댓글