본문 바로가기
Spring Batch

[Spring Batch - 06] JpaPagingItemReader와 JpaItemWriter

by cafecortado 2024. 11. 12.
reference
:https://devocean.sk.com/blog/techBoardDetail.do?ID=166902

 

Review

지난 포스팅에서는 Spring Batch의 ItemReader와 ItemWriter의 기본 구현체 중 하나인 JdbcPagingItemReader와 JdbcBatchItemWriter에 대해 알아보았다.

 

지난 포스팅의 JdbcPagingItemReader와 JdbcBatchItemWriter, 그리고 이번 포스팅의 JpaPagingItemReader와 JpaItemWriter는 모두 데이터베이스를 통해 데이터를 처리한다는 공통점이 있다.

 

하지만 JdbcPagingItemReader와 JdbcBatchItemWriter는 JDBC 기반으로 SQL을 직접 작성해 데이터베이스와 통신하는 반면, JpaPagingItemReader와 JpaItemWriter는 JPA(Java Persistence API) 기반으로 엔티티 객체를 통해 ORM(Object-Relational Mapping) 방식으로 데이터베이스와 통신한다는 차이점이 있다.

 

ORM(Object-Relational Mapping)은 객체와 관계를 매핑한다는 의미다. 즉, 객체지향 언어의 객체와 관계형 데이터베이스의 테이블을 매핑하여, SQL 없이도 데이터베이스 작업을 객체 지향적으로 처리할 수 있게 하는 기술이다. JPA(Java Persistence API)는 대표적인 ORM 프레임워크이다.

 

 

ItemReader - JpaPagingItemReader 구현체 

JpaPagingItemReader는 이름에서 알 수 있듯이 JPA를 사용하여 페이지 단위로 데이터에서 데이터를 읽어온다.

 

아래는 데이터베이스에서 Customer 엔티티를 읽어오는 예제이다.

@Bean
public JpaPagingItemReader<Customer> customerJpaPagingItemReader() throws Exception {
    return new JpaPagingItemReaderBuilder<Customer>()
            .name("customerJpaPagingItemReader")
            .queryString("SELECT c FROM Customer c WHERE c.age > :age order by id desc")
            .pageSize(CHUNK_SIZE)
            .entityManagerFactory(entityManagerFactory)
            .parameterValues(Collections.singletonMap("age", 20))
            .build();
}
  • queryString: JPA 쿼리를 지정한다. Customer 테이블에서 age(c.age)가 지정한 값(:age) 이상인 데이터를 가져와 내림차순으로 정렬하는 쿼리이다.
  • entityManagerFactory: 데이터베이스와 연결을 위해 엔티티 매니저를 설정한다.
  • pageSize: 한 번에 읽어올 데이터의 사이즈를 지정한다.
  • parameterValues: 쿼리에 전달할 파라미터를 지정한다. 여기서는 age 파라미터(:age)를 20으로 지정한다.

 

 

ItemProcessor

아래는 Customer 엔티티를 입력받아 단순히 출력하는 예제이다.

package com.spring_batch.batch_sample.jobs.jpa;

import com.spring_batch.batch_sample.jobs.models.Customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.item.ItemProcessor;

@Slf4j
public class CustomerItemProcessor implements ItemProcessor<Customer, Customer> {
    @Override
    public Customer process(Customer item) throws Exception {
        log.info("Item Processor ------------------- {}", item);
        return item;
    }
}

 

 

ItemWriter - JpaItemWriter 구현체

JpaItemWriter는 JPA를 사용하여 ORM을 통해 객체 데이터를 데이터베이스에 저장한다.

 

아래는 Customer 엔티티 데이터를 데이터베이스에 저장하는 예제이다.

@Bean
public JpaItemWriter<Customer> jpaItemWriter() {
    return new JpaItemWriterBuilder<Customer>()
            .entityManagerFactory(entityManagerFactory)
            .usePersist(true)
            .build();
}
  • entityManagerFactory: 데이터베이스와의 연결을 관리하고 데이터를 저장하기 위해 사용되는 엔티티 매니저를 설정한다.
  • usePersist: 데이터를 저장할 때 JPA의 persist 메서드를 사용할지 여부를 지정한다.

 

 

Step 및 Job 설정

JpaPagingItemReader와 JpaItemWriter를 실행하기 위해 Step과 Job를 설정해야 한다.

 

아래는 Step을 정의하고 ItemReader, ItemProcessor, ItemWriter를 순서대로 지정하는 예시이다.

@Bean
public Step customerJpaPagingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) throws Exception {
    return new StepBuilder("customerJpaPagingStep", jobRepository)
            .<Customer, Customer>chunk(CHUNK_SIZE, transactionManager)
            .reader(customerJpaPagingItemReader())
            .processor(new CustomerItemProcessor())
            .writer(customerJpaFlatFileItemWriter())
            .build();
}

 

 

전체 코드

build.gradle 에 아래처럼 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 의존성을 추가한다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-batch'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.batch:spring-batch-test'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	implementation 'com.mysql:mysql-connector-j:8.3.0'

}

 

application.yaml 파일은 아래와 같이 설정한다.

spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      username: root
      password: root1234
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8&clusterInstanceHostPattern=?&zeroDateTimeBehavior=CONVERT_TO_NULL&allowMultiQueries=true

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    #    defer-datasource-initialization: true
    properties:
      hibernate:
        format_sql: true
  batch:
    job:
      name: JPA_ITEM_WRITER_JOB

 

package com.spring_batch.batch_sample.jobs.models;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "customer")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String name;
    private Integer age;
    private String gender;

}

 

package com.spring_batch.batch_sample.jobs.jpa;

import com.spring_batch.batch_sample.jobs.models.Customer;
import jakarta.persistence.EntityManagerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.database.JdbcPagingItemReader;
import org.springframework.batch.item.database.JpaPagingItemReader;
import org.springframework.batch.item.database.Order;
import org.springframework.batch.item.database.PagingQueryProvider;
import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder;
import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder;
import org.springframework.batch.item.database.orm.JpaNativeQueryProvider;
import org.springframework.batch.item.database.support.MySqlPagingQueryProvider;
import org.springframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean;
import org.springframework.batch.item.file.FlatFileItemWriter;
import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Configuration
public class JpaPagingReaderJobConfig {

    /**
     * CHUNK 크기를 지정한다.
     */
    public static final int CHUNK_SIZE = 2;
    public static final String ENCODING = "UTF-8";
    public static final String JPA_PAGING_CHUNK_JOB = "JPA_PAGING_CHUNK_JOB";

    @Autowired
    DataSource dataSource;

    @Autowired
    EntityManagerFactory entityManagerFactory;

    @Bean
    public JpaPagingItemReader<Customer> customerJpaPagingItemReader() throws Exception {

        return new JpaPagingItemReaderBuilder<Customer>()
                .name("customerJpaPagingItemReader")
                .queryString("SELECT c FROM Customer c WHERE c.age > :age order by id desc")
                .pageSize(CHUNK_SIZE)
                .entityManagerFactory(entityManagerFactory)
                .parameterValues(Collections.singletonMap("age", 20))
                .build();
    }


//    @Bean
//    public JpaPagingItemReader<Customer> customerJpaPagingItemReader() throws Exception {
//        JpaPagingItemReader<Customer> jpaPagingItemReader = new JpaPagingItemReader<>();
//        jpaPagingItemReader.setQueryString(
//                "SELECT c FROM Customer c WHERE c.age > :age order by id desc"
//        );
//        jpaPagingItemReader.setEntityManagerFactory(entityManagerFactory);
//        jpaPagingItemReader.setPageSize(CHUNK_SIZE);
//        jpaPagingItemReader.setParameterValues(Collections.singletonMap("age", 20));
//        return jpaPagingItemReader;
//    }

    @Bean
    public FlatFileItemWriter<Customer> customerJpaFlatFileItemWriter() {

        return new FlatFileItemWriterBuilder<Customer>()
                .name("customerJpaFlatFileItemWriter")
                .resource(new FileSystemResource("./output/customer_new_v2.csv"))
                .encoding(ENCODING)
                .delimited().delimiter("\t")
                .names("Name", "Age", "Gender")
                .build();
    }


    @Bean
    public Step customerJpaPagingStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) throws Exception {
        log.info("------------------ Init customerJpaPagingStep -----------------");

        return new StepBuilder("customerJpaPagingStep", jobRepository)
                .<Customer, Customer>chunk(CHUNK_SIZE, transactionManager)
                .reader(customerJpaPagingItemReader())
                .processor(new CustomerItemProcessor())
                .writer(customerJpaFlatFileItemWriter())
                .build();
    }

    @Bean
    public Job customerJpaPagingJob(Step customerJdbcPagingStep, JobRepository jobRepository) {
        log.info("------------------ Init customerJpaPagingJob -----------------");
        return new JobBuilder(JPA_PAGING_CHUNK_JOB, jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(customerJdbcPagingStep)
                .build();
    }
}

 

package com.spring_batch.batch_sample.jobs.models;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "customer2")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String name;
    private Integer age;
    private String gender;
}

 

package com.spring_batch.batch_sample.jobs.jpawriter;


import com.spring_batch.batch_sample.jobs.models.Customer;
import jakarta.persistence.EntityManagerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.database.JpaItemWriter;
import org.springframework.batch.item.database.builder.JpaItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

@Slf4j
@Configuration
public class JpaItemJobConfig {

    /**
     * CHUNK 크기를 지정한다.
     */
    public static final int CHUNK_SIZE = 100;
    public static final String ENCODING = "UTF-8";
    public static final String JPA_ITEM_WRITER_JOB = "JPA_ITEM_WRITER_JOB";

    @Autowired
    EntityManagerFactory entityManagerFactory;

    @Bean
    public FlatFileItemReader<Customer> flatFileItemReader() {

        return new FlatFileItemReaderBuilder<Customer>()
                .name("FlatFileItemReader")
                .resource(new ClassPathResource("./customer.csv"))
                .encoding(ENCODING)
                .delimited().delimiter(",")
                .names("name", "age", "gender")
                .targetType(Customer.class)
                .build();
    }

    @Bean
    public JpaItemWriter<Customer> jpaItemWriter() {
        return new JpaItemWriterBuilder<Customer>()
                .entityManagerFactory(entityManagerFactory)
                .usePersist(true)
                .build();
    }


    @Bean
    public Step flatFileStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        log.info("------------------ Init flatFileStep -----------------");

        return new StepBuilder("flatFileStep", jobRepository)
                .<Customer, Customer>chunk(CHUNK_SIZE, transactionManager)
                .reader(flatFileItemReader())
                .writer(jpaItemWriter())
                .build();
    }

    @Bean
    public Job flatFileJob(Step flatFileStep, JobRepository jobRepository) {
        log.info("------------------ Init flatFileJob -----------------");
        return new JobBuilder(JPA_ITEM_WRITER_JOB, jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(flatFileStep)
                .build();
    }
}

 

정리

  • JpaPagingItemReader와 JpaItemWriter: 객체 지향적이며 ORM을 통해 데이터베이스 독립성을 가진다.
  • JdbcPagingItemReader와 JdbcItemWriter: SQL을 직접 작성하는 방식으로 데이터베이스에 의존적이지만 빠르기 때문에 성능이 중요한 경우에 유리하다.
  • FlatFileItemReader와 FlatFileItemWriter: 파일에서 데이터를 읽거나 파일에 데이터를 기록하는 경우에 사용되며, 데이터베이스와 무관하게 파일 입출력이 필요한 작업에 유용하다.