Spring Batch Insert 성능 최적화 하기

Daeyoung Nam·2021년 10월 15일
2

프로젝트

목록 보기
11/16

파일을 읽고.. 저장을 해야하는데..

현재 진행하고있는 프로젝트에서 기존 산 데이터를 읽고 데이터베이스에 넣어줘야하는 배치작업이 필요하다는것을 깨닫고 Spring Batch를 도입하여 사용중이다.
데이터 크기는 다음과 같다

전국 산 등산로 데이터

6010934개

산 속성 데이터

56942개

산 데이터

2931개

그래서 기존에는 어떻게 했는데?

각 도메인들이 OneToMany로 설계되어있는 상황이고 resource에 있는 데이터를 읽어와서 Entity로 바꿔주고 모두 Insert 해야하는 상황이었는데..
기존에 도메인 Repository에 saveAll(Iterable)을 호출하여 데이터베이스에 insert 해줬었다.

물론 save()로 각 데이터 하나하나 insert를 해주는건 당연히 오버헤드가 큰 상황이었고 Transactional인 saveAll()을 호출하는게 당연히 효율적이라 생각하여 saveAll()로 저장을 해보았는데 13분 정도가 걸렸다.
로그에는 엔티티 하나당 insert문이 한개씩 계속 찍혔다.
JPA를 거쳐서 대용량 데이터를 저장하는것은 부적합하다는 결론이 나왔다.

해결책

JPA를 이용할게 아닌 Jdbc를 이용하여 Batch 작업을 해주기로 했다.
addBatch()는 java docs에 이렇게 나와있다.

Adds the given SQL command to the current list of commands for this Statement object. The commands in this list can be executed as a batch by calling the method executeBatch.

즉, addBatch()는 쿼리를 즉시 실행하는것이 아닌 메모리에 올려두었다가 executeBatch()가 호출되었을 때 대량의 Row를 insert/update/delete 할 수 있다는 것이다.

소스코드

class MountainJdbcBatchWriter(
    private val dataSource: DataSource
): ItemWriter<Mountain> {

    override fun write(items: MutableList<out Mountain>) {
        dataSource.connection.let { con ->
            val mountainPs = con.prepareStatement("REPLACE INTO mountain (id, mountain_name, location) VALUES (?, ?, ?)")
            val attributePs = con.prepareStatement(
                "INSERT INTO mountain_attribute (load_length, difficulty, mountain_id) VALUES (?, ?, ?)",
                Statement.RETURN_GENERATED_KEYS
            )
            val hikingTrailPs = con.prepareStatement(
                "INSERT INTO hiking_trail(latitude, longitude, mountain_attribute_id) VALUES (?, ?, ?)",
                Statement.RETURN_GENERATED_KEYS
            )

            for (mountain in items) {
                mountainPs.setString(1, mountain.mountainCode)
                mountainPs.setString(2, mountain.mountainName)
                mountainPs.setString(3, mountain.location)
                mountainPs.addBatch()
            }
            mountainPs.executeBatch()

            for (mountain in items) {
                for(attribute in mountain.mountainAttributes) {
                    attributePs.setDouble(1, attribute.loadLength)
                    attributePs.setString(2, attribute.difficulty)
                    attributePs.setString(3, mountain.mountainCode)
                    attributePs.addBatch()
                    attributePs.executeBatch()

                    val id = attributePs.generatedKeys.let {
                        generateSequence {
                            if(it.next()) it.getLong(1) else null
                        }.toList()
                    }[0]

                    for(hikingTrail in attribute.hikingTrails) {
                        hikingTrailPs.setDouble(1, hikingTrail.latitude)
                        hikingTrailPs.setDouble(2, hikingTrail.longitude)
                        hikingTrailPs.setLong(3, id)
                        hikingTrailPs.addBatch()
                    }
                }
            }
            
            hikingTrailPs.executeBatch()
        }

    }

}

완전 쌩짜로 db에 넣어줘야 했다.
OneToMany와 같은 연관관계도 걸려있기때문에 위와같이 작성했다.
먼저 Mountain에 대한 배치작업을 실행하고,
Mountain의 자식인 MountainAttribute에 대한 작업을 하는데
MountainAttribute의 경우 id가 auto_increment기 때문에
MountainAttribute의 자식인 HikingTrail에 FK를 걸어주기 위해선 먼저 배치를 실행하고 key값을 얻어와야 했다.

결과적으로

13분 걸리던 작업이 1분 54초로 단축되었다.

대용량 배치작업에서는 JPA를 사용하는것보단 Jdbc를 사용하는것이 더 효율이 좋은것같다.

역씨... ORM은 잘 알고 써야한다

profile
내가 짠 코드가 제일 깔끔해야하고 내가 만든 서버는 제일 탄탄해야한다 .. 😎

0개의 댓글