> 대부분 스프링 배치의 내용은 '기억보다는 기록을' 블로그를 참고하여 공부하였습니다.
배치에 대해 공부하고자 하시는분은 이 글 보다 아래 블로그를 보는게 훨씬 도움이 됩니다
https://jojoldu.tistory.com/
> 글은 총 2편으로 이어질 예정이며 지금까지 정리한 방법을 제외하고 더 좋은 방법을 공부하게 된다면 3편까지 이어질 수 있습니다.
### 1. 문제점
- 배치를 공부하면서 reader processor writer에 대한 구조를 안 뒤 만약 `특정 데이터셋을 조회하여 총합계, 통계, 합계 등을 구해야하는 배치는 어떻게 진행해야될까`에 대해 고민하면서 삽질한 경험을 바탕으로 정리하고자 글을 씁니다.
### 2. 문제 상황
```java
@ToString
@Getter
@Setter
@NoArgsConstructor
@Entity
public class Pay {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long amount;
private String txName;
private LocalDateTime txDateTime;
public Pay(Long amount, String txName, String txDateTime) {
this.amount = amount;
this.txName = txName;
this.txDateTime = LocalDateTime.parse(txDateTime, FORMATTER);
}
public Pay(Long amount, String txName, LocalDateTime txDateTime) {
this.amount = amount;
this.txName = txName;
this.txDateTime = txDateTime;
}
public Pay(Long id, Long amount, String txName, String txDateTime) {
this.id = id;
this.amount = amount;
this.txName = txName;
this.txDateTime = LocalDateTime.parse(txDateTime, FORMATTER);
}
}
@Entity
@Getter
@NoArgsConstructor
public class TotalPay {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long sum;
private LocalDate date;
public TotalPay(Long sum, String date) {
this.sum = sum;
this.date = LocalDate.parse(date,FORMATTER);
}
public void addSum(Long item) {
this.sum += sum;
}
}
```
1. 위와 같이 `Pay`, `TotalPay`가 있는 상황에서 특정 날짜의 Pay들의 Amount를 합산해서 저장해야하는 문제 상황이 있다고 가정했습니다.
2. Jpa를 기반으로 문제를 해결해야한다.
> Repository 클래스는 생략하겠습니다.
### 3. 첫번째 삽질
- 먼저 코드 부터 적겠습니다.
```java
@Slf4j
@Configuration
@RequiredArgsConstructor
public class PayTotalJobConfiguration {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static final String JOB_NAME = "PayTotalJob";
public static final String BEAN_PREFIX = JOB_NAME + "_";
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final EntityManagerFactory entityManagerFactory;
private final TotalPayRepository totalPayRepository;
private final DataSource dataSource;
//Pay를 돌면서 계속해서 추가할 total값
private Long total = 0L;
private final static int chunkSize = 1000;
@Bean(JOB_NAME)
public Job job() {
return jobBuilderFactory.get(JOB_NAME)
.start(step(null))
.next(step2(null))
.build();
}
@Bean(BEAN_PREFIX + "step")
@JobScope
public Step step(@Value("#{jobParameters[requestDate]}") String requestDate) {
return stepBuilderFactory.get(BEAN_PREFIX + "step")
.<Pay, Pay>chunk(chunkSize)
.reader(reader(null))
.writer(writer())
.build();
}
@Bean(BEAN_PREFIX + "reader")
@StepScope
public JpaPagingItemReader<Pay> reader(@Value("#{jobParameters[requestDate]}") String requestDate) {
return new JpaPagingItemReaderBuilder<Pay>()
.name(BEAN_PREFIX + "reader")
.entityManagerFactory(entityManagerFactory)
.pageSize(chunkSize)
.queryString("select p from Pay p where to_char(tx_date_time,'yyyy-mm-dd') = '" + requestDate + "'")
.build();
}
@Bean(BEAN_PREFIX + "writer")
@StepScope
public ItemWriter<Pay> writer() {
return list -> {
for(Pay pay : list) {
log.info("Current = {}", pay);
total += pay.getAmount();
}
};
}
@Bean(BEAN_PREFIX + "step2")
@JobScope
public Step step2(@Value("#{jobParameters[requestDate]}") String requestDate) {
return stepBuilderFactory.get(BEAN_PREFIX + "step2")
.<TotalPay, TotalPay>chunk(1)
.reader(reader2(null))
.writer(writer2())
.build();
}
@Bean(BEAN_PREFIX + "reader2")
@StepScope
public CustomCreateTotalPayReader reader2(@Value("#{jobParameters[requestDate]}") String requestDate) {
return new CustomCreateTotalPayReader(requestDate,total);
}
@Bean(BEAN_PREFIX + "writer2")
@StepScope
public JpaItemWriter<TotalPay> writer2() {
JpaItemWriter<TotalPay> jpaItemWriter = new JpaItemWriter<>();
jpaItemWriter.setEntityManagerFactory(entityManagerFactory);
return jpaItemWriter;
}
}
public class CustomCreateTotalPayReader implements ItemReader<TotalPay> {
private static int count;
private String requestDate;
private Long total;
public CustomCreateTotalPayReader(String requestDate, Long total) {
this.requestDate = requestDate;
this.total = total;
}
@Override
public TotalPay read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
TotalPay totalPay = null;
if(count < 1) {
totalPay = new TotalPay(total, requestDate);
count++;
}
return totalPay;
}
}
```
1. 첫번째 Step에서는 Pay를 읽어 온뒤 Writer를 통해 `Long total`에 값을 추가한다.
2. 두번째 Step에서는 필드 total값으로 `TotalPay`를 받아오는 `CustomCreateTotalPayReader` 를 만든다.
3. `CustomCreateTotalPayReader`에서 가져온 `TotalPay`를 JpaItemWriter를 통해 저장한다.
### 3-1. 첫번째 삽질의 문제점
- Configuration에 필드변수로 Long을 사용해 병렬적으로 처리할떄 문제점이 발생할 수 있다.
- 굳이 CustomCreateTotalPayReader라는 클래스를 생성해야하며 Reader클래스의 생성 방법도 마음에 들지 않는다.
### 4. 두번째 삽질
```java
@Slf4j
@Configuration
@RequiredArgsConstructor
public class PayTotalJobSecondConfiguration {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static final String JOB_NAME = "PayTotalSecondJob";
public static final String BEAN_PREFIX = JOB_NAME + "_";
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final EntityManagerFactory entityManagerFactory;
private final TotalPayRepository totalPayRepository;
private Long total = 0L;
private final static int chunkSize = 1000;
@Bean(JOB_NAME)
public Job job() {
return jobBuilderFactory.get(JOB_NAME)
.start(step(null))
.next(step2(null))
.build();
}
@Bean(BEAN_PREFIX + "step")
@JobScope
public Step step(@Value("#{jobParameters[requestDate]}") String requestDate) {
return stepBuilderFactory.get(BEAN_PREFIX + "step")
.<Pay, Pay>chunk(chunkSize)
.reader(reader(null))
.writer(writer())
.build();
}
@Bean(BEAN_PREFIX + "reader")
@StepScope
public JpaPagingItemReader<Pay> reader(@Value("#{jobParameters[requestDate]}") String requestDate) {
return new JpaPagingItemReaderBuilder<Pay>()
.name(BEAN_PREFIX + "reader")
.entityManagerFactory(entityManagerFactory)
.pageSize(chunkSize)
.queryString("select p from Pay p where to_char(tx_date_time,'yyyy-mm-dd') = '" + requestDate + "'")
.build();
}
@Bean(BEAN_PREFIX + "writer")
@StepScope
public ItemWriter<Pay> writer() {
return list -> {
for(Pay pay : list) {
log.info("Current = {}", pay);
total += pay.getAmount();
}
};
}
@Bean(BEAN_PREFIX + "step2")
@JobScope
public Step step2(@Value("#{jobParameters[requestDate]}") String requestDate) {
return stepBuilderFactory.get(BEAN_PREFIX + "step2")
.tasklet((contribution, chunkContext) -> {
TotalPay totalPay = new TotalPay(total,requestDate);
totalPayRepository.save(totalPay);
return RepeatStatus.FINISHED;
}).build();
}
}
```
1. 첫번째 Step에서는 Pay를 읽어 온뒤 Writer를 통해 `Long total`에 값을 추가하는 로직은 첫번째 삽질과 다를게 없다.
2. 두번째 Step에서는 따로 Reader, Writer가 아닌 그냥 TaskLet을 구현하는 방식으로 처리하였다.
3. 추가적인 커스텀 리더를 만들지 않고 JpaRepository를 Di받아와서 적용해버리는 방법으로 처리했다.
```java
return stepBuilderFactory.get(BEAN_PREFIX + "step2")
.tasklet((contribution, chunkContext) -> {
TotalPay totalPay = new TotalPay(total,requestDate);
totalPayRepository.save(totalPay);
return RepeatStatus.FINISHED;
}).build();
}
```
### 4.1 두번째 삽질의 문제점
- 결국 Long으로 되는 field를 사용하였기 때문에 여전히 문제가 있다.
### 5. 정리
- 뭔가 필드변수를 사용하는 것이 아닌 다른 방법이 필요하다.
'Spring' 카테고리의 다른 글
Spring Batch 삽질기 3탄 (0) | 2021.06.22 |
---|---|
Spring Batch 삽질기 2탄 (0) | 2021.06.22 |
SpringBatch Jpa Test 설정 (0) | 2021.06.22 |
Spring Collection @Valid (0) | 2021.06.22 |
Spring Bean 생명주기 (0) | 2021.06.22 |
최근댓글