기존 엑셀 업로드에 사용하는 insert 방식은 saveAll()인데
프로젝트를 진행하며 최대 1,000건 정도의 insert를 테스트 했기때문에 조금 느리더라도 그러려니 했다.
하지만 10만건, 30만건, 100만건을 테스트 해본 결과 실행 시간이 너무 느리게 느껴졌고
리팩토링을 하는 김에 insert방식을 최적화 해보기로 했다.
현재 saveAll() 방식으로 엑셀 업로드를 했을때 실행 시간을 정리한 표이다.
saveAll() | 수행 시간 |
10만건 | 37초 |
30만건 | 103초 |
100만건 | 325초 |
다양한 대량의 데이터 insert 방식
- 순수 JDBC 기반 : JDBC Batch, JdbcTemplate.batchUpdate()
- MyBatis 기반 : for-each + Mapper, SqlSession + ExecutorType.BATCH
- JPA 기반 : saveAll(), bulk insert (native query)
- 네이티브 SQL 기반 : JdbcTemplate, nativeQuery in JPA
- 외부 라이브러리, Spring Batch, jOOQ, QueryDSL (native)
많은 방식 중에서 이번 프로젝트에서 엑셀 다운로드때 집계함수를 사용해서 mybatis를 사용했기 때문에
saveAll(), for-each + Mapper,SqlSession + ExecutorType.BATCH 3개의 성능을 비교해보려고 한다.
BaseTimeEntity 문제
java.sql.SQLException: Field 'created_at' doesn't have a default value
기존에는 엑셀 업로드를 할때 saveAll()을 사용했는데
saveAll()은 Spring Data JPA가 제공하는 기능이고
내부적으로는 EntityManager.persist()나 merge() 등을 호출한다.
이때 JPA는 @PrePersist, @PreUpdate 같은 엔티티 생명주기 콜백 메서드를 호출한다.
하지만 MyBatis는 순수 SQL 매퍼이기 때문에, JPA처럼 엔티티의 생명주기 콜백을 모르고, 호출하지도 않는다.
즉, @PrePersist 같은 어노테이션은 전혀 작동하지 않아서 문제가 발생했다.
foreach코드
@Mapper
public interface AdminSalaryMapper {
void excelUploadWithForeach(@Param("salaries") List<Salary> salaries);
}
// xml파일
<insert id="excelUploadWithForeach">
INSERT INTO salary (employee_id, basic_salary, deduction, net_salary, pay_date, year, created_at, updated_at)
VALUES
<foreach collection="salaries" item="salary" separator=",">
(#{salary.employee.id}, #{salary.basicSalary}, #{salary.deduction}, #{salary.netSalary}, #{salary.payDate}, #{salary.year}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
</foreach>
</insert>
saveAll() | 수행 시간 |
10만건 | 37초 |
30만건 | 103초 |
100만건 | 325초 |
for-each | 수행 시간 |
10만건 | 7초 |
30만건 | 22초 |
100만건 | 메모리 부족.... |
saveAll()로 100만건을 insert할때는 느리지만 메모리 부족 에러가 발생하지 않았는데
foreach는 속도도 빨라서 당연히 메모리 문제가 없을거라고 생각했는데 에러가 발생했다.
그래서 10만건씩 끊어서 insert를 하고 메모리 체크를 해보았다.
10만건씩(엑셀 100만건)
수행 횟수 | Total Memory (MB) | Free Memory (MB) | Used Memory (MB) | 실행 시간 (초) |
1 | 1119.000 | 446.207 | 672.793 | 8.36 |
2 | 1206.000 | 628.121 | 577.879 | 6.65 |
3 | 1402.000 | 805.998 | 596.002 | 6.55 |
4 | 1402.000 | 704.200 | 697.800 | 6.46 |
5 | 1661.000 | 646.314 | 1014.686 | 6.43 |
6 | 1677.000 | 563.840 | 1113.160 | 6.52 |
7 | 1677.000 | 679.430 | 997.570 | 6.76 |
8 | 1826.000 | 1163.502 | 662.498 | 6.72 |
9 | 1913.000 | 953.057 | 959.943 | 6.47 |
10 | 1967.000 | 970.107 | 996.893 | 6.63 |
총합 | 68.53 |
수행 횟수 | Total Memory (MB) | Free Memory (MB) | Used Memory (MB) | 실행 시간 (초) |
1 | 1671.000 | 852.124 | 818.876 | 16.51 |
2 | 1778.000 | 870.034 | 907.966 | 13.08 |
3 | 1932.000 | 904.014 | 1027.986 | 12.91 |
4 | 1994.000 | 982.938 | 1011.062 | 14.14 |
5 | 2016.000 | 1003.755 | 1012.245 | 14.95 |
총합 | 71.61 |
50만건씩(엑셀 100만건)
수행 횟수 | Total Memory (MB) | Free Memory (MB) | Used Memory (MB) | 실행 시간 (초) |
1 | 2048.000 MB | 404.103 MB | 1643.897 MB | 42.93 |
2 | 2048.000 MB | 524.117 MB | 1523.883 MB | 37.48 |
총합 | 81.49 |
많은 데이터를 나누어 insert할 때, 각 배치의 메모리 사용량과 실행 시간이 증가하는것을 확인 할 수있고
이로 인해 100만 건을 한 번에 insert할 경우, 할당된 JVM 메모리 용량을 초과하여 사용 중인 메모리가 더 많아져 OOM(Out Of Memory) 오류가 발생한 것으로 예상됌
규칙이 궁금해서 5만건, 1만건 단위로도 확인해보았는데
10만건과 큰 차이가 없는거 같다.
5만건씩(엑셀 100만건)
수행 횟수 | Total Memory (MB) | Free Memory (MB) | Used Memory (MB) | 실행 시간 (초) |
1 | 1167 | 557.619 | 609.381 | 5.27 |
2 | 1167 | 270.337 | 896.663 | 3.46 |
3 | 1239 | 569.423 | 669.577 | 3.44 |
4 | 1239 | 644.342 | 594.658 | 3.10 |
5 | 1239 | 527.824 | 711.176 | 3.26 |
6 | 1488 | 639.258 | 848.742 | 3.36 |
7 | 1488 | 946.881 | 541.119 | 3.21 |
8 | 1626 | 885.184 | 740.816 | 3.24 |
9 | 1626 | 648.086 | 977.914 | 3.31 |
10 | 1626 | 710.829 | 915.171 | 3.07 |
11 | 1643 | 818.321 | 824.679 | 3.59 |
12 | 1643 | 860.184 | 782.816 | 3.35 |
13 | 1643 | 860.184 | 782.816 | 3.34 |
14 | 1643 | 870.102 | 772.898 | 3.12 |
15 | 1724 | 537.640 | 1186.360 | 3.59 |
16 | 1724 | 1064.567 | 659.433 | 3.23 |
17 | 1724 | 660.039 | 1063.961 | 3.30 |
18 | 1724 | 418.627 | 1305.373 | 3.31 |
19 | 1724 | 658.123 | 1065.877 | 3.31 |
20 | 1724 | 560.593 | 1163.407 | 3.31 |
총합 | 69.47 |
1만건씩(엑셀 100만건)
수행 횟수 | Total Memory (MB) | Free Memory (MB) | Used Memory (MB) | 실행 시간 (초) |
1 | 609.000 | 248.961 | 360.039 | 2.11 |
2 | 659.000 | 180.480 | 478.520 | 1.23 |
3 | 825.000 | 474.027 | 350.973 | 0.62 |
4 | 825.000 | 339.429 | 485.571 | 0.73 |
5 | 825.000 | 396.881 | 428.119 | 0.67 |
... | ... | ... | ... | ... |
96 | 1264.000 | 445.848 | 818.152 | 0.77 |
97 | 1264.000 | 137.620 | 1126.380 | 0.69 |
98 | 1264.000 | 471.923 | 792.077 | 0.64 |
99 | 1264.000 | 163.192 | 1100.808 | 0.71 |
100 | 1264.000 | 501.822 | 762.178 | 0.62 |
총합 | 69.42 |
10만건과 큰 차이가 없는거 같다.
찾아볼 내용
1. foreach원리는 어떻게 작동하길래 많은 데이터를 한번에 넣을때 메모리 사용량이 증가하는지?
'프로젝트' 카테고리의 다른 글
엑셀 100만건 업로드(엑셀 파싱 문제) 리팩토링 (2) (0) | 2025.04.23 |
---|---|
엑셀 100만건 업로드(100MB 레코드 제한) 리팩토링 (1) (0) | 2025.04.17 |