JdbcTemplate batchUpdate()를 활용한 Bulk Insert

Hyunho·2023년 8월 2일
0

JPA를 사용하면서 다수의 데이터를 저장 하기위해 spring data jpa의 saveAll()을 이용 하였지만 bulk insert로 처리되지 않는 것을 발견하여, 성능 문제를 개선한 내용입니다.

성능 이슈 발생한 상황

use case는 다음과 같습니다.
관리자가 공연을 등록할 때 공연장 좌석 정보 데이터도 함께 등록이 되는 상황입니다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PerformanceService {
    private final PerformanceRepository performanceRepository;
    private final PlaceRepository placeRepository;
    private final SeatRepository seatRepository;

    @Transactional
    public CreatePerformanceResult create(CreatePerformanceValue createPerformanceValue) {
    
        //... 생략
        
        Performance newPerformance = createPerformanceValue.toEntity(place);
        performanceRepository.save(newPerformance);

        seatRepository.saveAll(
            performanceSeats.getSeats(newPerformance.getId())
        );

        return new CreatePerformanceResult(
            newPerformance,
            new PerformancePlace(place)
        );
    }
}

위 코드를 보면 jpa에서 제공해주는 saveAll() 을 활용하여, 좌석 정보를 저장 하는 로직을 작성하였는데 확인 결과가 insert가 생성하려는 좌석 수 만큼 발생하는 것을 확인 하였습니다.(100건 등록시 insert 100번 발생)
spring data jpa 구현체인 SimpleJpaRepository를 확인해 보면 saveAll() 내부에서 for문을 통해 save()를 호출 하는 것을 확인할 수 있습니다.

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    ...

	@Transactional
	@Override
	public <S extends T> List<S> saveAll(Iterable<S> entities) {

		Assert.notNull(entities, "Entities must not be null");

		List<S> result = new ArrayList<>();

		for (S entity : entities) {
			result.add(save(entity));
		}

		return result;
	}
}

Hibernate 에서 batch insert를 제공하지만 기본키 생성 전략중 IDENTITY 전략에서는 Hibernate가 batch insert를 비활성화 하기 때문에 사용이 불가능 합니다.
비활성화를 하는 이유는 영속성 컨텍스트 내부에 엔티티를 식별할때 엔티티 타입과 PK값으로 식별하지만, IDENTITY전략의 경우 DB에 Insert한 후 PK 확인이 가능(jpa - entityManager.persist 시 insert 쿼리 발생)하기 때문입니다.
(IDENTITY - 기본키 생성을 DB에 위임하는 전략)

해당 서비스는 기본키 생성 전략을 IDENTITY를 활용 하기때문에 해당 성능 문제를 해결하기 위해 JdbcTemplate를 활용해 Bulk Insert 를 하였습니다.

JdbcTemplate BatchUpdate

Mysql을 사용 하고 있다면 rewriteBatchedStatements=true를 작성 해주셔야 합니다.

MySQL Connector/J 8.1 Developer Guide

Stops checking if every INSERT statement contains the "ON DUPLICATE KEY UPDATE" clause. As a side effect, obtaining the statement's generated keys information will return a list where normally it would not. Also be aware that, in this case, the list of generated keys returned may not be accurate. The effect of this property is canceled if set simultaneously with "rewriteBatchedStatements=true".

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/reservation?rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234

JdbcRepository 구현 코드

@Repository
@RequiredArgsConstructor
public class PerformanceJdbcRepository implements JdbcRepository {
    private final JdbcTemplate jdbcTemplate;

    @Override
    public void saveAll(List<Seat> seats) {
        String sql = "INSERT INTO reservation.seat (created_at, updated_at, is_reserved, location, number, performance_id)"
                + "VALUES (now(), now(), ?, ?, ?)";
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                Seat seat = seats.get(i);

                ps.setBoolean(1, seat.getIsReserved());
                ps.setString(2, seat.getLocation());
                ps.setInt(3, seat.getNumber());
                ps.setLong(4, seat.getPerformanceId());
            }

            @Override
            public int getBatchSize() {
                return 100;
            }
        });
    }
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PerformanceService {
    private final PerformanceRepository performanceRepository;
    private final PlaceRepository placeRepository;
    private final JdbcRepository jdbcRepository;

    @Transactional
    public CreatePerformanceResult create(CreatePerformanceValue createPerformanceValue) {
        ... 생략코드
        Performance newPerformance = createPerformanceValue.toEntity(place);
        performanceRepository.save(newPerformance);

        jdbcRepository.saveAll(
            performanceSeats.getSeats(newPerformance.getId())
        );

        return new CreatePerformanceResult(
            newPerformance,
            new PerformancePlace(place)
        );
    }
}

JPA saveAll()과 JdbcTemplate batchUpdate() 성능 비교

JMeter를 활용하여 테스트로 등록할 좌석정보 데이터는 30,000건 이며, load time(응답시간)을 비교한 결과입니다.
JPA saveAll() 결과 - 40127ms
image
JdbcTemplate batchUpdate() 결과 - 565ms
image

profile
hyunho

0개의 댓글