Spring Batch 삽질기 1탄

Miz·2021년 5월 28일
0

spring-batch

목록 보기
1/2

대부분 스프링 배치의 내용은 '기억보다는 기록을' 블로그를 참고하여 공부하였습니다.
배치에 대해 공부하고자 하시는분은 이 글 보다 아래 블로그를 보는게 훨씬 도움이 됩니다
https://jojoldu.tistory.com/

글은 총 2편으로 이어질 예정이며 지금까지 정리한 방법을 제외하고 더 좋은 방법을 공부하게 된다면 3편까지 이어질 수 있습니다.

1. 문제점

  • 배치를 공부하면서 reader processor writer에 대한 구조를 안 뒤 만약 특정 데이터셋을 조회하여 총합계, 통계, 합계 등을 구해야하는 배치는 어떻게 진행해야될까에 대해 고민하면서 삽질한 경험을 바탕으로 정리하고자 글을 씁니다.

2. 문제 상황

@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. 첫번째 삽질

  • 먼저 코드 부터 적겠습니다.
@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. 두번째 삽질

@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받아와서 적용해버리는 방법으로 처리했다.
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. 정리

  • 뭔가 필드변수를 사용하는 것이 아닌 다른 방법이 필요하다.
profile
2년차 백엔드 개발자

0개의 댓글