2022. 3. 29. 23:47ㆍSpring
Bulk Insert의 성능을 측정하고, 사용 시의 주의사항을 알아가는 것이 해당 포스팅의 목표입니다.
이 글을 Bulk Insert, 성능테스트 에 이은 두 번째 글입니다.
데이터베이스 쿼리의 성능을 높이는 방법으로 Bulk Insert를 사용하곤 하는데요.
지난 번은 데이터베이스의 직접적인 성능을 알아봤고,
이번에는 스프링에서의 Bulk Insert의 성능을 비교해보고자 합니다.
직접 실행해본 코드를 통해 어느정도 성능이 좋아지는 지를 직접 확인해보기 위함입니다.
Spring Test
테스트는 아주 간단히 Runner를 통해 확인할 수 있게끔 했습니다.
Test 코드를 짜기도 했는데, 가장 하단에 참고용으로 첨부할 예정입니다.
특별한 설명이 필요 없을 것 같아 바로 코드를 확인하겠습니다.
UserInsertRunner
@Log4j2
@Component
public class UserInsertRunner implements ApplicationRunner {
@Autowired
private InsertService insertService;
private final int INSERT_SIZE = 10_000;
private List<User> users = new ArrayList<>();
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("\n========== Runner ===========\n");
log.info("Test Size : " + INSERT_SIZE);
genUser();
long basicDuration = insertService.basicInsert(users);
long bulkDuration = insertService.bulkInsert(users);
log.info("Basic Insert Duration : " + basicDuration);
log.info("Bulk Insert Duration : " + bulkDuration);
log.info("\n\n=============================\n");
}
private void genUser() {
for (int i = 0; i < INSERT_SIZE; i++) {
users.add(new User(
"gngsn" + i,
"gngsn" + i + "@email.com",
new RandomString().nextString())
);
}
}
}
UserInsertRunner는 User 데이터를 생성해서,
InsertService에서 실행 시간을 반환하면 출력하는 역할을 합니다.
InsertService
@Service
@AllArgsConstructor
public class InsertService {
@Autowired
private UserDAO userDAO;
public long basicInsert(List<User> users) {
long start = System.currentTimeMillis();
users.forEach(user -> userDAO.insertUser(user));
return System.currentTimeMillis() - start;
}
public long bulkInsert(List<User> users) {
long start = System.currentTimeMillis();
userDAO.bulkInsertUserList(users);
return System.currentTimeMillis() - start;
}
}
각각 Basic Insert와 Bulk Insert 의 실행 시간을 ms 단위로 측정합니다.
UserDAO
@Repository
public class UserDAO {
private static final String NAMESPACE = "bulkInsert.UserDAO.";
@Autowired
private SqlSession sqlSession;
public int insertUser(User users) {
return sqlSession.insert(NAMESPACE + "insertUser", users);
}
public int bulkInsertUserList(List<User> users) {
return sqlSession.insert(NAMESPACE + "bulkInsertUserList", users);
}
}
Sql.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="bulkInsert.UserDAO">
<insert id="insertUser" parameterType="com.gngsn.demo.bulkInsert.User">
INSERT INTO test (name, email, PASSWORD)
VALUES (#{name}, #{email}, #{password})
</insert>
<insert id="bulkInsertUserList" parameterType="java.util.List">
INSERT INTO test (name, email, PASSWORD) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.email}, #{item.password})
</foreach>
</insert>
</mapper>
이 포스팅의 핵심 코드입니다.
두 코드의 차이를 확인하면, Bulk Insert를 어렵지 않게 사용할 수 있습니다.
사용법은 그렇게 어렵지 않으니 알아두면 두고두고 활용할 수 있겠죠?
그럼, 이제 결과를 확인해보고 어느정도의 성능차이가 있는지 확인해보도록 하겠습니다.
Output
========== Runner ===========
Test Size : 10000
Basic Insert Duration : 68698 ms
Bulk Insert Duration : 889 ms
=============================
결과 데이터를 보면, 둘의 성능 차이가 굉장히 크다는 걸 느낄 수 있습니다.
참고 : Test Code
@Log4j2
@SpringBootTest
class DemoApplicationTests {
@Autowired
private InsertService insertService;
private final int INSERT_SIZE = 1_0;
private List<User> users = new ArrayList<>();
@BeforeEach
void before() {
log.info("===================================\n");
for (int i=0; i < INSERT_SIZE; i++) {
users.add(new User(
"gngsn" + i,
"gngsn" + i + "@email.com",
new RandomString().nextString())
);
}
log.info("Test Size : " + INSERT_SIZE);
}
@Test
void basicInsert() {
long basicDuration = insertService.basicInsert(users);
log.info("Basic Insert Duration : " + basicDuration);
log.info("\n=================================");
}
@Test
void bulkInsert() {
long bulkDuration = insertService.bulkInsert(users);
log.info("Bulk Insert Duration : " + bulkDuration);
log.info("\n=================================");
}
}
@MybatisTest를 사용하다가 시간을 너무 뺏겨서 .. 무겁지만 SpringBootTest를 사용했습니다.
성공하신 분들은 코드 공유 부탁드려요 🥲
주의 사항
Bulk Insert의 리스트를 무작정 늘릴 수 있다면야 좋겠지만,
MySQL Client가 Server로 전달하는 Packet의 크기는 제한되어있습니다.
그래서, 너무 많은 양의 데이터를 한 번에 넣게 되면 아래와 같은 오류가 발생합니다.
### Cause: com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (5,679,285 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.
; Packet for query is too large (5,679,285 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet' variable.; nested exception is com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (5,679,285 > 4,194,304). You can change this value on the server by setting the 'max_allowed_packet ' variable.
오류를 읽어보면 알겠지만, Packet의 크기가 허용치를 넘어 예외 처리된 것을 확인할 수 있습니다.
또, 확인할 수 있는 내용은 max_allowed_packet 이 바로 그 허용치를 나타내고 있다는 점입니다.
max_allowed_packet
최대 허용 패킷 크기는 쿼리를 통해서 확인해볼 수 있습니다.
SHOW VARIABLES LIKE 'max_allowed_packet’;
위와 같은 쿼리로 데이터를 확인할 수 있는데요. 아래와 같은 결과를 확인할 수 있습니다.
제가 사용하는 MySQL의 경우 최대 허용 패킷 크기가 67108864Byte, 즉 67MB 라는 것을 확인할 수 있습니다.
이 부분을 염두해둔 상태에서 쿼리를 작성해야 예외가 발생하지 않겠죠 ?
'Spring' 카테고리의 다른 글
Caffeine Cache, 어렵지 않게 사용하기 1 (0) | 2022.04.04 |
---|---|
Spring Cache, 제대로 사용하기 (2) | 2022.04.03 |
Spring Security, 어렵지 않게 설정하기 (6) | 2022.03.24 |
Spring WebClient, 어렵지 않게 사용하기 (25) | 2022.03.17 |
Spring Interceptor, 제대로 이해하기 (6) | 2022.03.15 |
Backend Software Engineer
𝐒𝐮𝐧 · 𝙂𝙮𝙚𝙤𝙣𝙜𝙨𝙪𝙣 𝙋𝙖𝙧𝙠