최근에 회사에서 스프링배치를 사용하는 배치업무를 담당하고 있습니다.
간략하게 JobParameter를 수정할 내용이 있었는데 이를 추가할 때마다 모든 배치 소스를 수정해야 하는 불편함을 알게 되었습니다.(아래 적용하려다가 괜히 일이 커지긴 했다...)
해당 글에서는 이 JobParameter 를 조금 더 활용하는 법과 별도의 클래스(Class)로 만들어 공통으로 관리하고 빈(Bean)으로 등록해서 사용하는 것을 포스팅해보려고 합니다.
해당 글에서 사용된 소스는 Git 에서 확인하실 수 있습니다.
1. 기존 방식
- 기존에서는 JobParameter 를 사용하기 위해 아래와 같은 형태로 작성하였습니다.
1-1. 샘플코드
@Slf4j
@Configuration
@RequiredArgsConstructor
public class JobParameterTestBatchJob {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final EntityManagerFactory entityManagerFactory;
@Bean
public Job simpleJob() {
return jobBuilderFactory.get("JobParameterTestBatchJob")
.start(simpleStep1())
.build();
}
@Bean(name = "jobParameterTestStep")
@JobScope
public Step simpleStep1() {
return stepBuilderFactory.get("simpleStep1")
.<Member, Member>chunk(5)
.reader(reader(null, null)) // (1)
.processor(processor(null))
.writer(writer())
.build();
}
@Bean
@StepScope
public JpaPagingItemReader<Member> reader(
@Value("#{jobParameters[param]}") String param,
@Value("#{jobParameters[date]}") String date) { // (2)
log.info("ItemReader 에서는 데이터를 불러오는 로직 작성");
log.info("Param : " + param);
log.info("Date: " + date);
Map<String, Object> parameters = new HashMap<>();
//parameters.put("date", date); // (3-1) Error 발생 Type Error
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
parameters.put("date", LocalDate.parse(date, formatter)); // (3-2) 정상 조회
return new JpaPagingItemReaderBuilder<Member>()
.name("JobParameterTestReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(5)
.queryString("SELECT m FROM Member m WHERE m.createDate = :date")
.parameterValues(parameters)
.build();
}
@Bean
@StepScope
public ItemProcessor<Member, Member> processor(@Value("#{jobParameters[param]}") String param) {
log.info("ItemProcessor 에서는 데이터를 처리 하는 로직 작성");
return item -> item;
}
@Bean
public ItemWriter<Member> writer() {
log.info("ItemWriter 에서는 DB 저장과 같은 Transactional 한 로직 작성");
return items -> {
for(Member member: items) {
log.info("Member Info : " + member.toString());
}
};
}
}
1-2. JobParameter 사용 방식
- Batch 가 수행될 때 외부에서 JobParameter 를 세팅하여 해당 값을 꺼내어 사용합니다.
- reader(null, null)
- Reader 에서 JobParameter를 사용하기 위해 Step에서 null을 임시값으로 강제 세팅. - @Value("#{jobParameters[date]}") String date
- 실제 reader 메서드에서는 null 이 전달되어도 @Value("#{jobParameters[date]}") String date로 인해 JobParamter 의 값으로 교체되어 사용. - parameters.put("date", LocalDate.parse(date, formatter));
- 전달받은 JobParameter 값을 통해 로직 수행.
- JobParameter 는 String, Boolean, Integer, Date 만 타입으로 사용할 수 있습니다.
- 이로 인해 만약 Spring Batch에서 LocalDate를 사용하기 위해서는 위에서와 같이 형변환 작업이 필요합니다.
- 이런 방식에는 아래와 같은 단점들이 발생할 수 있습니다.
- 여러 배치가 있다면 LocalDate 로 바꾸는 코드를 매번 작성하는 중복이 발생한다.
- processor, writer 에서는 변환된 LocalDate 값을 사용할 수 없다.(다시 형변환 작업 필요)
- 확장성이 떨어집니다.(각 배치에서 원하는 로직이 필요하다)
- reader(null, null), processor(null) 를 살펴보면 null을 인자로 넘기는데, 처음 보는 사람은 NPE 발생을 우려할 수 있다.
2. JobParamter 활용
- 위에 나온 단점들에 대한 대안으로 JobParamter 를 따로 가지는 클래스(Class)를 만들고, 해당 클래스를 빈(Bean)으로 등록하여 사용하는 것입니다.
2-1. 샘플코드
- JobParameter를 담을 클래스에서 실제 JobParameter 값을 주입받는 방법에는 Setter / Constructor / Field 3 가지 방법이 있습니다.
- 자세한 내용은 이곳을 통해 참고해주시면 됩니다.
- 해당 글에서는 Setter 방식을 사용하였습니다.
@Getter
public class CustomJobParameter {
@Value("#{jobParameters[param]}") // (1)
private String param;
private LocalDate date;
@Value("#{jobParameters[date]}") // (2)
public void setDate(String date) {
if(!ObjectUtils.isEmpty(date)) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
this.date = LocalDate.parse(date, formatter);
}
}
}
- @Value("#{jobParameters[param]}")
- 해당 파라미터는 따로 로직이 필요 없기 때문에 setter를 사용하지 않고 주입이 가능합니다.
- Enum, Long, String의 타입은 직접 필드로 받아도 형변환이 가능합니다. - @Value("#{jobParameters[date]}")
- LocalDate와 같이 자동 형변환이 안 되는 경우에는 Setter 메서드에 @Value를 선언하여 문자열(String)로 받아 후처리(형변환)를 합니다.
- Null 체크를 무조건 진행합니다.
- 빈(Bean)으로 등록되기 때문에 CustomJobParameter에서 date(LocalDate 형변환)가 필요 없는 Job을 실행할 때는 해당 값을 전달하지 않을 것이고 그로 인해 NPE 가 발생할 수 있습니다.
- 이렇게 만든 클래스를 사용하게 되면 Batch 수행 클래스에서는 Bean을 등록하여 사용하게 됩니다.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class JobParameterTestBatchJob {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final EntityManagerFactory entityManagerFactory;
// (1-1) customJobParameter 의존성 주입
private final CustomJobParameter customJobParameter;
@Bean("JobParameterTestJobParameter")
@JobScope (1-2)
public CustomJobParameter jobParameter() {
return new CustomJobParameter();
}
@Bean
public Job simpleJob() {
return jobBuilderFactory.get("JobParameterTestBatchJob")
.start(simpleStep1())
.build();
}
@Bean(name = "jobParameterTestStep")
@JobScope
public Step simpleStep1() {
return stepBuilderFactory.get("simpleStep1")
.<Member, Member>chunk(5)
.reader(reader()) // (2) null 로 임의의 값 세팅하는 부분 생략가능
.processor(processor())
.writer(writer())
.build();
}
@Bean
@StepScope
public JpaPagingItemReader<Member> reader() {
log.info("ItemReader 에서는 데이터를 불러오는 로직 작성");
log.info("Param : " + customJobParameter.getParam());
log.info("Date: " + customJobParameter.getDate());
Map<String, Object> parameters = new HashMap<>();
parameters.put("date", customJobParameter.getDate()); // (3) customJobParameter 에서 바로 꺼내어 사용 가능
return new JpaPagingItemReaderBuilder<Member>()
.name("JobParameterTestReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(5)
.queryString("SELECT m FROM Member m WHERE m.createDate = :date")
.parameterValues(parameters)
.build();
}
...
// processor(), writer() 는 동일
}
- 의존성 주입
1-1) private final CustomJobParameter customJobParameter;
- JobParameter를 관리하는 CustomJobParameter 클래스를 Bean 주입을 받습니다.
- 내부 필드로 선언하였기 때문에 해당 클래스에서는 어느 곳이든 편하게 사용할 수 있습니다.
1-2) @JobScope public CustomJobParameter jobParameter()
- JobParamter의 @Value를 받기 위해서는 @JobScope, @StepScope 가 꼭 필요합니다.
- @Component에도 Scope 어노테이션을 사용할 수 있습니다.
- @Bean으로 선언할 경우 Bean 생성에 대한 여러 가지 옵션을 사용할 수 있는 장점이 있습니다. - reader()
- CustomJobParameter를 통해 내부 필드로 선언이 되어 임의로 Null 세팅을 하지 않아도 됩니다. - customJobParameter.getDate()
- 해당 클래스 내부에 있는 customJobParamter 변수를 Reader 메서드뿐만 아니라 모든 곳에서 바로 사용 가능합니다.
- 형변환은 CustomJobParameter에서 생성 시점에 형변환이 되었기 때문에 바로 사용할 수 있습니다.
2-2. 여러 Job에서 CustomJobParamter 사용
- 위와 같은 방식으로는 1개의 Job 클래스에서만 사용이 가능합니다.
- 다른 Job 클래스에서 사용하고자 한다면 아래와 같은 오류가 발생합니다.
2개 이상의 배치 Job에서 CustomJobParameter를 위와 같은 방식으로 선언하여 사용하는 경우 발생
Parameter 0 of constructor in com.study.springbatch.controller.TestController required a single bean, but 2 were found:
- simpleJob: defined by method 'simpleJob' in class path resource [com/study/springbatch/job/JobParameterTestBatchJob.class]
- customJob: defined by method 'customJob' in class path resource [com/study/springbatch/job/MultiCustomJobParameterBatchJob.class]
- 2개의 Job(SimpleJob, CustomJob)에서 CustomJobParameter 의존 관계를 필요로 합니다.
- 이때, 두 곳에서 Bean 이 띄어지면서 타입이 동일한 Bean 이 여러 개가 스프링 컨테이너에 등록됩니다.
- 스프링에서는 어떠한 Bean을 주입받을지 알 수가 없어 오류가 발생하게 됩니다.
타입은 동일하더라도 Bean 이름이 다르기 때문에 등록 자체로는 문제가 없습니다.
- 제일 간단하게는 @Qualifier 를 선언해서 각 job에 해당 Bean 이름을 명시해서 주입받으면 됩니다.
- 하지만 위와 같은 방식은 job 이 n 개이면 n 개만큼의 Bean 이 등록되게 됩니다.
3. CustomJobParameter 공통 관리하기
- CustomJobParameter를 관리하는 Config 클래스를 생성하여 Bean을 등록하고 관리를 합니다.
@Configuration(proxyBeanMethods = false)
public class CustomJobParameterConfig {
@Bean
@JobScope
public CustomJobParameter createJobParameter() {
return new CustomJobParameter();
}
}
- proxyBeanMethods = false는 @Bean 메서드를 호출할 때 의도적으로 매번 다른 객체가 생성되도록 합니다.
- 위 설정을 줌으로써 여러 배치에서 CustomJobParameter를 사용할 수 있게 됩니다.
- 해당 클래스를 추가함으로써 각 배치 클래스에서 CustomJobParameter 생성하는 로직을 제거합니다.
private final CustomJobParameter customJobParameter;
// CustomJobParameterConfig 적용 후
// @Bean("JobParameter")
// @JobScope
// public CustomJobParameter createJobParameter() {
// return new CustomJobParameter();
// }
- 이후 2개의 배치를 수행하여 확인합니다.
참고
- https://jojoldu.tistory.com/490
'BackEnd > Spring Batch' 카테고리의 다른 글
[Spring Batch 정리하기] 1. Spring Batch 개요 (0) | 2024.07.17 |
---|---|
[Spring] Spring Batch 장애 회고(Feat. ChunkListner 사용하기) (0) | 2024.07.12 |
[Spring] Spring Batch 사용해보기(3) - step(tasklet, chunk) (0) | 2022.09.18 |
[Spring] Spring Batch 사용해보기(2) - step(skip, retry) (0) | 2022.09.14 |
[Spring] Spring Batch 사용해보기(1) - 환경구성, 기본구현 (1) | 2022.09.08 |