문제
주식 종목 토론방 프로젝트를 하다보니 3600개 종목의 5년치 종목 일봉데이터(캔들데이터)를 저장할 일이 생겼다.
데이터의 수는 3600(종목수) x 5(년) x 200(1년치데이터) 으로 약 360만개 정도였다.
처음으로 저장하기 위해 시도한 방법은 단순히 DataJPA의 saveAll()메소드를 사용한 것이다.
하지만 저장하는 시간이 몇십분 이상이 걸리는 문제가 있었다.
간단한 테스트 코드로 성능을 측정해 보겠다.
* 테스트는 h2 memory DB로 하였기 때문에 성능은 실제 mysql db와 다를 수 있다.
@Before 10만개의 캔들데이터 생성
@BeforeAll
static void before() {
stockCandleList = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
stockCandleList.add(
StockCandle.builder()
.low(10000)
.open(10000)
.close(10000)
.volume(10000)
.high(10000)
.date(LocalDate.now())
.code("123456")
.build()
);
}
}
JPA saveAll() 테스트 코드
@DisplayName("JPA saveAll 성능 테스트")
@Test
public void testSaveAllMethod() throws Exception {
//given
long startTime = System.currentTimeMillis();
//when
stockCandleJpaRepository.saveAll(stockCandleList);
//then
long endTime = System.currentTimeMillis();
System.out.println("JPA saveAll 시간" + (endTime-startTime)/1000 +"초");
}
성능 측정 결과
10만개의 캔들데이터 저장에 18초라는 시간이 걸렸다.
만약 데이터가 360만개라고 한다면 18 * 36 = 648 초가 걸릴 것이다.
H2 Inmemory DB임에도 Data JPA의 saveAll 메소드는 10분 이상의 시간이 걸리는 문제를 발견 할 수 있다.
원인
JPA saveAll() 쿼리
원인은 JPA saveAll() 의 쿼리를 확인 하면 금방 알 수 있다.
다음과 같이 하나의 stockCandle 데이터를 입력할 때 마다 insert 쿼리가 발생하기 때문이다.
실제로 DB에는 10만개의 insert쿼리가 발생하는 것이다.
해결 1
JDBC Template Batch Insert
해결책으로는 JDBC Template 을 사용해 Batch Insert 문을 작성하는 것이다.
먼저 코드를 보며 어떻게 작성하는지 보겠다.
@Repository
@RequiredArgsConstructor
public class StockJdbcRepository {
private final JdbcTemplate jdbcTemplate;
public void batchInsertStockCandles(List<StockCandle> stockCandles) {
String sql = "INSERT INTO stock_candle "
+ "(open, low, high, close, volume, code, date, bollinger_bands, macd, moving_average_12, moving_average_20, moving_average_26 )" +
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(sql,
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
StockCandle stockCandle = stockCandles.get(i);
ps.setInt(1, stockCandle.getOpen());
ps.setInt(2, stockCandle.getLow());
ps.setInt(3, stockCandle.getHigh());
ps.setInt(4, stockCandle.getClose());
ps.setInt(5, stockCandle.getVolume());
ps.setString(6, stockCandle.getCode());
ps.setDate(7, Date.valueOf(stockCandle.getDate()));
ps.setDouble(8, stockCandle.getBollingerBands());
ps.setDouble(9, stockCandle.getMacd());
ps.setDouble(10, stockCandle.getMovingAverage_12());
ps.setDouble(11, stockCandle.getMovingAverage_20());
ps.setDouble(12, stockCandle.getMovingAverage_26());
}
@Override
public int getBatchSize() {
return stockCandles.size();
}
});
}
}
위와 같이 insert 문을 작성한 후 jdbcTemplate의 batchUpdate 기능을 사용하면 되는 것이다.
JDBC Template Batch Insert 테스트 코드
@DisplayName("JDBC batchInsert 성능 테스트")
@Test
public void testBatchInsertMethod() throws Exception {
//given
long startTime = System.currentTimeMillis();
//when
stockJdbcRepository.batchInsertStockCandles(stockCandleList);
//then
long endTime = System.currentTimeMillis();
System.out.println("Jdbc batchInsert 시간" + (endTime - startTime) / 1000);
}
쿼리 확인
INSERT INTO stock_candle ( , , , ~) VALUES (? , ? , ? ~ ) 라는 쿼리가 한번만 나가는 것을 확인할 수 있다.
VALUES 의 갯수는 내가 입력할 값 만큼 갯수가 늘어 날 것이다.
INSERT INTO stock_candle ( , , , ~) VALUES (? , ? , ? ~ ), VALUES (? , ? , ? ~ ) , VALUES (? , ? , ? ~ )
성능 측정 결과
10만개의 캔들데이터 저장에 1.582 초라는 시간이 밖에 걸리지 않았다.
약 11배 정도의 성능 개선이 된 것이다.
해결2
두번째 해결 방법으로 멀티쓰레드를 적용하면 더 빠르게 저장할 수 있을 것이라고 생각했다.
다음은 10개의 쓰레드를 생성한 후 멀티쓰레드를 적용시킨 테스트 코드이다.
멀티쓰레드 테스트 코드
@DisplayName("JDBC batchInsert with MultiThread 성능 테스트")
@Test
public void testBatchInsertWithMultiThread() throws Exception {
//given
int numThreads = 10; // 쓰레드 개수
int batchSize = stockCandleList.size() / numThreads; // 각 쓰레드당 처리할 데이터 개수
long startTime = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
List<Future<?>> futures = new ArrayList<>();
// 데이터를 쓰레드 개수에 맞게 분할하여 쓰레드로 전달
for (int i = 0; i < numThreads; i++) {
int start = i * batchSize;
int end = (i == numThreads - 1) ? stockCandleList.size() : (i + 1) * batchSize;
List<StockCandle> sublist = stockCandleList.subList(start, end);
futures.add(executorService.submit(() -> {
stockJdbcRepository.batchInsertStockCandles(sublist);
}));
}
// 모든 쓰레드 작업이 완료될 때까지 기다림
for (Future<?> future : futures) {
future.get(); // 작업 완료 대기
}
executorService.shutdown(); // 쓰레드풀 종료
//then
long endTime = System.currentTimeMillis();
System.out.println("Jdbc batchInsert 시간 " + (double) (endTime - startTime) / 1000 + "초");
}
성능 측정 결과
1.5 s -> 1.0 s 로 약 50프로의 성능 향상을 확인 할 수 있다.
요약
대용량 데이터를 저장할 경우 BatchInsert를 활용하자
더하여 MultiThread를 적용하면 더욱 좋은 성능을 뽑아 낼 수 있다.
아래는 성능 측정 요약 그래프 이다.
'프로젝트 > 주식종목토론방' 카테고리의 다른 글
'Monolithic'에서 'MSA'구조로 전환 (1) | 2024.03.18 |
---|---|
뉴스피드 구현 방식 선택 - Fan Out On Read, Fan Out On Write (0) | 2024.03.07 |