m1ndy5's coding blog

Ditto 프로젝트 종목 불러오기 with batchUpdate 본문

Toy Projects/Ditto - Discuss Today's Topic

Ditto 프로젝트 종목 불러오기 with batchUpdate

정민됴 2024. 2. 29. 21:48

코스피와 코스닥 종목을 가져오면서 해당 종목의 개수가 거의 4000개에 가까웠고 각 종목의 10일치 데이터만 가져온다고 해도 거의 40000개의 쿼리가 날라가야하는 상황이었다. JPA saveAll()를 사용해도 bulk insert 는 되지 않았기 때문에 jdbcTemplate의 batchUpdate를 사용하여 bulk insert를 처리하였다.

 

일단 나는 Mysql 8.0을 사용하고 있기 때문에

DATABASE_URL=jdbc:mysql://{rds 주소}/ditto?&rewriteBatchedStatements=true

이렇게 스키마 뒤에 &rewriteBatchedStatements=true 를 붙여주었다.

 

JPA saveAll()과 JdbcTemplateBatchUpdate() 성능 비교

BatchUpdate 기준

package org.example.repository;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang.time.StopWatch;
import org.example.domain.Company;
import org.example.domain.PricePerDay;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;

@Repository
@RequiredArgsConstructor
public class PriceBulkRepository {

    private final JdbcTemplate jdbcTemplate;

    @Transactional
    public void saveAll(List<PricePerDay> pricePerDays){
        String sql = "INSERT INTO PricePerDay (company_id,date,high_price,last_price,low_price,start_price,trading_volume) " +
                "VALUES (?, ?, ?, ?, ?, ?, ?)";

        jdbcTemplate.batchUpdate(sql,
                new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        PricePerDay pricePerDay = pricePerDays.get(i);
                        ps.setLong(1, pricePerDay.getCompanyId());
                        ps.setDate(2, Date.valueOf(pricePerDay.getDate()));
                        ps.setInt(3, pricePerDay.getHighPrice());
                        ps.setInt(4, pricePerDay.getLastPrice());
                        ps.setInt(5, pricePerDay.getLowPrice());
                        ps.setInt(6, pricePerDay.getStartPrice());
                        ps.setLong(7, pricePerDay.getTradingVolume());
                    }

                    @Override
                    public int getBatchSize() {
                        return pricePerDays.size();
                    }
                });
    }
}
package org.example.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.domain.PricePerDay;
import org.example.repository.PriceBulkRepository;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Slf4j
@RequiredArgsConstructor
public class FiveYearPriceItemWriter implements ItemWriter<List<PricePerDay>> {

    private final PriceBulkRepository priceBulkRepository;

    @Override
    public void write(Chunk<? extends List<PricePerDay>> chunk) throws Exception {
        long startTime = System.currentTimeMillis();
        int total = chunk.getItems().get(0).size();
        for (int i = 0; i < total; i+=999){
            if (i+999 > chunk.getItems().get(0).size()){
                priceBulkRepository.saveAll(chunk.getItems().get(0).subList(i, total-1));
            } else {
                log.info(String.valueOf(i));
                priceBulkRepository.saveAll(chunk.getItems().get(0).subList(i, i + 999));
            }
        }
        long stopTime = System.currentTimeMillis();
        log.info("코드 실행 시간: " + (stopTime - startTime));
    }
}

총 36963개보다 조금 넘고 1598 밀리초가 걸렸다.

JPA saveAll() 기준

똑같은 개수 기준 JPA saveAll()은 352261밀리초가 걸렸다.

와우 정말 생각보다 차이가 많이 나서 놀랐다.

 

결론

대용량 데이터 저장에는 JPA saveAll()이 아닌 JDBC BatchUpdate()를 사용하자!!