실전 개발해보기

Bulk 데이터 삽입 최적화 - save() , saveAll() , batchUpdate()

김용녀 2023. 12. 9. 14:36

 

 

 

상황

 

토이 프로젝트로 스포츠 경기 예매 시스템을 만들고있다.

 

Game 클래스는 축구,야구 등 경기를 나타내는 도메인이고


Seat 클래스는 한 경기당 생성되는 수만개의 경기 좌석이다.

 

Seat -> Game 은 N:1  단방향 (Seat에서 @ManyToOne 으로) 매핑이 되어있다.

 

스포츠 경기 특성상 한 경기마다 수만개의 좌석을 확보해야한다.

 


 

이 코드 짜기 직전에 RBF에서 팀원이 spring data jpa 에서 save()와 saveAll() 의 성능 비교를 보여줘서

bulk 데이터 저장에 쉽게 접근할 수 있었다.

 

https://voidmelody.tistory.com/189

 

JPA의 save와 saveAll의 성능차이

spring batch를 통해 데이터를 가공하고 저장하는 업무가 있었다. 당시 나는 for문을 통해 단순히 한 건마다 save를 진행해서 코드를 작성했는데 생각보다 처리 시간이 너무나 오래 걸려 이를 최적화

voidmelody.tistory.com

 

그래도 직접 비교해보고자 둘다 실행해봤다!

 

 

GameService

 

save() 

//game 생성할때 좌석도 생성을 해야함.
    public void createSeat(Game game) {
        
        for (long i = 1L; i <= 100_000; i++) {
            seatRepository.save(Seat.builder()
                    .id(i)
                    .seatType(SeatType.VIP)
                    .game(game)
                    .availability(true)
                    .build());
        }
    }

 

save()결과

 

 

saveAll()

//game 생성할때 좌석도 생성을 해야함.
    public void createSeat(Game game) {
        List<Seat>seats = new ArrayList<>();
        for (long i = 1L; i <= 100_000; i++) {
            seats.add(Seat.builder()
                    .id(i)
                    .seatType(SeatType.VIP)
                    .game(game)
                    .availability(true)
                    .build());
        }
        seatRepository.saveAll(seats);
    }

 

saveAll()

 

팀원이 말한대로 거의 두배정도의 차이를 보였다.

 


 

 

한 경기 테스트하는데 54초?.. 

saveAll()도 결국 일일히 save()를 날리기 때문에 여전히 오래걸린다.

 

spring data jpa repository 인터페이스를 이용하지 않고

 

jdbc 순수 메서드인 batchUpdate()가 있었다.

 

GameService

  public void createSeat(Game game) {
        //game 생성할때 좌석도 생성을 해야함.
        List<Seat> seats = new ArrayList<>();
        for (long i = 1L; i <= 100; i++) {
            seats.add(Seat.builder()
                    .id(i)
                    .seatType(SeatType.VIP)
                    .game(game)
                    .availability(true)
                    .build());
        }

        seatBulkRepository.saveAll(seats);
    }

 

SeatBulkRepository ( jpa x, 순수 클래스)

  @Transactional
    public void saveAll(List<Seat> seats){
        String sql = "INSERT INTO seat(id,seat_type,game_id,availability) "+
                "VALUES (?, ?, ?, ? )";

        jdbcTemplate.batchUpdate(sql,
                seats,
                seats.size(),
                (PreparedStatement ps, Seat seat) -> {
                    ps.setLong(1, seat.getId());
                    ps.setString(2, String.valueOf(seat.getSeatType()));
                    ps.setBytes(3, uuidToBytes(seat.getGame().getId()));
                    ps.setBoolean(4, seat.isAvailability());

                });

    }

 

batchUpdate는 

하나의 레코드마다 insert 쿼리가 날라가지 않는다. 쌓아서 한번에 날라간다.

 

예를들어 2개의 객체를 저장할때

 

spring data jpa 의 save()는 

insert  into table_name(col1,col2) values (val1,val2);

insert  into table_name(col1,col2) values (val1,val2);

 

두번 날라간다.

 

반면 jdbc의 메서드인 batchUpdate()는

 

insert  into table_name(col1,col2) values (val1,val2),(val3,val4);

 

한번에 날라간다.

 

 

그덕분에 10만개의 데이터를 저장함에도 2초라는 비약적인 시간 단축을 할 수 있었다.

 

 


 

Reference

 

- https://hyeonic.github.io/spring/jdbc/spring-jdbc-batch.html#jdbctemplate%E1%84%8B%E1%85%B4-batchupdate-%E1%84%86%E1%85%A6%E1%84%89%E1%85%A5%E1%84%83%E1%85%B3

- https://gksdudrb922.tistory.com/154