JDBC batch insert를 통한 대용량 데이터 삽입 최적화

Simple·2023년 8월 13일
0

개선 시리즈

목록 보기
3/5

상황

batch 작업을 통해 유저 데이터를 머신 러닝 서버에 전달한 뒤 이후 다른 job에 의해 추천 데이터들을 받고 있는 상황이다.

추천 데이터를 넘겨받고 저장하려는 로직을 만들고 있는 상황에서 머신 러닝 서버에서 사용되는 Matrix Factorization 방식에 의해 데이터를 한 번에 매우 많이 저장하는 상황이 예상되었다.

기본적으로 (전체 유저 수) (전체 문제 수)의 연산이 진행되고 내부적인 계산들로 데이터의 개수가 나오는데 최대값이 (전체 유저 수) (전체 문제 수)이다.

그러면 유저 수가 1000명이고, 전체 문제 수가 100개면 최악의 상황에서
최대 100,000 개의 데이터가 생길 수 있는 것이다.

그래서 10만개의 데이터를 테스트를 해봤다.

현재 프로젝트에서는 데이터 접근 기술을 JPA를 사용하고 있기 때문에 먼저 테스트 해본다.


테스트( JPA saveAll )

	@Test
    @DisplayName("추천 데이터 삽입 테스트")
    void insert_recommend_user() throws Exception {

        // given
        List<RecommendUser> recommendUsers = new ArrayList<>();
        for(long i=1; i<=TEST_COUNT; i++){
            recommendUsers.add(RecommendUser.createRecommendUser(i,i));
        }
        recommendUserRepository.saveAll(recommendUsers);
        // then
        assertThat(recommendUsers).hasSize(TEST_COUNT);
    }

수행 결과

무려 14분 12초나 걸렸다.

원인은 sql 로그를 보고 알 수 있었는데

insert 
    into
        RECOMMEND_USER
        (created_at,problemId,status,updated_at,userId) 
    values
        (?,?,?,?,?)
insert 
    into
        RECOMMEND_USER
        (created_at,problemId,status,updated_at,userId) 
    values
        (?,?,?,?,?)
...

위처럼 insert문이 데이터 1개당 1개씩 나가고 있었다.
10만개면 10만개의 쿼리가 나가고 있기 때문에 당연히 느릴 수 밖에 없다.

그러면 insert문 한 번에 values가 10만개 들어갈 수는 없나? 하고 찾아 보던 중

jdbc batch insert를 알게 됐는데 위 쿼리를

INSERT INTO RECOMMEND_USER (user_id, problem_id, status, created_at, updated_at)
  VALUES (1, 1, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(2, 2, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(3, 3, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(4, 4, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
...

1번의 insert로 처리가 가능하다.

jpa에서 batch insert를 어떻게 적용할까?

결론부터 말하자면
Hibernate 에서는 Auto Increment일 경우엔 이 방식을 지원하지 않는다.

현재 추천 데이터의 키 생성 전략은 Auto Increment이므로 적용할 수 없다.

참조: hibernate 공식문서

그러면 여기서 2가지 해결책이 생각이 나는데

  1. 기본 키 생성 전략을 변경한다.
  2. JDBC를 사용한다.

2가지 해결책이 있다.

이 부분도 결론을 말하자면

1번은 성능상 이슈와 Dead Lock에 대한 이슈가 존재하기 때문에 JDBC를 사용하기로 결정했다.

참조: why-you-should-never-use-the-table-identifier-generator-with-jpa-and-hibernate

이제 본격적으로 JDBC를 적용해보자.

적용하기에 앞서 insert 쿼리 합치는 설정을 해주기 위해
jdbc:mysql://[DB의 URL]:[DB 포트]/[스키마]?rewriteBatchedStatements=true
rewriteBatchedStatements=true 부분을 꼭 추가해주자

JDBC 적용

@Repository
public class RecommendUserJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public RecommendUserJdbcRepository(DataSource dataSource){
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void saveRecommendUsers(List<RecommendUser> recommendUsers){
        String sql = "INSERT INTO RECOMMEND_USER (user_id, problem_id, status, created_at, updated_at) " +
                " VALUES (?, ?, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)";

        jdbcTemplate.batchUpdate(sql,
                recommendUsers,
                recommendUsers.size(),
                (PreparedStatement ps, RecommendUser recommendUser) ->{
                    ps.setLong(1, recommendUser.getUserId());
                    ps.setLong(2, recommendUser.getProblemId());
                });
    }
}

테스트

	@Test
    @DisplayName("추천 데이터 삽입 테스트")
    @Transactional
    void insert_recommend_user() throws Exception {

        // given
        List<RecommendUser> recommendUsers = new ArrayList<>();
        for(long i=1; i<=TEST_COUNT; i++){
            recommendUsers.add(RecommendUser.createRecommendUser(i,i));
        }
        recommendUserJdbcRepository.saveAll(recommendUsers);
        // expected
        assertThat(recommendUsers).hasSize(TEST_COUNT);
    }

결과

INSERT INTO RECOMMEND_USER (user_id, problem_id, status, created_at, updated_at)
  VALUES (1, 1, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(2, 2, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(3, 3, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(4, 4, 'ACTIVE', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
...

14분 12초 -> 3.8초로 매우 많이 개선됐다.


결론

  • JPA만으로 해결하려하기 보다는 JDBC나 네이티브 쪽도 같이 잘 활용하면 좋습니다.

 

참고자료

https://jojoldu.tistory.com/507

profile
몰입하는 개발자

1개의 댓글

comment-user-thumbnail
2023년 12월 22일

엄청나네요

답글 달기