Post

[Spring Batch 7] MyBatisPagingItemReader/MyBatisItemWriter 구현

Spring Batch의 MyBatisPagingItemReaderMyBatisItemWriter로 DB 데이터를 읽고 쓰는 방법을 알아보자.

1. MyBatisPagingItemReader/MyBatisItemWriter 개요

1-1. MyBatisPagingItemReader 개요

  • Spring Batch에서 제공하는 ItemReader 인터페이스를 구현하는 클래스이다.
  • 장점
    • 간편한 설정: MyBatis 쿼리 매퍼를 직접 활용하여 데이터를 읽을 수 있고 설정이 간편하다.
    • 쿼리 최적화: MyBatis의 다양한 기능을 활용하여 최적화된 쿼리를 작성할 수 있다.
    • 동적 쿼리 지원: 런타임 시 조건에 따라 동적으로 쿼리를 생성할 수 있다.
  • 단점
    • MyBatis 의존성: MyBatis 라이브러리에 의존해야 한다.
    • 커스터마이징 복잡: Chunk-oriented Processing 방식과 비교했을 때 커스터마이징이 더 복잡할 수 있다.

1-2. MyBatisPagingItemReader 주요 구성 요소

  • SqlSessionFactory
    • MyBatis 설정 정보 및 SQL 쿼리 매퍼 정보를 담고 있는 객체이다.
    • 설정은 @Bean, Spring Batch XML, Java 코드로 직접 설정 가능하다.
  • QueryId
    • 데이터를 읽을 MyBatis 쿼리 ID이다.
    • MyBatisPagingItemReader setQueryId() 메소드를 통해 데이터를 읽을 MyBatis 쿼리 ID를 설정한다.
    • 쿼리 ID는 com.example.mapper.CustomerMapper.selectCustomers 와 같은 형식으로 지정된다.
  • PageSize
    • 페이징 쿼리를 위한 페이지 크기를 지정한다.
  • SkippableItemReader
    • 오류 발생 시 해당 Item을 건너뛸 수 있도록 한다.
  • ReadListener
    • 읽기 시작, 종료, 오류 발생 등의 이벤트를 처리할 수 있도록 한다.
  • SaveStateCallback
    • 잡의 중단 시 현재 상태를 저장하여 재시작 시 이어서 처리할 수 있도록 한다.

1-3. MyBatisItemWriter 개요

  • Spring Batch에서 제공하는 ItemWriter 인터페이스를 구현하는 클래스이다.
  • 데이터를 MyBatis를 통해 데이터베이스에 저장하는 데 사용된다.
  • 장점
    • ORM 연동: MyBatis를 통해 다양한 데이터베이스에 저장할 수 있다.
    • SQL 쿼리 분리: SQL 쿼리를 Java 코드로부터 분리하여 관리 및 유지 보수가 용이하다.
    • 유연성: 다양한 설정을 통해 원하는 방식으로 데이터를 저장할 수 있다.
  • 단점
    • 설정 복잡성: MyBatis 설정 및 SQL 매퍼 작성이 복잡할 수 있다.
    • 데이터베이스 종속: 특정 데이터베이스에 종속적이다.
    • 오류 가능성: 설정 오류 시 데이터 손상 가능성이 있다.

1-4. MyBatisItemWriter 주요 구성 요소

  • SqlSessionTemplate
    • MyBatis SqlSession 생성 및 관리를 위한 템플릿 객체이다.
  • SqlSessionFactory
    • SqlSessionTemplate 생성을 위한 팩토리 객체이다.
  • StatementId
    • 실행할 MyBatis SQL 맵퍼의 statement ID이다.
  • ItemToParameterConverter
    • 객체를 ParameterMap으로 변경할수 있다.



2. MyBatisPagingItemReader 구현

MyBatisPagingItemReader를 활용하여 db의 customer 테이블로 부터 데이터를 읽어들이고 flatfile(csv)로 저장하는 로직을 구현해보자.

구현에 앞서 먼저 mybatis-spring 관련 라이브러리 의존성을 추가한다

1
2
3
4
    // SpringBoot 3.2.2 기준 myBatis 관련 의존성 추가
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
    implementation 'org.mybatis:mybatis-spring:3.0.4'
    implementation 'org.mybatis:mybatis:3.5.16'

다음으로 myBatis에서 쿼리 매퍼로 사용할 xml 파일을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.schooldevops.springbatch.batchstudy.jobs">

  <resultMap id="customerResult" type="com.schooldevops.springbatch.batchstudy.models.Customer">
    <result property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="age" column="age"/>
    <result property="gender" column="gender"/>
  </resultMap>

  <select id="selectCustomers" resultMap="customerResult">
    SELECT id, name, age, gender
    FROM customer
    LIMIT #{_skiprows}, #{_pagesize}
  </select>
</mapper>
  • namespace: 쿼리들을 그룹화해서 모아놓은 이름 공간이다.
  • resultMap: 결과로 반환할 결과맵을 지정한다. 이는 db 칼럼과, java 필드 이름을 매핑한다.
  • select: 쿼리를 지정한다.
  • #{_skiprows}: 오프셋을 이야기하며, 쿼리 결과에서 얼마나 스킵할지 지정한다. pageSize를 지정했다면 자동으로 계산된다.
  • #{_pagesize}: 한번에 가져올 페이지를 지정한다.

application.yml 파일에도 mapper 파일의 위치를 지정해준다.

1
2
mybatis:
  mapper-locations: classpath:/mappers/**/*.xml

2-1. 코드 구현

MyBatisPagingItemReader 빈을 구현해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
@Configuration
public class MyBatisReaderJobConfig {

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

  @Autowired
  SqlSessionFactory sqlSessionFactory;

  @Bean
  public MyBatisPagingItemReader<Customer> myBatisItemReader() throws Exception {

    return new MyBatisPagingItemReaderBuilder<Customer>()
      .sqlSessionFactory(sqlSessionFactory)
      .pageSize(CHUNK_SIZE)
      .queryId("com.schooldevops.springbatch.batchstudy.jobs.selectCustomers")
      .build();
  }

...
  • sqlSessionFactory: SqlSession을 생성하는 팩토리 객체를 지정한다.
  • pageSize: 페이징 단위를 지정한다.
  • queryId: 쿼리 매퍼 파일에서 사용할 쿼리 id를 지정한다.

나머지 Job, Step도 마저 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...

@Bean
public FlatFileItemWriter<Customer> CustomerCursorFlatFileItemWriter() {
  return new FlatFileItemWriterBuilder<Customer>()
    .name("CustomerCursorFlatFileItemWriter")
    .resource(new FileSystemResource("./output/Customer_new_v4.csv"))
    .encoding(ENCODING)
    .delimited().delimiter("\t")
    .names("Name", "Age", "Gender")
    .build();
}

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

  return new StepBuilder("CustomerJdbcCursorStep", jobRepository)
    .<Customer, Customer>chunk(CHUNK_SIZE, transactionManager)
    .reader(myBatisItemReader())
    .processor(new CustomerItemProcessor())
    .writer(CustomerCursorFlatFileItemWriter())
    .build();
}

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

2-2. 실행 및 결과

먼저 customer 테이블에서 읽어들일 데이터를 저장해둔다.

img.png

스프링배치를 실행하여 FlatFileItemWriter 빈에서 출력 파일로 지정했던 Customer_new_v4.csv 파일이 생성되고, 데이터가 잘 읽어왔는지 확인해보자.

img_1.png

정상적으로 csv 파일이 생성되고 데이터를 읽어온 것을 확인할 수 있다.



3. MyBatisItemWriter 구현

MyBatisItemWriter를 활용하여 flatfile에서 데이터를 읽어들인 후 customer2 테이블에 저장하는 로직을 구현해보자.

먼저 customer.csv 파일에 불러올 데이터를 저장해두자.

img_2.png

다음에는 쿼리 매퍼 파일에 customer 테이블에 insert를 하기 위한 쿼리문을 추가한다.

1
2
3
4
5
6
...

<insert id="insertCustomers" parameterType="com.schooldevops.springbatch.batchstudy.models.Customer">
  INSERT INTO customer2(name, age, gender) VALUES (#{name}, #{age}, #{gender});
</insert>
  </mapper>

3-1. 코드 구현

MyBatisItemWriter 빈을 구현해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@Configuration
public class MybatisItemJobConfig {

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

  @Autowired
  SqlSessionFactory sqlSessionFactory;

  @Bean
  public MyBatisBatchItemWriter<Customer> mybatisItemWriter() {
    return new MyBatisBatchItemWriterBuilder<Customer>()
      .sqlSessionFactory(sqlSessionFactory)
      .statementId("com.schooldevops.springbatch.batchstudy.jobs.insertCustomers")
      .build();
  }

...
  • sqlSessionFactory: SqlSession을 생성하는 팩토리 객체를 지정한다.
  • statementId: 쿼리 매퍼 파일에서 사용할 insert 쿼리문의 id를 지정한다.

나머지 Job, Step도 마저 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
...

@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 Step flatFileStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
  log.info("------------------ Init flatFileStep -----------------");

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

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

3-2. 실행 및 결과

스프링 배치를 실행하여 customer.csv 파일의 데이터가 customer2 테이블에 잘 저장되었는지 확인해보자.

img_3.png

정상적으로 저장된 것을 확인할 수 있다.


참고: DEVOCEAN KIDO님 SpringBatch 연재 07


This post is licensed under CC BY 4.0 by the author.