Spring Batch 삽질기 1탄

Spring / / 2021. 6. 22. 15:18


> 대부분 스프링 배치의 내용은 '기억보다는 기록을' 블로그를 참고하여 공부하였습니다.
배치에 대해 공부하고자 하시는분은 이 글 보다 아래 블로그를 보는게 훨씬 도움이 됩니다
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
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기