danuri
오늘의 기록
danuri
전체 방문자
오늘
어제
  • 오늘의 기록 (307)
    • java (150)
      • java (33)
      • spring (63)
      • jpa (36)
      • querydsl (7)
      • intelliJ (9)
    • kotlin (8)
    • python (24)
      • python (10)
      • data analysis (13)
      • crawling (1)
    • ddd (2)
    • chatgpt (2)
    • algorithm (33)
      • theory (9)
      • problems (23)
    • http (8)
    • git (8)
    • database (5)
    • aws (12)
    • devops (10)
      • docker (6)
      • cicd (4)
    • book (44)
      • clean code (9)
      • 도메인 주도 개발 시작하기 (10)
      • 자바 최적화 (11)
      • 마이크로서비스 패턴 (0)
      • 스프링으로 시작하는 리액티브 프로그래밍 (14)
    • tistory (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

태그

  • nuribank
  • 등가속도 운동
  • S3
  • CICD
  • SWAGGER
  • mockito
  • Bitmask
  • Kotlin
  • Database
  • DDD
  • docker
  • POSTGIS
  • AWS
  • JPA
  • Thymeleaf
  • Spring
  • RDS
  • 트랜잭션
  • 마이크로서비스패턴
  • Security
  • 자바 최적화
  • Jackson
  • Java
  • ChatGPT
  • Saving Plans
  • connection
  • reactive
  • gitlab
  • PostgreSQL
  • 도메인 주도 설계

최근 댓글

최근 글

hELLO · Designed By 정상우.
danuri

오늘의 기록

java/spring

[Spring] SpringBatch를 사용해 csv 파일 읽어 DB에 저장

2021. 8. 6. 00:55

 

프로젝트 폴더에 있는 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에 생성되는에 이를 항상 유지한다는 설정이다.

Hola

'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
    'java/spring' 카테고리의 다른 글
    • [Spring] FCM 서버 구축하기 (특정 시간대에 알림 보내기)
    • [Spring] Spring Jdbc - batchUpdate()를 사용한 bulk Insert 최적화
    • [Spring] AWS S3, csv 파일 읽어서 DB에 저장
    • [Spring] AWS S3에서 Spring Boot로 파일 다운로드
    danuri
    danuri
    IT 관련 정보(컴퓨터 지식, 개발)를 꾸준히 기록하는 블로그입니다.

    티스토리툴바