예제로 배우는 핵심 Spring Batch

Ch06. 배치 작업의 기본, 파일 읽기와 쓰기

webmaster 2023. 6. 9. 10:59
728x90

https://docs.spring.io/spring-batch/docs/current/reference/html/index-single.html#simpleDelimitedFileReadingExample

 

Spring Batch - Reference Documentation

If a group of Steps share similar configurations, then it may be helpful to define a “parent” Step from which the concrete Steps may inherit properties. Similar to class inheritance in Java, the “child” Step combines its elements and attributes wit

docs.spring.io

초기 세팅

plyers.csv

ID,lastName,firstName,position,birthYear,debutYear
"AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996",
"AbduRa00,Abdullah,Rabih,rb,1975,1999",
"AberWa00,Abercrombie,Walter,rb,1959,1982",
"AbraDa00,Abramowicz,Danny,wr,1945,1967",
"AdamBo00,Adams,Bob,te,1946,1969",
"AdamCh00,Adams,Charlie,wr,1979,2003"
  • csv 파일로 ", "를 기준으로 데이터를 구분한다.
  • 첫 번째 줄 데이터는 어떤 데이터인지를 구분해 주는 row로 실제 데이터는 아니다.

File 읽어서 Print 하기

Player(dto 생성)

@Data
public class Player {

  private String id;
  private String lastName;
  private String firstName;
  private String position;
  private int birthYear;
  private int debutYear;

}

FileDataReadWriteConfig

/**
 * desc: 파일 읽고 쓰기
 * run: --spring.batch.job.name=fileReadWriteStep
 */
@Configuration
@RequiredArgsConstructor
public class FileDataReadWriteConfig {

  private final JobBuilderFactory jobBuilderFactory;

  private final StepBuilderFactory stepBuilderFactory;

  @Bean
  public Job fileDataReadWriteJob(Step fileReadWriteStep) {
    return jobBuilderFactory.get("fileDataReadWriteJob")
        .incrementer(new RunIdIncrementer())
        .start(fileReadWriteStep)
        .build();
  }

  @Bean
  @JobScope
  public Step fileReadWriteStep(ItemReader playerItemReader) {
    return stepBuilderFactory.get("fileReadWriteStep")
        .<Player, Player>chunk(5) //5개의 크기로 읽고 쓰겠다.
        .reader(playerItemReader)
        .writer(
            new ItemWriter() {
              @Override
              public void write(List items) throws Exception {
                items.forEach(System.out::println);
              }
            }
        )
        .build();
  }

  @Bean
  @StepScope
  public FlatFileItemReader<Player> playerItemReader(){
    return new FlatFileItemReaderBuilder<Player>()
        .name("playerItemReader")
        .resource(new FileSystemResource("resource.Players.csv"))
        .lineTokenizer(new DelimitedLineTokenizer())
        .fieldSetMapper(new PlayerFieldSetMapper()) //Play - File mapping
        .linesToSkip(1) //1번쨰 줄 스킵
        .build();
  }
}

PlayerFieldSetMapper

public class PlayerFieldSetMapper implements FieldSetMapper<Player> {

  @Override
  public Player mapFieldSet(FieldSet fieldSet) throws BindException {
    Player player = new Player();
    player.setId(fieldSet.readString(0));
    player.setLastName(fieldSet.readString(1));
    player.setFirstName(fieldSet.readString(2));
    player.setPosition(fieldSet.readString(3));
    player.setBirthYear(fieldSet.readInt(4));
    player.setDebutYear(fieldSet.readInt(5));

    return player;
  }
}
  • 옮길 객체를 만들어 준다(player)
  • FileDataReadWriteConfig에서 ItemReader를 생성할 때, FlatFileItemReaderBuilder를 통해 생성한다.
    • 이름, 리소스 위치(파일 위치) 적어준다.
    • lineTokenizer를 통해 줄마다 자르고, DelimitedLineTokenizer를 통해 ", "마다 구분해 준다.
    • fieldSetMapper에 변환시켜 줄 Mapper(PlayerFieldSetMapper)를 적어준다.
    • 첫 번째 줄은 실제 데이터가 아니므로 lineToSkip()을 통해 생략한다.
  • PlayerFieldSetMapper은 FieldSetMapper를 구현하여, mapFieldSet을 구현한다.
    • Player객체를 생성하여, 여기에 파일에서 읽은 값을 전달해서 세팅한다.
  • ItemWriter에서는 단순하게 Console에 Print 하는 역할만 한다.

 

File 읽어서 Print 하기

FileDataReadWriteConfig

/**
 * desc: 파일 읽고 쓰기 run: --spring.batch.job.name=fileDataReadWriteJob
 */
@Configuration
@RequiredArgsConstructor
public class FileDataReadWriteConfig {

  private final JobBuilderFactory jobBuilderFactory;

  private final StepBuilderFactory stepBuilderFactory;

  @Bean
  public Job fileDataReadWriteJob(Step fileReadWriteStep) {
    return jobBuilderFactory.get("fileDataReadWriteJob")
        .incrementer(new RunIdIncrementer())
        .start(fileReadWriteStep)
        .build();
  }

  @Bean
  @JobScope
  public Step fileReadWriteStep(ItemReader playerItemReader, ItemProcessor playerItemProcessor, ItemWriter playerItemWriter) {
    return stepBuilderFactory.get("fileReadWriteStep")
        .<Player, PlayerYears>chunk(5) //5개의 크기로 읽고 쓰겠다.
        .reader(playerItemReader)
        /*
        .writer(

            new ItemWriter() {
              @Override
              public void write(List items) throws Exception {
                items.forEach(System.out::println);
              }
            }
        )
         */
        .processor(playerItemProcessor) //중간 작업
        .writer(playerItemWriter)
        .build();
  }

  @Bean
  @StepScope
  public FlatFileItemReader<Player> playerItemReader() {
    return new FlatFileItemReaderBuilder<Player>()
        .name("playerItemReader")
        .resource(new FileSystemResource("Players.csv"))
        .lineTokenizer(new DelimitedLineTokenizer())
        .fieldSetMapper(new PlayerFieldSetMapper()) //Play - File mapping
        .linesToSkip(1) //1번쨰 줄 스킵
        .build();
  }

  @Bean
  @StepScope
  public ItemProcessor<Player, PlayerYears> playerItemProcessor() {
    return new ItemProcessor<Player, PlayerYears>() {
      @Override
      public PlayerYears process(Player item) throws Exception {
        return new PlayerYears(item);
      }
    };
  }


  @Bean
  @StepScope
  public FlatFileItemWriter<PlayerYears> playerItemWriter() {
    //오류 발생: : org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to
    //원래라면 ItemWriter는 ItemStream으로 리턴되어도 상관없지만, 위에 @Bean과 @StepScope를 같이 사용해서 Bean이 proxy로 설정되어 FlatFileItemWriter에서 지원하는 open() 메서드가 동작을 안하게 되어 발생되는 문제.
    /**
     * 해결방안
     * 1. csv파일을 write 해주는 객체 FlatFileItemWriter로 리턴해야 하는 경우, 똑같이 반환타입을 맞춘다.
     * 2. ItemWriter로 반환하려면 @Bean이나 @StepScope 둘 중 하나를 삭제한다. (proxy 로 지정되는 부분을 삭제)
     */
    BeanWrapperFieldExtractor<PlayerYears> fieldExtractor = new BeanWrapperFieldExtractor<>();
    fieldExtractor.setNames(new String[]{"id", "lastName", "position", "yearsExperience"}); //새로운 파일 만들 조건
    fieldExtractor.afterPropertiesSet();

    DelimitedLineAggregator<PlayerYears> lineAggregator = new DelimitedLineAggregator<>(); //구분자 추가
    lineAggregator.setDelimiter(",");
    lineAggregator.setFieldExtractor(fieldExtractor);

    FileSystemResource outputResource = new FileSystemResource("players_output.txt");

    return new FlatFileItemWriterBuilder<PlayerYears>()
        .name("playerItemWriter")
        .resource(outputResource)
        .lineAggregator(lineAggregator)
        .build();
  }
}

PlayerYears

@Data
public class PlayerYears {

  private String id;
  private String lastName;
  private String firstName;
  private String position;
  private int birthYear;
  private int debutYear;
  private int yearsExperience;

  public PlayerYears(Player player) {
    this.id = player.getId();
    this.lastName = player.getLastName();
    this.firstName = player.getFirstName();
    this.position = player.getPosition();
    this.birthYear = player.getBirthYear();
    this.debutYear = player.getDebutYear();
    this.yearsExperience = Year.now().getValue() - player.getDebutYear();
  }
}
  • 실무에서는 Processor 작업이 반드시 필요하기에 해당 작업을 추가했다.
    • 여기서는 Player 객체를 PlayerYear 객체로 변환하는 작업을 한다.
    • PlayerYear의 yearsExperience(연차) 필드는 현재 년도에 데뷔년도를 뺀다.
  • playerItemWriter에서 실제 파일에 넣는 작업을 진행한다.
    • BeanWrapperFieldExtractor를 생성해서, 어떤 필드 값들을 파일에 쓸지 작성한다.
    • DelimitedLineAggregator를 사용해서 추가할 구분자를 작성한다.
    • FileSystemResource를 생성하여, 어느 파일에 쓸지 작성한다.
    • FlatFileItemWriterBuilder를 이용해 ItemWriter를 생성한다.

참조 > https://choisblog.tistory.com/m/83

 

Spring Batch Error : Writer must be open before it can be written to

에러 메시지 : org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to 원인 : ItemWriter 구현시, csv파일을 write하는 작업에서 발생한 에러. @Bean 과 @StepScope으로 ItemWriter을 구현

choisblog.tistory.com

  • 실제 실행하게 되면 오류(org.springframework.batch.item.WriterNotOpenException: Writer must be open before it can be written to)가 발생하는데, ItemWriter 구현시, .txt 파일을 write하는 작업에서 발생한 에러이다.
  • ItemWriter는 ItemStream으로 리턴되어도 상관없지만, @Bean과 @StepScope를 같이 사용해서 Bean이 Proxy로 설정이 되어FlatFileItemWriter에서 지원하는 open() 메서드가 동작을 안하게 되어 발생되는 문제이다.
  • 해결방법으로 FlatFileItemWriter를 반환하도록 하거나, Bean/StepScioe 어노테이션중 하나를 제거하면 해결된다.

 

 

728x90