Spring batch writer 의 구현체는 아래와 같은 종류들이 존재한다.
- Jdbc 및 JPA 는 mysql 8.0 을 기준으로 테스트 진행
1.1. JdbcBatchItemWriter
- JDBC를 사용하여 관계형 데이터베이스에 데이터를 배치로 쓰는 Writer
- 대량의 데이터를 효율적으로 삽입할 수 있으며, 트랜잭션 관리가 용이
AUTO_INCREMENT 사용하지 않은 경우
- GenerationType.AUTO 을 사용하는 경우 Domain 과 쿼리가 같아 테스트 진행하지 않음
@Test
fun testDomainJdbcBatchItemWriter() {
// given
val writer = JdbcBatchItemWriterBuilder<Domain>()
.dataSource(jdbcTemplate.dataSource!!)
.sql("INSERT INTO domain (id, name) VALUES (:id, :name)")
.beanMapped()
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> Domain(id = i.toLong(), name = "test_jdbc_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("JdbcBatchItemWriter (Domain): Time taken to write $count items: ${timeTaken}ms")
}
2024-11-13T06:45:41.877+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : JdbcBatchItemWriter (Domain): Time taken to write 10000 items: 3238ms
AUTO_INCREMENT 사용시
@Test
fun testIdentityDomainJdbcBatchItemWriter() {
// given
val writer = JdbcBatchItemWriterBuilder<IdentityDomain>()
.dataSource(jdbcTemplate.dataSource!!)
.sql("INSERT INTO identity_domain (name) VALUES (:name)")
.beanMapped()
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> IdentityDomain(name = "test_jdbc_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("JdbcBatchItemWriter (IdentityDomain): Time taken to write $count items: ${timeTaken}ms")
}
2024-11-13T06:45:52.738+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : JdbcBatchItemWriter (IdentityDomain): Time taken to write 10000 items: 3415ms
약 5.47%% 의 성능 차이를 보였다
1.2. JpaItemWriter
- JPA를 사용하여 엔티티를 데이터베이스에 저장하는 Writer
- ORM의 이점을 활용할 수 있으나, 대량 데이터 처리 시 성능 이슈가 발생할 가능성이 존재
생성 전략이 존재 하지 않는 경우
@Test
fun testDomainJpaItemWriter() {
// given
val writer = JpaItemWriterBuilder<Domain>()
.usePersist(true)
.entityManagerFactory(entityManagerFactory)
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> Domain(id = i.toLong(), name = "test_domain_jpa_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("JpaItemWriter (Domain): Time taken to write $count items: ${timeTaken}ms")
}
Hibernate: select d1_0.id,d1_0.name from domain d1_0 where d1_0.id=?
Hibernate: select d1_0.id,d1_0.name from domain d1_0 where d1_0.id=?
Hibernate: select d1_0.id,d1_0.name from domain d1_0 where d1_0.id=?
Hibernate: select d1_0.id,d1_0.name from domain d1_0 where d1_0.id=?
...
Hibernate: insert into domain (name,id) values (?,?)
Hibernate: insert into domain (name,id) values (?,?)
Hibernate: insert into domain (name,id) values (?,?)
Hibernate: insert into domain (name,id) values (?,?)
2024-11-13T06:45:49.271+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : JpaItemWriter (Domain): Time taken to write 10000 items: 7283ms
테스트 코드에서 usePersist
를 사용하였는데 이는 신규 엔티티에 대하여 사용할때 merge
에 비하여 유용하다.
persist
의 경우 id
가 있는 상태의 엔티티 저장시 에러가 발생하게 된다.
usePersist
는 기본 설정은 false
이며 이 경우 merge
가 사용되는데 이는 기존 엔티티를 업데이트하거나, 영속성 컨텍스트에 없는 엔티티를 병합한다.
@Test
fun testMergeDomainJpaItemWriter() {
// given
val writer = JpaItemWriterBuilder<Domain>()
.entityManagerFactory(entityManagerFactory)
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> Domain(id = i.toLong(), name = "test_domain_jpa_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("JpaItemWriter (Domain): Time taken to write $count items: ${timeTaken}ms")
}
Hibernate: select d1_0.id,d1_0.name from domain d1_0 where d1_0.id=?
Hibernate: insert into domain (name,id) values (?,?)
Hibernate: insert into domain (name,id) values (?,?)
2024-11-13T06:46:03.805+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : JpaItemWriter (Domain): Time taken to write 10000 items: 7576ms
신규 엔티티 생성시 merge
를 사용하는 경우 4.02% 정도의 성능 차이를 보여준다.
그리고 JPA 에서 다시 생성 전략이 존재 하지 않는 경우의 실행된 쿼리를 보면 select
를 진행 한 후 insert
를 진행하게 되며 오랜 시간이 소모 되었음을 알 수 있다.
GenerationType.AUTO(Sequence)
를 사용한 경우
@Test
fun testAutoDomainJpaItemWriter() {
// given
val writer = JpaItemWriterBuilder<AutoDomain>()
.usePersist(true)
.entityManagerFactory(entityManagerFactory)
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> AutoDomain(name = "test_auto_jpa_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("JpaItemWriter (AutoDomain): Time taken to write $count items: ${timeTaken}ms")
}
Hibernate: select next_val as id_val from auto_domain_seq for update
Hibernate: update auto_domain_seq set next_val= ? where next_val=?
Hibernate: select next_val as id_val from auto_domain_seq for update
Hibernate: update auto_domain_seq set next_val= ? where next_val=?
Hibernate: select next_val as id_val from auto_domain_seq for update
Hibernate: update auto_domain_seq set next_val= ? where next_val=?
...
Hibernate: insert into auto_domain (name,id) values (?,?)
Hibernate: insert into auto_domain (name,id) values (?,?)
Hibernate: insert into auto_domain (name,id) values (?,?)
2024-11-13T06:45:56.188+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : JpaItemWriter (AutoDomain): Time taken to write 10000 items: 3384ms
실행된 쿼리를 보면 id 의 값에 대해 select
와 update
진행 한 후 insert
를 진행하게 됨을 알 수 있다.
이는 Auto 에서 Sequence 를 적용하였는데 이와 관련된 내용은 GenerationType.AUTO인데 SEQUENCE로 동작하나요? 를 확인해보면 알 수 있다.
Sequence 의 경우 생성 전략이 없는 경우 보단 아니지만 Jdbc 를 이용한 경우에 비하면 시간이 소모 되었다.
GenerationType.IDENTITY
을 사용한 경우
@Test
fun testIdentityDomainJpaItemWriter() {
// given
val writer = JpaItemWriterBuilder<IdentityDomain>()
.usePersist(true)
.entityManagerFactory(entityManagerFactory)
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> IdentityDomain(name = "test_identity_jpa_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("JpaItemWriter (IdentityDomain): Time taken to write $count items: ${timeTaken}ms")
}
Hibernate: insert into identity_domain (name) values (?)
Hibernate: insert into identity_domain (name) values (?)
Hibernate: insert into identity_domain (name) values (?)
Hibernate: insert into identity_domain (name) values (?)
Hibernate: insert into identity_domain (name) values (?)
Hibernate: insert into identity_domain (name) values (?)
2024-11-13T06:46:07.091+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : JpaItemWriter (IdentityDomain): Time taken to write 10000 items: 3245ms
실행된 쿼리를 보면 select
없이 insert
가 진행되었음을 보여준다
전반적으로 Jdbc 와 비교했을때 더 느린 성능을 보여주며 특히 ID 생성 전략이 없는 경우 가장 느린 결과를 보여준다.
1.3. FlatFileItemWriter
- CSV, TXT 등 평문 파일에 데이터를 쓰는 Writer
- 간단한 파일 출력에 적합하며, 포맷 지정 가능
성능에 대해 비교할 만한 writer
가 없어 테스트는 진행하지 않는다
1.4. MongoItemWriter
- MongoDB에 데이터를 쓰는 Writer
- 비정형 데이터 저장에 유리하며, 스케일링에 장점
mysql 이랑 비교하긴 애매하나 궁금해서 테스트 해보았다
@Test
fun testMongoItemWriter() {
// given
mongoTemplate.setWriteConcern(WriteConcern.ACKNOWLEDGED)
val writer = MongoItemWriterBuilder<MongoDomain>()
.template(mongoTemplate)
.collection("mongo_domains")
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> MongoDomain(name = "test_mongo_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("MongoItemWriter: Time taken to write $count items: ${timeTaken}ms")
}
2024-11-13T06:46:07.128+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : MongoItemWriter: Time taken to write 10000 items: 1ms
결과를 보면 매우 빠른 성능을 보여준다. 그러나 MongoDB 의 경우
WriteConcern 에 따라 결과를 반환 하는 시기가 다르다.
- WriteConcern.ACKNOWLEDGED: 기본 설정. MongoDB 서버가 쓰기를 수신하고 메모리에 저장한 후 즉시 응답을 반환
- WriteConcern.UNACKNOWLEDGED: 응답을 기다리지 않고, 클라이언트가 요청을 전송하자마자 다음 작업으로 넘어갑니다. 성능은 높지만, 데이터 손실 가능성이 있음
- WriteConcern.MAJORITY: 데이터를 주요 복제본에 모두 기록한 후 응답을 반환합니다. 쓰기 일관성이 높아지지만, 성능은 다소 느려질 수 있음
- WriteConcern.JOURNALED: 데이터가 디스크에 기록되고, 저널에 저장된 후 응답을 반환합니다. 데이터 안전성이 높아짐
@Test
fun testSyncMongoItemWriter() {
// given
mongoTemplate.setWriteConcern(WriteConcern.MAJORITY)
val writer = MongoItemWriterBuilder<MongoDomain>()
.template(mongoTemplate)
.collection("mongo_domains")
.build()
writer.afterPropertiesSet()
val items = List(count) { i -> MongoDomain(name = "test_mongo_$i") }
// when
val timeTaken = measureTimeMillis {
assertDoesNotThrow {
writer.write(Chunk(items))
}
}
logger.info("MongoItemWriter: Time taken to write $count items: ${timeTaken}ms")
}
2024-11-13T06:45:49.316+09:00 INFO 57165 --- [ Test worker] com.hanbong.demo.JobConfigTest : MongoItemWriter: Time taken to write 10000 items: 1ms
그래도 여전히 빠르다. bulk 로 인하여 대용량의 데이터를 효율적으로 저장하는데 최적화
되어있다고 한다.
1.5. HibernateItemWriter
- Hibernate를 사용하여 데이터베이스에 데이터를 쓰는 Writer
- Spring batch 5.0 에서 deprecated
삭제 되었기에 생략한다.
결과 정리
아래 표는 각 Writer 유형별 소요 시간을 정리한 것이다.
작성자 유형 | 소요 시간 (ms) |
---|---|
JdbcBatchItemWriter (Domain) | 3238 |
JdbcBatchItemWriter (IdentityDomain) | 3415 |
JpaItemWriter (Domain, usePersist) | 7283 |
JpaItemWriter (Domain, merge) | 7576 |
JpaItemWriter (AutoDomain, GenerationType.AUTO) | 3384 |
JpaItemWriter (IdentityDomain, GenerationType.IDENTITY) | 3245 |
MongoItemWriter (ACKNOWLEDGED) | 1 |
MongoItemWriter (MAJORITY) | 1 |
'IT > Spring' 카테고리의 다른 글
chunk vs tasklet 차이 (1) | 2024.11.18 |
---|---|
HikariCP 데드락 이슈 (3) | 2024.11.14 |
Spring Boot 3.2 변경점 (3) | 2023.11.24 |
스프링의 트랜잭션 (0) | 2023.10.12 |
Spring AOP 분석 (0) | 2023.10.10 |
댓글