프로젝트 폴더에 있는 csv 파일을 읽어 적절한 엔티티 형태로 바꿔 DB에 저장하는 방법을 알아보자.
Spring Batch
배치(Batch)는 일괄처리 란 뜻을 가지고 있다.
만약 대용량의 파일을 DB에 저장하는 기능이 필요하다고 가정해보자. 이렇게 큰 데이터를 읽고, 가공하고, 저장한다면 해당 서버는 순식간에 CPU, I/O 등의 자원을 다써버릴 것이다.
그리고 이 집계 기능은 하루에 1번 수행된다.
이를 위해 API를 구성하는 것은 너무 낭비가 아닐까?
바로 이런 단발성으로 대용량의 데이터를 처리하는 애플리케이션을 배치 애플리케이션이라고 한다.
스프링에서는 Spring Batch를 통해 배치 애플리케이션을 사용할 수 있다.
이해를 돕기 위해 대용량의 csv 파일(food.csv)을 읽어 DB에 저장하는 예시를 Spring Batch를 통해 알아볼 것이다.
build.gradle - 의존성 추가
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-batch'
...
}
spring batch를 build.gradle에 추가한다.
스프링부트 Application - 어노테이션 추가
@EnableBatchProcessing // 배치 사용을 위한 선언
@SpringBootApplication
public class BatchApplication {
public static void main(String[] args) {
SpringApplication.run(BatchApplication.class, args);
}
}
배치 사용을 위해 스프링 부트 Application 파일에 어노테이션을 추가한다.
Spring Batch Job 등록
@Slf4j
@Configuration
@RequiredArgsConstructor
public class FileItemReaderJobConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final CsvReader csvReader;
private final CsvWriter csvWriter;
private static final int chunkSize = 1000;
@Bean
public Job csvFileItemReaderJob() {
return jobBuilderFactory.get("csvFileItemReaderJob")
.start(csvFileItemReaderStep())
.build();
}
@Bean
public Step csvFileItemReaderStep() {
return stepBuilderFactory.get("csvFileItemReaderStep")
.<Food, Food>chunk(chunkSize)
.reader(csvReader.csvFileItemReader())
.writer(csvWriter)
.build();
}
}
Spring Batch에서 Job은 하나의 배치 작업 단위를 이야기한다.
하나의 Job 안에는 여러 Step이 존재하고 Step 안에 Reader, Writer 등이 포함된다.
Job
위 코드에서 csvFileItemReaderJob라는 이름의 Job을 생성하였고,
Step
Job에 속하는 csvFileItemReaderStep라는 이름의 Step을 생성하였다.
Reader & Writer
Step은 CsvReader와 CsvWriter를 가지고 있다.
csv 파일을 읽어오는 행위를 CsvReader에서 실행할 것이고 읽어온 데이터를 DB에 저장하는 행위를 CsvWriter에서 실행할 것이라고 예상할 수 있다.
.<Food, Food>chunk(chunkSize)
Spring Batch에서 chunk의 의미는, 데이터 덩어리이다.
reader에서 1 row씩 데이터를 읽어 row가 chunk size만큼 쌓이면 해당 row를 한꺼번에 묶어서 wirter에 한 번에 보내는 것이다.
즉, chunk size만큼 트랜잭션을 실행한다는 의미이다.
또한 <Food, Food>의 의미는 Reader에서 읽어올 타입이 Food이며, Writer에 넘겨줄 타입이 Food라는 의미이다.
Reader
@Configuration
@RequiredArgsConstructor
public class CsvReader {
@Bean
public FlatFileItemReader<FoodDto> csvFileItemReader() {
/* file read */
FlatFileItemReader<Food> flatFileItemReader = new FlatFileItemReader<>();
flatFileItemReader.setResource(new ClassPathResource("/csv/food.csv"));
flatFileItemReader.setLinesToSkip(1); // header line skip
flatFileItemReader.setEncoding("UTF-8"); // encoding
/* read하는 데이터를 내부적으로 LineMapper을 통해 Mapping */
DefaultLineMapper<Food> defaultLineMapper = new DefaultLineMapper<>();
/* delimitedLineTokenizer : setNames를 통해 각각의 데이터의 이름 설정 */
DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer(",");
delimitedLineTokenizer.setNames("id", "name");
defaultLineMapper.setLineTokenizer(delimitedLineTokenizer);
/* beanWrapperFieldSetMapper : Tokenizer에서 가지고온 데이터들을 VO로 바인드하는 역할 */
BeanWrapperFieldSetMapper<Food> beanWrapperFieldSetMapper = new BeanWrapperFieldSetMapper<>();
beanWrapperFieldSetMapper.setTargetType(Food.class);
defaultLineMapper.setFieldSetMapper(beanWrapperFieldSetMapper);
/* lineMapper 지정 */
flatFileItemReader.setLineMapper(defaultLineMapper);
return flatFileItemReader;
}
}
CsvReader파일이다. 부분적으로 살펴보자.
FlatFileItemReader 객체 생성
//FlatFileItemReader 객체 생성
FlatFileItemReader<Food> flatFileItemReader = new FlatFileItemReader<>();
//src/main/resources/csv/food.csv 경로의 파일 지정
flatFileItemReader.setResource(new ClassPathResource("/csv/food.csv"));
//맨 윗줄(header)는 읽지 않고 skip 하겠다는 의미 (맨 윗줄이 제목 줄인 경우 사용)
flatFileItemReader.setLinesToSkip(1); // header line skip
//인코딩 지정
flatFileItemReader.setEncoding("UTF-8"); // encoding
DefaultLineMapper 객체 생성
//매핑을 원하는 엔티티(Food)로 객체를 생성한다.
DefaultLineMapper<Food> defaultLineMapper = new DefaultLineMapper<>();
DelimitedLineTokenizer 객체 생성
//csv 파일에서 구분자 설정
DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer(",");
// 각각의 데이터 이름 설정 - 엔티티 필드의 이름과 동일하게 설정하면 된다.
delimitedLineTokenizer.setNames("id", "name");
defaultLineMapper.setLineTokenizer(delimitedLineTokenizer);
<food.csv 예시>
1,계란
2,라면
3,시금치
...
<Food 엔티티>
@Data
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Entity
public class Food {
@Id
@Column(name = "food_id")
private String id;
//==생성자==//
public Food(String id, String name) {
this.id = id;
this.name = name;
}
}
Reader에서는 food.csv의 데이터를 Food 엔티티 저장해서 Writer에 전달하는 것이 목적이다.
엔티티에는 PUBLIC 기본 생성자와 getter, setter가 필요하다.
Writer
@Configuration
@RequiredArgsConstructor
public class CsvWriter implements ItemWriter<Food> {
private final FoodRepository foodRepository;
@Override
public void write(List<? extends Food> list) throws Exception {
foodRepository.saveAll(new ArrayList<Food>(list));
}
}
CsvWriter 파일이다.
Writer에서는 ItemWriter를 통해 write 메서드를 오버라이딩하였다.파라미터인 list는 Reader를 통해 csv 파일에서 읽어온 데이터가 담겨져있는 리스트이고, 이를 Repository를 통해 DB에 저장한다.
실행
insert 쿼리가 차례대로 나가는 것을 확인할 수 있다.
만약 SQLSyntaxErrorException: Table '{schema}.BATCH_JOB_INSTANCE' doesn't exist 오류가 발생한다면
application.yml에 다음 설정을 추가한다.
spring:
batch:
initialize-schema: always
Spring Batch를 사용하면 BATCH와 관련된 테이블들이 DB에 생성되는에 이를 항상 유지한다는 설정이다.
'java > spring' 카테고리의 다른 글
[Spring] FCM 서버 구축하기 (특정 시간대에 알림 보내기) (4) | 2021.12.10 |
---|---|
[Spring] Spring Jdbc - batchUpdate()를 사용한 bulk Insert 최적화 (0) | 2021.08.06 |
[Spring] AWS S3, csv 파일 읽어서 DB에 저장 (0) | 2021.08.05 |
[Spring] AWS S3에서 Spring Boot로 파일 다운로드 (0) | 2021.08.05 |
[Spring] Jasypt를 이용한 암호화 (2) | 2021.07.21 |