본문 바로가기
IT/Spring

Spring Batch Writer 성능 비교

by 봉즙 2024. 11. 13.

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 의 값에 대해 selectupdate 진행 한 후 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

댓글