spring boot
기반의 spring batch
공부를 할 때 반복적으로 나오는 코드 및 세팅에 대해서 기록하는 글입니다. 기록성 글이기 때문에 자세한 설명을 쓰지 않습니다.
정리 및 요약글은 추후에 spring batch 에 대한 공부가 어느정도 다듬어지면 하겠습니다.
참고로 여기 나오는 코드, 세팅법은 제가 공부하는 인터넷 강의 를 기반으로 작성된 것입니다.
개발 환경
- Window 10
- spring boot (v2.7.10)
- java 17
- maven dependency
- postgresql
- h2
- spring-boot-starter-batch
- lombok
- 그외는 중요하지 않아서 생략합니다...
package me.daily.batch;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class JobParameterConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job job() {
return jobBuilderFactory.get("job")
.start(step1())
//.next(step2())
.build();
}
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
System.out.println("step1 called");
return RepeatStatus.FINISHED;
// default 로 무한 반복이다. 한번 실행하고 끝내도록 하자.
}
}).build();
}
}
spring:
profiles:
active: postgres # Run Configuration 에서 Profile 을 주면 override 됨
---
spring:
config:
activate:
on-profile: local
datasource:
hikari:
jdbc-url: jdbc:h2:mem:testdb
username: sa
password:
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
---
spring:
config:
activate:
on-profile: postgres
datasource:
hikari:
username: postgres
password: root
jdbc-url: jdbc:postgresql://localhost:5432/spring_batch
driver-class-name: org.postgresql.Driver
batch:
jdbc:
initialize-schema: never
# initialize-schema: always
job:
enabled: false # 자동 실행 방지
spring.batch.job.enabled: true
가 default 인데, 이러면 스프링 부트가 자동으로 실행한다는 의미합니다.
자동 실행을 원치 않으면 spring.batch.job.enabled: false
를 주면 되고, 이를 수동 실행하기 위해서는 아래와 같은 코드를 추가적으로 작성해줍니다.
package me.daily.batch;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class JobRunner implements ApplicationRunner {
@Autowired
private JobLauncher jobLauncher;
@Autowired
private Job job;
@Override
public void run(ApplicationArguments args) throws Exception {
JobParameters jobParameters = new JobParametersBuilder().addString("name", "user2")
.toJobParameters();
jobLauncher.run(job, jobParameters);
}
}
이 외에도 initialize-schema: embedded
가 default 이다. 이건 테스트용 메모리 DB 에서 쓰면 유용합니다.
하지만 실제 회사에서 사용하거나, 여러사람들이 같이 쓸 때는 never 로 세팅하고, spring batch 가 필요로하는 Schema 들을 수동으로 생성하는 걸 추천합니다.
주관적인 생각이지만 회사에서는 여러 사람이 같은 코드를 사용하는데, 함부로 애플리케이션이 DB 에 접근해서는 안된다고 생각합니다.
다만 로컬 환경(자기 컴퓨터)에서 postgresql(또는 h2)를 설치하고 테스트하는 것은 당연히 initialize-schema
에 대해서 어떻게 세팅하든 상관없습니다.
참고로 spring-batch
에서 자신의 DBMS 에 맞는 sql schema 를 제공하는데, 해당 내용은 아래와 같이 찾아갈 수 있습니다.
sql schema 위치는 아래와 같습니다.
org.springframework.batch.core.Job
이라는 인터페이스 경로로 갑니다.
해당 경로를 좀만 뒤져보면 아래와 같이 dbms 별 schema sql 찾아낼 수 있습니다.
postgresql 의 경우에는 아래와 같이 나옵니다.
PS C:\Users\dailyCode\spring-batch\target> `
>> java -jar spring-batch-0.0.1-SNAPSHOT.jar --spring.profiles.active=postgres "name=mickeymouse seq(long)=3L date(date)=2023-03-03 age(double)=16.5"
@Configuration
@RequiredArgsConstructor
public class JobParameterConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job job() {
return jobBuilderFactory.get("job")
.start(step1())
.next(step2())
.build();
}
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> {
// 방식 1
JobParameters jobParameters = contribution
.getStepExecution()
.getJobExecution()
.getJobParameters();
jobParameters.getString("name");
jobParameters.getLong("seq");
jobParameters.getDate("date");
jobParameters.getDouble("age");
// 방식 2
Map<String, Object> jobParameters1 = chunkContext.getStepContext().getJobParameters();
System.out.println("step1 called");
return RepeatStatus.FINISHED;
}).build();
}
}
@Component
public class ExecutionContextTasklet1 implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
System.out.println("step 1 was executed");
ExecutionContext jobExecutionContext = contribution.getStepExecution().getJobExecution().getExecutionContext();
ExecutionContext stepExecutionContext = contribution.getStepExecution().getExecutionContext();
String jobName = chunkContext.getStepContext().getStepExecution().getJobExecution().getJobInstance().getJobName();
String stepName = chunkContext.getStepContext().getStepExecution().getStepName();
if (jobExecutionContext.get("jobName") == null) {
jobExecutionContext.put("jobName", jobName);
}
if (stepExecutionContext.get("stepName") == null) {
stepExecutionContext.put("stepName", stepName);
}
System.out.println("jobName : " + jobExecutionContext.get("jobName"));
System.out.println("stepName : " + stepExecutionContext.get("stepName"));
return RepeatStatus.FINISHED;
}
}
package me.dailycode.springbatch;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class TransitionConfiguration {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job batchJob() {
return jobBuilderFactory.get("batchJob")
.start(step1())
.on("FAILED")
.to(step2())
.on("FAILED")
.stop()
.from(step1())
.on("*")
.to(step3())
.next(step4())
.from(step2())
.on("*")
.to(step5())
.end()
.build();
}
/*
@Bean
public Job batchJob() {
return jobBuilderFactory.get("batchJob")
.start(step1())
.on("FAILED")
.to(step2())
.on("*")
.stop()
.from(step1()).on("*")
.to(step3())
.next(step4())
.on("FAILED")
.end()
.end()
.build();
}
*/
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.tasklet(new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
System.out.println(">> step1 has executed");
contribution.setExitStatus(ExitStatus.FAILED);
return RepeatStatus.FINISHED;
}
})
.build();
}
@Bean
public Step step2() {
return stepBuilderFactory.get("step2")
.tasklet((contribution, chunkContext) -> {
System.out.println(">> step2 has executed");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step step3() {
return stepBuilderFactory.get("step3")
.tasklet((contribution, chunkContext) -> {
System.out.println(">> step3 has executed");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step step4() {
return stepBuilderFactory.get("step4")
.tasklet((contribution, chunkContext) -> {
System.out.println(">> step4 has executed");
// throw new RuntimeException("uwu");
return RepeatStatus.FINISHED;
})
.build();
}
@Bean
public Step step5() {
return stepBuilderFactory.get("step5")
.tasklet((contribution, chunkContext) -> {
System.out.println(">> step5 has executed");
return RepeatStatus.FINISHED;
})
.build();
}
}
spring:
profiles:
active: local
---
spring:
config:
activate:
on-profile: local
datasource:
hikari:
jdbc-url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
---
spring:
config:
activate:
on-profile: postgres
datasource:
hikari:
jdbc-url: jdbc:postgresql://localhost:5432/spring_batch
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
batch:
job:
names: ${job.name:NONE}
jdbc:
initialize-schema: always
# enabled: true
# jdbc:
# initialize-schema: always
# table-prefix: SYSTEM_