티스토리 뷰

개요

  • 현재 내가 몸 담고 있는 프로젝트는 세금 계산과 관련되어 한 번에 수십에서 수백만의 엔티티 생성이 발생하는 비지니스 로직이 존재하며, 레코드 생성의 속도가 곧 경쟁력이 되기에 최적화에 있어 아주 중요한 요소이다. 이번 글에서는 기존에 대량의 엔티티가 단건으로 저장되던 것을 Buik Insert로 개선하는 과정을 정리하였다.

MySQL 커넥션 스트링 추가

  • MySQL은 븐다시 rewriteBatchedStatements=true 옵션을 커넥션 스트링에 추가해야 Bulk Insert가 활성화된다. Hikari Pool을 이용하여DataSource를 생성할 경우 아래와 같이 옵션을 추가하면 된다.
val config = HikariConfig().apply {
    ...
    addDataSourceProperty("rewriteBatchedStatements", true)
}
val datasource = HikariDataSource(config)

FooRepository#saveAll 작성

  • 아래는 실제 코드에서 JdbcTemplate#batchUpdate를 사용하여 Foo라는 엔티티의 목록을 배치로 생성하는 Bulk Insert의 실행 예이다.
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.sql.Timestamp

@Repository
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED)
class FooRepositorySupport(
    private val jdbcTemplate: JdbcTemplate
) {
    @Transactional(isolation = Isolation.READ_COMMITTED)
    fun saveAll(foos: List<Foo>) {
        jdbcTemplate.batchUpdate(
            "INSERT INTO foo (string, long, double, boolean, instant) VALUES (?, ?, ?, ?, ?)",
            foos,
            4096
        ) { ps, foo ->
            foo.string?.let { ps.setString(1, it) } ?: ps.setNull(1, java.sql.Types.NULL)
            foo.long?.let { ps.setLong(2, it) } ?: ps.setNull(2, java.sql.Types.NULL)
            foo.double?.let { ps.setBigDecimal(3, BigDecimal.valueOf(it)) } ?: ps.setNull(3, java.sql.Types.NULL)
            foo.boolean?.let { ps.setBoolean(4, it) } ?: ps.setNull(4, java.sql.Types.NULL)
            foo.instant?.let { ps.setTimestamp(5, Timestamp.from(it)) } ?: ps.setNull(5, java.sql.Types.NULL)
        }
    }
}
  • 예제 설명을 위해 Foo 엔티티가 String, Long, Double, Boolean, Instant 타입의 필드를 가진 것으로 가정했다. 전달되는 파라메터가 null일 경우 그대로 실행하면 오류가 발생하기 때문에 예외 처리 구문을 추가했다.
  • JdbcTemplate#batchUpdatebatchSize 파라메터로 하나의 배치 단위에 생성할 엔티티 개수를 지정할 수 있다. 위 예제에서는 4096을 지정했는데, 이 것만으로 운영 환경에서 22만건의 엔티티의 생성 소요시간이 10배가 단축된 것을 확인했다.
  • MySQL의 경우, 한 번에 실행되는 쿼리의 크기가 클 경우 max_allowed_packet 파라메터에 의해 제한이 걸려 ER_NET_PACKET_TOO_LARGE 오류가 발생한다. max_allowed_packetbatchSize 파라메터를 적절히 튜닝해야 한다.

적용 후기

  • JdbcTemplate#batchUpdate로 구현한 Bulk Insert 성능은 드라마틱하다. 앞서 언급했듯이 운영 환경에서 22만건의 엔티티의 생성 소요시간이 10배가 단축된 것을 확인했다.
  • Type-Safe의 장점을 누구보다 잘 알고 추구하기에 평소 코드 레벨에서 RAW SQL의 사용은 최대한 지양하려고 노력했지만, 기존 데이터베이스 스키마의 변경 없이 속도 개선이 가능하다는 것은 상당한 이득이었다.

참고 글

댓글
댓글쓰기 폼