아래 글은 한국 스프링 사용자 모임(KSUG)에서 진행된 스프링 배치 스터디 내용을 정리한 게시글입니다.
DEVOCEAN에 연재 중인 KIDO님의 글을 참고하여 실습한 내용을 기록했습니다.
원본: [SpringBatch 연재 07] MyBatisPagingItemReader로 DB내용을 읽고, MyBatisItemWriter로 DB에 쓰기
MyBatis는 자바 기반의 SQL 매퍼 프레임워크로, 애플리케이션에서 데이터베이스와 상호작용하기 위한 도구입니다. 주로 SQL 문을 직접 작성하고 이를 자바 코드와 연결하는 데 사용됩니다.
JPA, Hibernate, MyBatis의 차이가 헷갈려서, 먼저 각 개념이 무엇인지 정리해보았습니다.
특징 | JPA | Hibernate | MyBatis |
쿼리 작성 | JPQL (추상화된 SQL) 사용 | HQL 사용 | SQL 직접 작성 |
자동화 수준 | CRUD 자동화 (Spring Data JPA) | ORM으로 대부분 자동화 | SQL 제어 필요 |
복잡한 쿼리 | JPQL로 작성 가능하지만 제약 있음 | HQL 또는 네이티브 SQL 사용 가능 | SQL 작성으로 제약 없음 |
초기 설정 | 비교적 간단 | 비교적 간단 | XML 매퍼 등 설정 추가 필요 |
유연성 | 엔티티 중심으로 유연성 제한 | 엔티티 중심 | SQL 기반으로 유연함 |
- JPA는 CRUD 중심 애플리케이션에 적합하며 빠른 개발 속도를 제공합니다.
- Hibernate는 더 세밀한 제어와 확장성을 원할 때 적합합니다.
- MyBatis는 복잡한 SQL을 제어하고 작성해야 하는 프로젝트에 적합합니다.
MyBatisItemReader
Spring Mybatis에서 제공하는 ItemReader 인터페이스를 구현하는 클래스입니다.
MyBatis의 Object Relation Mapper를 이용합니다.
장점
- 간편한 설정: MyBatis 쿼리 매퍼를 직접 활용하여 데이터를 읽을 수 있어 설정이 간단합니다.
- 쿼리 최적화: MyBatis의 다양한 기능을 통해 최적화된 쿼리를 작성할 수 있습니다.
- 동적 쿼리 지원: 런타임 시 조건에 따라 동적으로 쿼리를 생성할 수 있습니다.
단점
- MyBatis 의존성: MyBatis 라이브러리에 대한 의존성이 존재합니다.
- 커스터마이징 복잡: Chunk-oriented Processing 방식에 비해 커스터마이징이 더 복잡할 수 있습니다.
주요 구성 요소
- SqlSessionFactory: SqlSessionFactory는 아래와 같은 방법으로 설정하실 수 있습니다.
- @Bean 어노테이션을 사용하여 직접 생성
- Spring Batch XML 설정 파일에서 설정
- Java 코드에서 직접 생성 적절한 설정을 통해 다양한 데이터베이스 환경에서도 데이터를 안정적으로 읽을 수 있도록 도와줍니다.
- MyBatisPagingItemReader는 SqlSessionFactory 객체를 통해 MyBatis와 연동됩니다.
- QueryId
- 쿼리 ID는 com.example.mapper.CustomerMapper.selectCustomers와 같은 형식으로 지정됩니다.
- 여러 테이블에서 데이터를 읽어야 할 경우, 네임스페이스를 활용하여 매퍼 파일을 구성하는 것이 좋습니다.
- 쿼리를 참조할 때는 매퍼 파일의 네임스페이스를 반드시 확인하시기 바랍니다.
- MyBatisPagingItemReader의 setQueryId() 메소드를 통해 데이터를 읽을 MyBatis 쿼리 ID를 설정합니다.
- ParameterValues
- JobExecutionContext의 값을 가져오는 SpEL 표현식을 사용할 수 있습니다.
- 예: #{yesterday,jdbcType=TIMESTAMP}와 같이 매퍼 파일에서 파라미터를 지정할 수 있습니다.
- 이때, MyBatis의 타입 핸들러가 적절히 설정되어 있다면 JodaTime 날짜 객체도 파라미터로 전달 가능합니다.
- step 스코프를 사용하면 JobExecutionContext를 통해 SpEL 표현식을 적용할 수 있습니다.
- 추가적인 파라미터는 parameterValues 맵을 통해 전달 가능합니다.
- PageSize
- pageSize는 MyBatisPagingItemReader가 데이터를 읽을 때 사용하는 배치 단위의 크기를 결정합니다.
- 이를 통해 offset과 limit 기반의 페이징 처리가 이루어지며, 효율적인 데이터 처리가 가능합니다.
- 청크 기반으로 데이터 처리가 이루어질 경우, 이 값을 적절히 설정하는 것이 중요합니다.
추가 구성 요소
- SkippableItemReader
- 데이터 읽기 과정에서 오류가 발생했을 경우, 해당 Item을 건너뛰도록 설정할 수 있습니다.
- 이를 통해 프로세스의 중단 없이 처리가 지속되도록 유연성을 제공합니다.
- ReadListener
- 데이터 읽기 시작, 종료, 오류 발생 등의 이벤트를 처리할 수 있습니다.
- 예를 들어, 읽기 과정 중 로그를 남기거나 특정 작업을 트리거하는 데 유용하게 활용됩니다.
- SaveStateCallback
- 잡이 중단되었을 때 현재 상태를 저장하여, 이후 재시작 시 이어서 처리할 수 있도록 도와줍니다.
실습
1. build.gradle 에 MyBatis 의존성 추가
// MyBatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'
2. customer.xml 파일 생성
resources 폴더 아래에 생성해야 하며, 저는 resources/xml_mybatis 폴더에 만들어 두었습니다.
<?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">
<!--namespace: XML Mapper 파일의 식별자, 쿼리들을 그룹화 해서 모아놓은 이름 공간-->
<mapper namespace="batch_sample.jobs">
<resultMap id="customerResult" type="com.example.batch_sample.jobs.task06.Customer">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<result property="gender" column="gender"/>
</resultMap>
<!-- resultMap: 결과로 반환할 결과맵. db column과 java field 이름 매핑 -->
<select id="selectCustomers" resultMap="customerResult">
-- 쿼리 지정
SELECT id, name, age, gender
FROM customer
LIMIT #{_skiprows}, #{_pagesize}
-- _skiprows: 오프셋. 쿼리 별과에서 얼마나 스킵할지 지정. pageSize를 지정했다면 자동으로 계산 됨
-- _pagesize: 한 번에 가져올 페이지 지정
</select>
</mapper>
application.yaml 파일에 만든 xml 파일의 위치를 설정합니다.
mybatis:
mapper-locations: classpath:/xml_mybatis/customer.xml
3. 전체 코드
@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
DataSource dataSource;
@Autowired
SqlSessionFactory sqlSessionFactory;
/**
* DB 쿼리 결과를 읽을 수 있도록 ItemReader 를 반환한다.
* @return MyBatisPagingItemReader
*/
@Bean
public MyBatisPagingItemReader<Customer> myBatisItemReader() throws Exception {
return new MyBatisPagingItemReaderBuilder<Customer>()
.sqlSessionFactory(sqlSessionFactory) // 세션 팩토리 지정
.pageSize(CHUNK_SIZE) // 페이징 단위 지정
.queryId("batch_sample.jobs.selectCustomers") // 네임스페이스 + SQL ID
.build();
}
@Bean
public FlatFileItemWriter<Customer> task07customerCursorFlatFileItemWriter() {
return new FlatFileItemWriterBuilder<Customer>()
.name("customerCursorFlatFileItemWriter")
.resource(new FileSystemResource("./output/task07_customer_new_v4.csv"))
.encoding(ENCODING)
.delimited().delimiter("\\t")
.names("Name", "Age", "Gender")
.build();
}
@Bean
public Step task07customerJdbcCursorStep(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(task07customerCursorFlatFileItemWriter())
.build();
}
@Bean
public Job task07customerJdbcCursorPagingJob(Step task07customerJdbcCursorStep, JobRepository jobRepository) {
log.info("------------------ Init customerJdbcCursorPagingJob -----------------");
return new JobBuilder(MYBATIS_CHUNK_JOB, jobRepository)
.incrementer(new RunIdIncrementer())
.start(task07customerJdbcCursorStep)
.build();
}
}
MyBatisItemWriter
Spring Batch에서 제공하는 ItemWriter 인터페이스를 구현한 클래스입니다.
MyBatis를 통해 데이터를 데이터베이스에 저장하는 데 사용됩니다.
구성 요소
- SqlSessionTemplate
- MyBatis의 SqlSession 생성을 관리하는 템플릿 객체입니다.
- SqlSessionFactory
- SqlSessionTemplate 생성을 위한 팩토리 객체입니다.
- StatementId
- 실행할 MyBatis SQL 맵퍼의 Statement ID입니다.
- ItemToParameterConverter
- 객체를 ParameterMap으로 변환하는 기능을 제공합니다.
구분 | 내용 | |
장점 | ORM 연동 | MyBatis를 활용하여 다양한 데이터베이스에 데이터를 저장할 수 있습니다. |
SQL 쿼리 분리 | SQL 쿼리를 Java 코드에서 분리하여 관리 및 유지 보수가 편리합니다. | |
유연성 | 설정을 통해 데이터를 다양한 방식으로 저장할 수 있습니다. | |
단점 | 설정 복잡성 | MyBatis 설정 및 SQL 맵퍼 작성이 다소 복잡할 수 있습니다. |
데이터베이스 종속 | 특정 데이터베이스 환경에 의존적일 수 있습니다. | |
오류 가능성 | 설정 오류로 인해 데이터 손상이 발생할 가능성이 있습니다. |
실습하기
1. Customer.xml 파일에 insertCustomers 추가하기
<insert id="insertCustomers" parameterType="com.example.batch_sample.jobs.task06.Customer">
INSERT INTO customer2(name, age, gender) VALUES (#{name}, #{age}, #{gender});
</insert>
2. MyBatisBatchItemWriter 메서드 만들기
@Bean
public MyBatisBatchItemWriter<Customer> task07mybatisItemWriter() {
return new MyBatisBatchItemWriterBuilder<Customer>()
.sqlSessionFactory(sqlSessionFactory)
.statementId("batch_sample.jobs.insertCustomers")
.build();
}
Map으로 파라미터를 전달한다면 다음과 같이 작성할수도 있습니다.
@Bean
public MyBatisBatchItemWriter<Customer> mybatisItemWriter() {
return new MyBatisBatchItemWriterBuilder<Customer>()
.sqlSessionFactory(sqlSessionFactory)
.statementId("batch_sample.jobs.insertCustomers")
.itemToParameterConverter(item -> {
Map<String, Object> parameter = new HashMap<>();
parameter.put("name", item.getName());
parameter.put("age", item.getAge());
parameter.put("gender", item.getGender());
return parameter;
})
.build();
}
해당 방법을 사용하면 객체의 필드 이름과 Mapper의 SQL 파라미터 이름이 달라도 커스터마이징이 가능해 유연성이 높습니다.
결과
mysql> select * from customer2;
+----+---------+-----+--------+
| id | name | age | gender |
+----+---------+-----+--------+
| 1 | Alice | 30 | F |
| 2 | Bob | 45 | M |
| 3 | Charlie | 25 | M |
| 4 | Diana | 29 | F |
| 5 | Evan | 35 | M |
| 6 | Fiona | 40 | F |
| 7 | George | 55 | M |
| 8 | Hannah | 32 | F |
| 9 | Alice | 30 | F |
| 10 | Bob | 45 | M |
| 11 | Charlie | 25 | M |
| 12 | Diana | 29 | F |
| 13 | Evan | 35 | M |
| 14 | Fiona | 40 | F |
| 15 | George | 55 | M |
| 16 | Hannah | 32 | F |
+----+---------+-----+--------+
16 rows in set (0.00 sec)
WrapUp
- MyBatisPagingItemReader, MyBatisBatchItemWriter를 사용 해보았습니다.
MyBatis는 처음 쓰는 거라 조금 헷갈리고 생소했지만 새로운 경험이어서 좋았습니다.
'알아두면 좋은 개발 지식 > Spring Batch 스터디' 카테고리의 다른 글
[8회차] CompositeItemProcessor 으로 여러단계에 걸쳐 데이터 Transform하기 (1) | 2024.11.28 |
---|---|
[7회차] Spring Batch 스터디: 후기 및 추가 학습 내용 (4) | 2024.11.20 |
[6회차] Spring Batch 스터디: 후기 및 추가 학습 내용 (2) | 2024.11.16 |
[6회차] JpaPagingItemReader로 DB내용을 읽고, JpaItemWriter로 DB에 쓰기 (2) | 2024.11.08 |
[5회차] Spring Batch 스터디: 후기 및 추가 학습 내용 (1) | 2024.11.05 |