본문 바로가기
개발/성능 개선

Bulk Update 성능 개선: JPA에서 JdbcTemplate으로 전환

by nineteen 2025. 4. 7.
반응형

이번 포스팅에서는 제가 실제 프로젝트에서 겪었던 대량 데이터 처리 성능 이슈와 이를 해결한 경험을 적어보고자 합니다.
JPA의 saveAll()을 사용하던 로직을 JdbcTemplate의 Bulk Update로 전환하면서 얻은 성능 개선 경험을 기록해보고자 합니다.

 

겪었던 성능 이슈

프로젝트를 진행하면서 다음과 같은 성능 문제에 직면했습니다:

  • JPA의 saveAll()을 사용한 대량 데이터 업데이트 시 심각한 성능 저하 발생
  • 약 8,000건의 데이터 처리에 약 18분이라는 긴 시간이 소요
  • 각 엔티티마다 개별 UPDATE 쿼리가 실행되어 데이터베이스 부하 증가

실제 처리해야 할 데이터는 10,000건 미만이었지만, 단순 데이터 적재 외에도 여러 부가적인 처리 작업이 필요했기 때문에 꽤나 긴 시간이 소요되었습니다.
이러한 문제를 해결하기 위해 JdbcTemplate의 Bulk Update를 도입하기로 결정했습니다.

 

성능 측정 결과

개선 전 (JPA saveAll 사용)

  • 실행 쿼리 수: 데이터 건수만큼 실행 (1000건이면 1000번)
  • 평균 응답 시간: 약 18분
  • 문제점:
    • 각 레코드마다 개별 UPDATE 쿼리 실행
    • 데이터베이스 통신 비용 증가
      // AS-IS
      memberRepository.saveAll(memberList)

개선 후 (Bulk Update 사용)

  • 실행 쿼리 수: batch size 단위로 실행 (예: 1000건을 batch size 100으로 처리 시 10번)
  • 평균 응답 시간: 약 4분
  • 개선율: 약 78% 성능 향상
  • 개선 포인트:
    • 데이터베이스 통신 횟수 대폭 감소
    • 효율적인 리소스 활용

 

개선 방법 상세 설명

실제 코드가 아닌 예시 코드입니당

1. JdbcTemplate의 batchUpdate 설정

MySQL을 사용하는 경우 반드시 다음 설정을 추가해야 합니다:

jdbc:mysql://[DB URL]?rewriteBatchedStatements=true

2. 데이터 청크 단위 처리

메모리 효율성을 고려하여 데이터를 청크 단위로 분할하여 처리합니다:

val batchSize = 1000
val chunkedMemberList = allMembers.chunked(batchSize)
chunkedMemberList.forEachIndexed { index, chunk ->
    bulkRepository.updateMembers(chunk)
    log.info {
        "[member] (${index + 1}/${chunkedMemberList.size}) | Bulk Update 완료 :: size=${chunk.size}"
    }
}
  • 적절한 배치 사이즈를 설정하여 메모리 사용량을 관리해야 합니다.

3. Repository 구현

@Repository
class BulkRepository(
    private val jdbcTemplate: JdbcTemplate,
    private val objectMapper: ObjectMapper,
) {
    fun updateMembers(memberList: List<MemberEntity>) {
        val sql = "UPDATE member SET status = ? updated_at = ? WHERE member_id = ?"

        jdbcTemplate.batchUpdate(sql, object : BatchPreparedStatementSetter {
            override fun setValues(ps: PreparedStatement, i: Int) {
                val entity = memberList[i]
                ps.setString(1, entity.status)
                ps.setTimestamp(2, Timestamp.valueOf(entity.updatedAt))
                ps.setLong(3, entity.memberId)
            }

            override fun getBatchSize(): Int = memberList.size
        })
    }
}

 

마치며

이번 성능 개선을 통해 약 18분이 걸리던 처리 시간을 약 4분으로 단축할 수 있었습니다.

물론 이 해결방법이 모든 상황에 최적의 답은 아닐 수 있습니다.
하지만 비슷한 성능 이슈로 고민하시는 분들께 저의 경험이 하나의 참고사례가 되었으면 합니다.

그리고 더 좋은 방법이 있다면, 이 방법이 최선이 아니라는 것을 알고 계신다면 알려주세요ㅎㅎ

 

p.s. Bulk UPDATE 작업 시 WHERE IN 절을 활용하는 방법도 더 좋은 성능을 낼 수 있는 대안이 될 수 있다고 합니다.
(저의 경우에는 각 ROW 별 업데이트를 했어야 했으므로 활용하진 않았습니다.)