Spring/Spring Jpa

spring jpa를 활용한 데이터 저장 성능 향상(saveAll(), bulk insert)

윤밥밥 2024. 5. 16. 00:21

Spring boot를 이용하여 프로젝트 과제를 하다가,

20만 건 이상의 데이터를 저장시키는 작업이 필요했다.

 

“그냥 저장시키면 되겠지”라는 안일한 생각으로 저장로직을 작성해 저장을 시켜보니

무려 40 이라는 시간이 걸리는 게 아닌가..

 

내 기준 40분이면 밥🍚 먹고 설거지까지 마칠 시간이라 도저히 용납할 수 없었다.

그래서 어떻게 성능을 올릴 수 있을까 고민하는 시간을 가져봤다.

 

1. @Transactional 쓰기 지연을 활용할 수 없을까?

어디서 가장 크게 성능 저하가 일어날까 고민해봤을 때

db에 저장될 때 성능저하가 크게 일어나지 않을까 추측하였다.

 

왜냐하면 db에 작업을 할 때에는 매 번 db에 연결하기 위해 작업이 필요하기 때문이다.

그래서 db에 연결하는 횟수를 줄이기 위한 방법을 고민하게 되었다.

 

이론적으로 insert쿼리문이 한번에 몰아서 날라가면 된다.

 

일단, 내 코드는 여러 데이터들을 for문을 돌면서 save()를 호출하여 차례차례 저장했었다.

 

@Transctional이 붙은 메서드는 메서드 호출이 끝날 때, 한번에 commit된다고 알고 있었기에 이를 잘 설정하면 되지 않을까 생각했고

다음과 같이 메서드 위에 @Transactional을 붙이면 한번에 몰아서commit되지 않을까 생각하였다.

 

하지만 틀렸다.

 

이에 대해 잘 설명해주는

트랜잭션의 동작 원리에 대해 잘 정리된 글이 있어 참고하면 될 거 같다.

@Transactional 과 PROXY

 

@Transactional 과 PROXY

@Transactional에는 Spring AOP의 Proxy방식이 사용된다. 그렇기 때문에 우선 Proxy에 대해서 알아보자.Spring에는 크게 두 가지 프록시 구현체를 사용한다. JDK PROXY(=Dynamic PROXY) 와 CGLib이다. Spring AOP

velog.io

 

원인을 말하자면,

나는 A라는 메서드에 @Transactional 이 붙고, A메서드 안에서 @Transational 이 붙은 B라는 메서드를 호출하면 @Transactional 이 A기준으로 적용될 거라 생각하였다.

 

하지만 이는 같은 클래스에 두 메서드가 같이 있냐 없냐에 따라 달라지며,

jpaRepository.save()는 내가 사용하는 메서드와 다른 클래스에 존재하여, save()는 자신이 갖고 있는 @Transactional 의 영향을 받는다는 것이다.

 

즉 save() 메서드 호출이 끝나면 무조건 insert 쿼리가 나가게 된다.

 

따라서 JpaRepository의 save() 메서드를 사용하는 한 @Transactional을 이용해 성능 개선하기는 어렵다고 판단이 들었다.

2. jpaRepository.save() 대신 jpaRepository.saveAll() 사용

save() 메서드와 saveAll() 메서드는 각각 모두 @transaction 이 명시되어 있기에,

save() 와 saveAll() 모두 각 메서드 호출이 끝날 때 commit되는 것은 변함이 없다.

 

하지만 saveAll() 은 다음과 같은 코드로 되어있다.

@Transactional
	@Override
	public <S extends T> S save(S entity) {

		Assert.notNull(entity, "Entity must not be null.");

		if (entityInformation.isNew(entity)) {
			em.persist(entity);
			return entity;
		} else {
			return em.merge(entity);
		}
	}

@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;
	}

즉 saveAll() 메서드에서 save() 메서드를 호출하더라도 같은 클래스 내부에 save() , saveAll() 이 존재하므로 saveAll() 은 @Transactional을 적용하여도 다음과 같다.

public <S extends T> List<S> saveAll(Iterable<S> entities) {
		EntityTransaction tx = em.getTransaction();
    tx.begin();

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

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

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

즉 saveAll 은 프록시 되지 않은 save 메서드를 호출하기에, saveAll() 이 끝난 다음에야 commit되는 것이다.

 

이러한 지식을 토대로

save() 부분을 saveAll() 메서드를 호출하여 db에 저장하도록 해보았다.

 

약 70초가 소요되었다.

 

기존 40분에서 70초로 성능이 향상되다니..

이 정도로 차이가 날 수 있다는 걸 아니 경악할 수 밖에 없었다.

3. bulk Insert 사용하기

하지만 2번의 saveAll() 또한 각각의 record에 대해 insert쿼리를 날리기에 비효율적인 것은 여전하다.

bulk Insert를 사용한다면

insert into post_table (post_title, post_content) values (title1, content1)
insert into post_table (post_title, post_content) values (title2, content2)
insert into post_table (post_title, post_content) values (title3, content3)
insert into post_table (post_title, post_content) values (title4, content4)

기존 위와 같은 insert쿼리를

insert into post_table (post_title, post_content)
	values
	(title1, content1),
	(title2, content2),
	(title3, content3),
	(title4, content4)

아래와 같이 한번에 저장되도록 할 수 있다.

 

bulk insert를 위해서는 sequence 전략을 사용해야 한다.

왜냐하면 identity전략에 대한 bulk insert를 Hibernate쪽에서 막아놨기 때문이다.

(자세한 건 아래 참고 확인)

Spring JDBC를 사용하여 Batch Insert 수행하기

 

Spring JDBC를 사용하여 Batch Insert 수행하기

서론 평소에 JPA를 활용하면서 데이터를 추가할 때, 많은 개발자들이 repository.save() 함수를 이용하여 단건 단위로 저장하는 로직을 구현합니다. 그러나 수백, 수천 개 이상의 대량 데이터를 저장

dkswnkk.tistory.com

 

 

하지만 mysql에서는 sequence 전략을 제공하지 않아 사용할 수 없기에,

이를 해결하기 위해 JdbcTemplate를 사용하여 bulk insert를 구현하였다.

 

이에 대한 구현법은 위 링크에 잘 정리되어 있기에 참고하며 될 거 같다.

 

이를 이용하여

다시 20만개의 데이터에 대해 저장해본 결과

성능이 대략 saveAll() 에 대해 3배 정도 상승하였다.

결론적으로 save() → saveAll() → bulkInsert 순으로 성능이 n배씩 상승한 걸 알 수 있다.

 

 

참고:

Spring JPA Save() vs SaveAll() vs Bulk Insert