Spring Boot와 JPA를 사용중이고, db는 Mysql을 사용하고있다.
프로젝트 진행 도중 상점에 대한 초기 데이터를 csv를 읽어와 구축할 일이 생겼슴다. Spring batch 5에 대한 정보가 별로 없는 상태에서 여기저기 훔쳐보며 어떻게든 만?듦
opencsv를 사용한 초기 데이터 구축 방식은 지난 삽질에서 화긴..
엥 웬 갑자기 Batch요 니 opencsv로 삽질했다메
opencsv를 사용한 이유는, 걍 데이터 양이 얼마 안되니까 써본거였슴다. 찍먹해본 소감으로는 데이터들을 컴마(',') 기준으로 잘라주고, 넣어준다..?
그래서 Spring Batch를 써보려는 이유는여
입니다 레쓰고~
슈퍼 카카시를 행하며 만들고자 하는 기능의 흐름은 다음과 같다
implementation 'org.springframework.boot:spring-boot-starter-batch'
//예제 csv
냥냥샵,123123-123123,"지구 멀리 어딘가....
안녕히 계쎄요",서울특별시 강남구
테스트샵,123123-44444,개구리 반찬 맛집 주소,경상북도
마지막샵,123-123,"ㅁㄴㅇㄹㅁㄴㅇㄹㅁㄴㅇㄹ
ㄹㅁㄴㅇㄹㅁㄴㅇㄹ
ㅁㅁㅁㅁㅁㅁ",제주도 서귀포시
Spring Batch 5 부터는 기존 FactoryBuilder 형식 대신, 명시적으로 필요한 것들을 넣도록 바뀌었다고 합니다..(4버전을 안써본사람)
@Slf4j
@Configuration
@RequiredArgsConstructor
public class SimpleJobConfiguration {
// step에서 사용할 reader, writer class도 따로 작성해줬습니다
private final CsvReader csvReader;
private final CsvScheduleWriter csvScheduleWriter;
// private final CsvFileDeleteListener csvFileDeleteListener;
@Bean
public Job shopDataLoadJob(JobRepository jobRepository, Step shopDataLoadStep) {
return new JobBuilder("shopInformationLoadJob", jobRepository)
.start(shopDataLoadStep) // 스텝 실행
.build();
// 필요 시 listener 추가 가능
}
@Bean
public Step shopDataLoadStep(
JobRepository jobRepository,
PlatformTransactionManager platformTransactionManager) {
return new StepBuilder("shopDataLoadStep", jobRepository)
.<ShopCsvData, ShopCsvData>chunk(100, platformTransactionManager)
.reader(csvReader.csvScheduleReader())
.writer(csvScheduleWriter)
// .listener(csvFileDeleteListener)
.build();
}
일단은 데이터를 올리는데까지만 작성해보자! Job에서 간단한 Step을 실행하는게 전부임.
ShopCsvData 형식으로 read 해서, 100chunk씩 ShopCsvData 형식으로 writer 한테 넘긴다.
reader, writer 각각의 파라미터들은 필요한 방식으로 파싱을 진행한 것!
+)Job - Step은 모두 listener를 사용해 전후에 원하는 함수를 추가적으로 실행할 수 있다.
Batch에 대한 Configuration을 작성했으니, 실행해보자!
에러는 익듁하니까.. 별 소리 아니다. Spring Batch는 실행 시 db에 관련 테이블들에 데이터를 저장하거나 로드하는데, 우린 해당 테이블을 아직 만든 적이 없다.
spring:
batch:
jdbc:
initialize-schema: always
초기화 항상 진행해달라고 yml 파일에 간곡히 부탁한 후 재실행해보자.
Batch님 오십니다~~
관련 테이블들이 생성되었다. 본격적으로 데이터를 읽고 저장만 하면 끝이다 헉!
DTO이고, csv 파일과 컬럼 명을 맞춰줘야 합니다.
+) 기존 DTO들은 record 타입으로 관리했었는데, reader 내부에서 데이터를 읽어온 뒤 기본생성자를 반드시 필요하는 부분이 있어서 class로 만들었습니다.
@Data
public class shopCsvData {
private String name;
private String phoneNumber;
private String comment;
private String address;
// setName 메서드에서 컬럼명 하나씩 써주기 귀찮아서..
public static List<String> getFieldNames() {
Field[] declaredFields = shopCsvData.class.getDeclaredFields();
List<String> result = new ArrayList<>();
for (Field declaredField : declaredFields) {
result.add(declaredField.getName());
}
return result;
}
}
껍데기도 만들었으니, 읽어봅시다.
@Configuration
@RequiredArgsConstructor
public class CsvReader {
@Value("${shop.csv-path}")
private String shopCsv;
@Bean
public FlatFileItemReader<ShopCsvData> csvScheduleReader() {
// 파일 경로 지정 및 인코딩
FlatFileItemReader<ShopCsvData> flatFileItemReader = new FlatFileItemReader<>();
flatFileItemReader.setResource(new ClassPathResource(shopCsv));
flatFileItemReader.setEncoding("UTF-8");
// 데이터 내부에 개행이 있으면 꼭! 추가해주세요
flatFileItemReader.setRecordSeparatorPolicy(new DefaultRecordSeparatorPolicy());
// 읽어온 파일을 한 줄씩 읽기
DefaultLineMapper<ShopCsvData> defaultLineMapper = new DefaultLineMapper<>();
// 따로 설정하지 않으면 기본값은 ","
DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer();
// "name", "phoneNumber", "comment", "address" 필드 설정
delimitedLineTokenizer.setNames(ShopCsvData.getFieldNames().toArray(String[]::new));
defaultLineMapper.setLineTokenizer(delimitedLineTokenizer);
// 매칭할 class 타입 지정(필드 지정)
BeanWrapperFieldSetMapper<ShopCsvData> beanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
beanWrapperFieldSetMapper.setTargetType(ShopCsvData.class);
defaultLineMapper.setFieldSetMapper(beanWrapperFieldSetMapper);
flatFileItemReader.setLineMapper(defaultLineMapper);
return flatFileItemReader;
}
}
자. 읽어보자!!!
2024-03-24T20:33:05.796+09:00 ERROR 29111 --- [ restartedMain] o.s.batch.core.step.AbstractStep : Encountered an error executing step shopDataLoadStep in job simpleJob
org.springframework.batch.item.file.FlatFileParseException: Parsing error at line: 1 in resource=[class path resource [shop-information.csv]], input=[냥냥샵,123123-123123,"지구 멀리 어딘가....]
Caused by: org.springframework.batch.item.file.transform.IncorrectTokenCountException: Incorrect number of tokens found in record: expected 4 actual 3
만약 이 에러를 마주하셨다면 데이터 내부에 개행이 있어서 그렇슴다 저 위에도 써놨슴 이걸 추가하면 돼요.. 전 몰라서 1시간 헤맸습니다 어흑
// 데이터 내부에 개행이 있으면 꼭! 추가해주세요
flatFileItemReader.setRecordSeparatorPolicy(new DefaultRecordSeparatorPolicy());
// 타고 들어가보시면 내부는 이렇게 구현되어 있슴다 쿼터가 있으면 내부의 개행 무시하도록!
public class DefaultRecordSeparatorPolicy extends SimpleRecordSeparatorPolicy {
private static final String QUOTE = "\"";
private static final String CONTINUATION = "\\";
private String quoteCharacter;
private String continuation;
public DefaultRecordSeparatorPolicy() {
this("\"", "\\");
}
읽어와서 DTO 형식으로 받았으니, DB에 저장만 하면 된다!!
@Configuration
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CsvScheduleWriter implements ItemWriter<ShopCsvData> {
private final ShopRepository shopRepository;
@Override
@Transactional
public void write(Chunk<? extends ShopCsvData> chunk) throws Exception {
Chunk<Shop> shops = new Chunk<>();
chunk.forEach(shopCsvData -> {
Shop shop = Shop.of(shopCsvData.getName(), shopCsvData.getAddress, shopCsvData.getPhoneNumber(),
shopCsvData.getComment());
shops.add(shop);
});
shopRepository.saveAll(shops);
}
}
키아~ 완성입니다. 고생많았다!
기본 기능만 썼는데도 글이 너무 길다.. 다음 글에서는 listener를 사용해 업로드를 마친 파일들을 어떻게 처리할지(삭제 혹은 옮기기), 병렬 처리 방식은 무엇인지에 대해 알아보자!!!
+) Batch에 대해 설정도 맞는것 같고, 로그도 잘 찍히는데 데이터가 안들어가있을때
2024-03-24T20:39:43.120+09:00 INFO 29111 --- [ restartedMain] o.s.batch.core.step.AbstractStep : Step: [shopDataLoadStep] executed in 101ms
2024-03-24T20:39:43.126+09:00 INFO 29111 --- [ restartedMain] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=simpleJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] in
이렇게 batch Complete 됐다고 성공 로그도 잘 찍혔는데, 정작 db를 열어봤을 땐 batch 테이블들만 있고 데이터는 올라가지 않은 어메이징한 상황이 있습니다.
당황하지 마시고~ batch 관련 테이블 혹은 데이터를 지운 후 서버를 재시작 하거나, COMPELETE 이후에도 재실행 하고싶은거라면 step 부분에 allowStartIfComplete(true)
를 추가하면 됩니다
테이블들을 하나씩 뜯어보면 알겠지만, Spring Batch는 Job에 대한 시도가 성공했는지, 실패했는지 모두 기록에 남기고, 로그로도 보여줍니다. 저는 데이터를 올리기 전 reader까지만 작성하고 테스트하는 과정에서 이미 COMPLETED Status로 변경되어 Job이 반복해서 실행되지 않는 케이스였어서, 관련 데이터를 지우고 재실행 했습니다~!
참고 자료