본문 바로가기
프로그래밍/Spring

[JPA] Spring JPA 환경에서 bulk insert를 효율적으로 해보자 - JPA의 한계와 JDBC 활용

by 사바라다 2022. 4. 12.

안녕하세요. 오늘은 Spring JPA 환경에서 bulk insert를 효율적으로 하는 방법에 대해서 알아보는 시간을 가져보도록 하겠습니다. JPA를 사용하고 ID를 전략을 사용했을 때 JPA의 성능 문제는 이전에 [JPA] JPA의 AUTO_INCREMENT 테이블에서 다건 insert 시간 비교 - save vs saveAll 포스팅에서 간단히 다룬적이 있습니다. 해당 포스팅의 주된 내용은 save와 saveAll의 성능 시간 비교였지만 연관이 있으므로 관심있으신 분들은 해당 포스팅을 일부 참고하셔도 좋을 것 같습니다.

Spring Data JPA의 bulk insert와 그 한계점

bulk insert는 한번의 쿼리로 여러건의 데이터 row를 insert할 수 있는 insert 방법입니다. batch 시스템을 만들다보면 한건씩 수만, 수십만건의 데이터를 넣다보면 퍼포먼스적으로 오래걸리는걸 알 수 있고 이를 해결하기 위해서 주로 사용됩니다.

그런데 만약 Mysql을 사용하고 id 채번을 auto_increment 전략을 이용하고 JPA를 사용한다면 아쉽지만 bulk insert는 사용할 수 있는 방법이 없습니다. 사용하기 위해서는 채번 전략을 바꿔야합니다. 아쉽지만 이는 JPA의 철학에 의해 금지되어 있기 때문에 앞으로 버전이 업데이트 된다고 해도 이부분이 개선될 여지는 없습니다. Link

위에서 한번 언급했떤 이전 포스팅에서는 saveAll과 save의 성능을 비교했었습니다. 하지만 결국 saveAll 역시 bulk insert를 사용하는 것은 아니기 때문에 너무 많은 건 수를 insert하면 속도적으로 만족으러운 결과를 얻지 못할 수 있습니다.

대안

JPA에서 대안으로 제시하는 방안이 batch를 사용할 수 있도록 id 채번 방식을 SEQUENCE 또는 TABLE을 이용할 수 있도록 하는 것입니다. 하지만 MySQL은 SEQUENCE 채번 방식을 지원하지 않으며, table 채번 방식은 테이블 마다 채번 테이블을 추가로 만든다는 것이므로 사용하는 입장에서는 부담스럽긴 마찬가지입니다.

아래 이미지를 보도록 하겠습니다. spring data JPA의 내부 의존성인데요. 해당 의존성을 보시면 중간에 spring-boot-starter-jdbc를 가지고 있는것을 확인하실 수 있습니다. 이 내부 의존성은 jdbc를 JPA가 결국 감싸서 사용하고 있기 때문인데요. 즉, 우리는 jdbc를 직접 사용하면 JPA에서 하지 못했던 bulk insert를 할 수 있게됩니다.

실습

예제를 통해서 실습해보고 JPA의 saveAll 방식과 jdbc를 직접 이용했을 때의 성능차이를 눈으로 확인해보도록 하겠습니다.

실습 Entity

아래는 오늘 테스트할 Entity입니다. JPA 환경이기 때문에 @Entity 어노테이션이 붙어있습니다. JPA에서는 해당 모델 클래스를 직접 사용하며 jdbc에서는 해당 모델의 변수들을 쿼리에 입력하는 방식으로 진행됩니다.

@Entity
class SampleUser(
    val userId: Long,
    val expiredAt: Instant?,
) {

    @Id
    @GeneratedValue
    override var id: Long? = null

    @CreatedDate
    @Column(updatable = false)
    var createdAt: Instant = Instant.now()

    @LastModifiedDate
    var updatedAt: Instant? = null
}

jdbc에서 bulk insert 하는 방법 ( batch )

JPA는 saveAll을 호출하기면 하면 되기 때문에 jdbc에서 bulk insert를 하는 방법에 대해서 알아보도록 하겠습니다. 사용은 아래와 같이 할 수 있습니다. 자세한 내용은 코드내의 주석을 참고해주시기 바랍니다.

@Repository
class SampleUserCustomRepositoryImpl(
    private val jdbcTemplate: JdbcTemplate // JdbcTemplate 의존성 주입
) : SampleUserCustomRepository {

    override fun bulkSave(sampleUsers: List<SampleUser>) {
        batchInsert(1000, sampleUsers) // batch size는 1000으로 진행. 즉, 1000 건을 하나의 쿼리로 진행
    }

    private fun batchInsert(batchSize: Int, subItems: List<SampleUser>) {

        jdbcTemplate.batchUpdate(
            " INSERT INTO sample_user ( " +
            "user_id, expired_at, updated_at, created_at" +
            ") values (" +
            "?, ?, ?, ?" +
            ")", // bulk insert에 사용할 기본 쿼리
            subItems, // insert할 모델
            batchSize // 1번의 batch로 함께 insert할 batch 사이즈
        ) { ps, argument ->
            ps.setLong(1, argument.userId) // 쿼리의 ?의 순서대로 1번으로 할당되며 해당 쿼리 ? 대신 치환

            if (argument.expiredAt != null) {
                ps.setDate(2, Date(argument.expiredAt!!.toEpochMilli()))
            } else {
                ps.setNull(2, Types.DATE)
            }

            ps.setDate(3, Date(Instant.now().toEpochMilli()))
            ps.setDate(4, Date(Instant.now().toEpochMilli()))
        }
    }
}

Transaction 거는 법

RDMBS를 사용할 때 Transaction을 거는 것은 중요한 부분입니다. jdbcTemplate은 동일하게 Spring에서 지원하는 것이기 때문에 해당 funciont 위에 @Transactional을 붙여도 정상적으로 동작합니다.

@Transactional
override fun bulkSave(sampleUsers: List<SampleUser>) {
    batchInsert(1000, sampleUsers) // batch size는 1000으로 진행. 즉, 1000 건을 하나의 쿼리로 진행
}

성능 비교

그렇다면 이제 결과적으로 어떤 성능적인 차이가 나왔는지 확인해보겠습니다. 아래표를 보시면 아시겠지만 bulk insert를 사용했을때와 사용하지 않았을 때, 40 ~ 50배 정도의 성능 차이가 나는 것을 확인할 수 있었습니다.

  10건 100건 1000건 10000건 100000건
JPA 0.03s 0.19s 2.06s 18.47s 175.58s
jdbc 0.01s 0.01s 0.09s 0.33s 4.31s
JPA/jdbc 비율 3 19 22 55.96 40.73

마무리

프로젝트에서 JPA를 사용하고 있다고해서 JPA에 매몰되서는 안된다고 합니다.

흔히들 실버불렛은 없다고 합니다. JPA 또한 실버불렛이 아닙니다. JPA 또한 약점이 있습니다.

그렇기 때문에 JPA의 약점을 매꿀 수 있는 기술이 있다면 그 기술을 유도리있게 선택하는것이 좋을 수 있습니다.

감사합니다.

참조

[1] https://www.youtube.com/watch?v=SJlKBkZ2yAU

[2] https://homoefficio.github.io/2020/01/25/Spring-Data%EC%97%90%EC%84%9C-Batch-Insert-%EC%B5%9C%EC%A0%81%ED%99%94/

[3] https://sabarada.tistory.com/195

댓글