본문 바로가기
Lecture

스프링 배치 Validator, listener, FlatFileItemReader 및 Writer를 사용하여 간단한 text 변환 작업을 구현해보자

by Renechoi 2023. 7. 12.

 

JobParameterValidator 

 

@Configuration
@AllArgsConstructor
@Slf4j
public class AdvancedJobConfig {

   private final JobBuilderFactory jobBuilderFactory;
   private final StepBuilderFactory stepBuilderFactory;

   @Bean
   public Job advancedJob(Step advancedStep){
      return jobBuilderFactory.get("advancedJob")
         .incrementer(new RunIdIncrementer())
         .validator(new LocalDateParameterValidator("targetDate"))
         .start(advancedStep)
         .build();
   }

   @JobScope
   @Bean
   public Step advancedStep(Tasklet advancedTasklet ){
      return stepBuilderFactory.get("advancedStep")
         .tasklet(advancedTasklet).build();
   }

   @StepScope
   @Bean
   public Tasklet advancedTasklet(@Value("#{jobParameters['targetDate']}") String targetDate){
      return ((contribution, chunkContext) -> {
         log.info("[AdvancedJobConfig] JobParameter - targetDate = " + targetDate);
         log.info("[AdvancedJobConfig] executed advancedTasklet");
         return RepeatStatus.FINISHED;
      });
   }
}

 

파라미터로 받을 수 있는 설정 

 

 

 

 

// todo 코드 설명 

 

 

 

JobExecutionListener

 

 

 

@JobScope
@Bean
public JobExecutionListener jobExecutionListener() {
   return new JobExecutionListener() {
      @Override
      public void beforeJob(JobExecution jobExecution) {
         log.info("[JobExecutionListener] JobExecution is " + jobExecution.getStatus());
      }

      @Override
      public void afterJob(JobExecution jobExecution) {
         log.info("[JobExecutionListener] JobExecution is " + jobExecution.getStatus());
      }
   };
}

 

 

 

@Bean
public Job advancedJob(Step advancedStep, JobExecutionListener jobExecutionListener) {
   return jobBuilderFactory.get("advancedJob")
      .incrementer(new RunIdIncrementer())
      .validator(new LocalDateParameterValidator("targetDate"))
      .listener(jobExecutionListener)
      .start(advancedStep)
      .build();
}

 

 

 

 

어떻게 사용할까? 작업이 실패했을 때 알림을 받는 용도로 사용할 수 있다. 

 

@Override
public void afterJob(JobExecution jobExecution) {
   if (jobExecution.getStatus() == BatchStatus.FAILED) {
      log.info("[JobExecutionListener] JobExecution is " + jobExecution.getStatus());
      // notificationservice -> notify 
   }
}

 

 

StepListener도 같은 방식으로 구현할 수 있다. 

 

@StepScope
@Bean
public StepExecutionListener stepExecutionListener() {
   return new StepExecutionListener() {
      @Override
      public void beforeStep(StepExecution stepExecution) {
         log.info("[StepExecutionListener#beforeStep] stepExecution is " + stepExecution.getStatus());
      }

      @Override
      public ExitStatus afterStep(StepExecution stepExecution) {
         log.info("[StepExecutionListener#afterStep] stepExecution is " + stepExecution.getStatus());
         return stepExecution.getExitStatus();
      }
   };
}

 

그 외에도 다양한 listener를 구현할 수 있다. 

 

https://docs.spring.io/spring-batch/docs/current/reference/html/step.html#interceptingStepExecution

 

Configuring a Step

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

 

FlatFileItemReader

 

 

파일을 읽도록 해주는 ItemReader이다. Chunk 기반으로 Item들을 읽을 수 있다. 

 

 

다음과 같은 내용을 가진 player.txt를 읽어 객체로 만들어 처리하는 예제 코드를 작성해보자. 

 

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

 

해당 파일을 읽어올 dto를 생성한다. 

 

@Data
public class PlayerDto {
   private String ID;
   private String lastName;
   private String firstName;
   private String position;
   private int birthYear;
   private int debutYear;
}

 

ItemReader를 다음과 같이 작성한다. 

 

@StepScope
@Bean
public FlatFileItemReader<PlayerDto> playerFileItemReader() {
    return new FlatFileItemReaderBuilder<PlayerDto>()
        .name("playerFileItemReader")
        .lineTokenizer(new DelimitedLineTokenizer())
        .linesToSkip(1)
        .fieldSetMapper(new PlayerFieldSetMapper())
        .resource(new FileSystemResource("player-list.txt"))
        .build();
}

 

lineTokenizer는 delimiter를 설정할 수 있다. 기본 설정 값으로 ','를 제거해준다. 

 

linestoskip은 건너뛸 줄을 1로 설정한다. 가장 위부터 하나의 줄을 건너뛰고 파일을 읽도록 해준다. 

 

mapper는 객체로 매핑해줄 mapper이다. 

 

resource는 읽을 파일이다. 

 

 

매퍼를 다음과 같이 구현한다. 

 

public class PlayerFieldSetMapper implements FieldSetMapper<PlayerDto> {
    @Override
    public PlayerDto mapFieldSet(FieldSet fieldSet) {
        PlayerDto dto = new PlayerDto();
        dto.setID(fieldSet.readString(0));
        dto.setLastName(fieldSet.readString(1));
        dto.setFirstName(fieldSet.readString(2));
        dto.setPosition(fieldSet.readString(3));
        dto.setBirthYear(fieldSet.readInt(4));
        dto.setDebutYear(fieldSet.readInt(5));
        return dto;
    }
}

 

mapper는 인터페이스로 구현한다. delimeter로 구분지어진 fieldSet이 들어오면 매핑해줄 내용을 작성한다. 

 

 

 

ItemProcessorAdapter 

 

 

Reader로 읽어온 데이터를 처리할 수 있도록 먼저 ItemProcessorAdapter를 구현해보자. 데이터를 가공하는 부분으로서 비즈니스 로직과 관련된 로직이 프로세스에 위치하게 된다. 

 

 

받아온 Player 정보를 비즈니스 로직에서 계산하여 Salary 정보로 반환하는 서비스를 만든다고 해보자. 

 

이를 위해 먼저 SalaryDto를 작성한다. 

 

@Data
public class PlayerSalaryDto {
   private String ID;
   private String lastName;
   private String firstName;
   private String position;
   private int birthYear;
   private int debutYear;
   private int salary;

   public static PlayerSalaryDto of(PlayerDto player, int salary) {
      PlayerSalaryDto playerSalary = new PlayerSalaryDto();
      playerSalary.setID(player.getID());
      playerSalary.setLastName(player.getLastName());
      playerSalary.setFirstName(player.getFirstName());
      playerSalary.setPosition(player.getPosition());
      playerSalary.setBirthYear(player.getBirthYear());
      playerSalary.setDebutYear(player.getDebutYear());
      playerSalary.setSalary(salary);
      return playerSalary;
   }
}

 

PlayerDto가 가진 정보를 동일하게 가지되, 변환을 하면서 Salary 항목이 추가된다. 

 

 

Salary를 계산하는 서비스를 만들어보자. 

 

@Service
public class PlayerSalaryService {

   public PlayerSalaryDto calcSalary(PlayerDto player) {
      int salary = (Year.now().getValue() - player.getBirthYear()) * 1000000;
      return PlayerSalaryDto.of(player, salary);
   }
}

 

단순하게 출생년도에 100만원을 곱한 로직이다. 

 

이러한 처리를 담당하는 processor를 adapter 형식으로 구현할 수 있다. 

 

@StepScope
@Bean
public ItemProcessorAdapter<PlayerDto, PlayerSalaryDto> playerSalaryItemProcessorAdapter(
    PlayerSalaryService playerSalaryService) {
    ItemProcessorAdapter<PlayerDto, PlayerSalaryDto> adapter = new ItemProcessorAdapter<>();
    adapter.setTargetObject(playerSalaryService);
    adapter.setTargetMethod("calcSalary");
    return adapter;
}

 

PlayerService를 설정해주고, 호출하는 메서드 명을 스트링으로 설정해준다. 

 

해당 어댑터를 다음과 같이 사용할 수 있다. 

 

@JobScope
@Bean
public Step flatFileStep(FlatFileItemReader<PlayerDto> playerFileItemReader,
  ItemProcessorAdapter<PlayerDto, PlayerSalaryDto> itemProcessorAdapter
) {
    return stepBuilderFactory.get("flatFileStep")
        .<PlayerDto, PlayerSalaryDto>chunk(5)
        .reader(playerFileItemReader)
        .processor(itemProcessorAdapter)
        .build();
}

 

adapter를 사용하는 것의 장점은 기존의 만들어진 서비스를 set 방식으로 활용할 수 있다는 점이다. 

 

 

FlatFileItemWriter 

 

이제 해당 로직을 파일에 작성하는 Writer를 사용해보자. 

 

writer를 다음과 같이 작성한다. 

 

@StepScope
@Bean
public FlatFileItemWriter<PlayerSalaryDto> playerFileItemWriter() throws IOException {
    BeanWrapperFieldExtractor<PlayerSalaryDto> fieldExtractor = new BeanWrapperFieldExtractor<>();
    fieldExtractor.setNames(new String[]{"ID", "firstName", "lastName", "salary"});
    fieldExtractor.afterPropertiesSet();

    DelimitedLineAggregator<PlayerSalaryDto> lineAggregator = new DelimitedLineAggregator<>();
    lineAggregator.setDelimiter("\t");
    lineAggregator.setFieldExtractor(fieldExtractor);

    // 기존의 파일을 덮어쓰는 방식
    new File("player-salary-list.txt").createNewFile();
    FileSystemResource resource = new FileSystemResource("player-salary-list.txt");

    return new FlatFileItemWriterBuilder<PlayerSalaryDto>()
        .name("playerFileItemWriter")
        .resource(resource)
        .lineAggregator(lineAggregator)
        .build();
}

 

lineAggregator는 Extractor를 사용해서 어떻게 쓸 것인지를 정의해주는 역할을 한다. 

 

쓰고자 하는 names를 set 해주고, 해당값들에 대한 delimiter를 지정한다. 

 

 

이제 Job을 돌리면 다음과 같이 변환된 결과물이 나오는 것을 볼 수 있다. 

 

작업 전

작업 후

 

 

 

 


참고 자료 

- Spring Batch Reference Doc: https://docs.spring.io/spring-batch/docs/current/reference/html/

- 패스트캠퍼스: 한 번에 끝내는 Spring 완.전.판 초격차 패키지 Online.

 

 

 

 

 

반응형