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
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.
'Lecture' 카테고리의 다른 글
RestDocs로 API 문서를 만들어보자 (MockMvc 테스트, 커스터마이징 옵션 설정하기) (0) | 2023.07.13 |
---|---|
스프링 배치 병렬처리, mock과 static mock, AssertFile을 이용한 배치 로직 테스트 (0) | 2023.07.12 |
Spring Batch 프로젝트 환경 구성, 데이터 읽고 처리하고 쓰기, Batch 테스트 코드 (0) | 2023.07.12 |
Spring 배치 사용 이유와 기본 아키텍처에 대해 알아보자 (0) | 2023.07.11 |
선착순 이벤트 시스템에서 발생가능한 동시성 문제와 해결 방안 탐구(redis, kafka) (0) | 2023.07.11 |