Programming/Spring Boot

JPA save, saveAll 성능 차이가 발생하는 이유

Jan92 2023. 8. 26. 11:29
반응형

JPA save(), saveAll() 성능 차이가 발생하는 이유(Feat. @Transactional)

JPA 구현체 Hibernate

Spring Data Jpa에서 데이터를 insert 할 때 'save()' 메서드 또는 'saveAll()' 메서드를 사용할 수 있는데요.

구현된 코드를 살펴보면 saveAll() 메서드는 내부적으로 save() 메서드를 반복하는 구조로 되어 있습니다.

그러면 단순하게 생각해서 여러 건의 데이터를 반복문을 통해 각각 save() 하는 것과 한 번에 saveAll() 하는 것에는 차이가 없을 것 같은데요.

 

결론을 먼저 말씀드리면, 여러 건의 데이터를 insert 할 때는 saveAll() 메서드를 사용하는 것이 성능상 더 좋으며, 해당 포스팅은 'saveAll()을 사용하였을 때 성능 차이가 발생하는 이유'에 대해 정리한 내용입니다.

 


성능 차이 테스트

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "shops")
public class Shop {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    @Column(name = "id", nullable = false)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    public Shop(String name) {
        this.name = name;
    }
}

(테스트에 사용할 Shop Entity)

 

@SpringBootTest
public class ShopServiceTests {

    @Autowired
    private ShopRepository shopRepository;

    @DisplayName("10만 건 save() test")
    @Test
    public void saveMethodTest() {
        for (int i = 0; i < 100000; i++) {
            shopRepository.save(new Shop("shop" + i));
        }
    }

    @DisplayName("10만 건 saveAll() test")
    @Test
    public void saveAllMethodTest() {
        List<Shop> shopList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            shopList.add(new Shop("shop" + i));
        }

        shopRepository.saveAll(shopList);
    }
}

(10만 건의 데이터에 대한 비교 테스트)

 

먼저 다음과 같은 테스트 코드를 통해 10만 건의 Shop 객체를 저장해 보았으며, 아래와 같은 결과를 얻을 수 있었는데요.

 

save(), saveAll() 비교 테스트 결과

saveAll() 메서드를 통해 데이터를 저장하는 방식이 save() 메서드를 반복문을 돌려 저장하는 방식에 비해 절반 정도의 시간이 소요되는 것을 확인할 수 있었습니다.

 

 


save(), saveAll()

위와 같은 테스트 결과가 발생하는 이유를 찾기 위해 각각의 메서드가 어떻게 구현되어 있는지 살펴보았는데요.

 

save() method

 

saveAll() method

앞서 말한 것처럼 saveAll() 메서드는 내부적으로 반복문을 통해 save() 메서드를 호출하고 있는 것을 볼 수 있었습니다.

그리고 두 메서드 모두 @Transactional 어노테이션이 적용된 것을 볼 수 있는데요.

성능적 차이가 발생하는 이유가 바로 '@Transactional Annotation' 때문입니다.

 

 

***

@Transactional은 스프링에서 지원하는 선언적 트랜잭션 처리 방법으로, 프록시 방식으로 동작하는 Spring AOP(Aspect Oriented Programming)의 대표적인 예로 볼 수 있습니다.

 

 


@Transactional

@Transactional 어노테이션을 선언하면 해당 메서드를 하나의 트랜잭션 안에서 수행할 수 있는데요.

 

이때 만약 saveAll() 메서드에서 save() 메서드를 호출하는 것처럼 트랜잭션 내부에서 트랜잭션을 또 호출한다면 어떻게 될까요?

내부에서 호출되는 트랜잭션에 대해서는 새로운 트랜잭션이 생성되거나, 상위 트랜잭션에 합류되는 등의 경우가 발생할 수 있을 것 같은데요.

이처럼 진행되고 있는 트랜잭션에서 다른 트랜잭션이 호출될 때 어떤 방식으로 처리할지 결정하는 것을 '트랜잭션의 전파 설정'이라고 합니다.

 

트랜잭션 전파 설정의 옵션으로는 REQUIRED(default), REQUIRES_NEW, MANDATORY, NESTED, NEVER, SUPPORTS, NOT_SUPPORTED가 존재하는데요.

 

여기서 save()와 saveAll() 메서드의 @Transactional 전파 속성은 default 속성인 'REQUIRED'가 적용되어 있습니다.

REQUIRED 속성의 경우 상위 트랜잭션이 있을 경우 상위 트랜잭션으로 합류하며, 상위 트랜잭션이 없을 경우 새로운 트랜잭션을 생성하는 옵션인데요.

 

 

***

즉, 반복문 내부에서 save() 메서드를 통해 데이터를 저장하는 경우 save() 메서드가 호출될 때마다 새로운 트랜잭션이 생성됩니다.

하지만 saveAll() 메서드의 경우 내부적으로 save() 메서드가 호출될 때, 새로운 트랜잭션이 생성되는 것이 아니라 saveAll() 메서드 호출 시 생성된 상위 트랜잭션에 합류하게 된다는 차이점이 발생하는데요.

 

정리하자면, 여러 건의 데이터에 대해 saveAll() 메서드를 사용하는 경우, 하나의 트랜잭션 안에서 모든 과정이 수행되는 것으로 save() 마다 트랜잭션을 생성해야 하는 불필요한 프록시 과정이 발생하지 않게 되며, 이것이 save()와 saveAll() 사이의 성능 차이를 발생시키는 것입니다.

 

 

2023-08-25T22:45:11.607+09:00 DEBUG 1742 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

// insert shop
// insert shop
// insert shop

2023-08-25T22:45:11.662+09:00 DEBUG 1742 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1112584385<open>)]

(3건의 데이터에 대해 saveAll() 메서드를 사용한 경우)

 

2023-08-25T22:40:54.966+09:00 DEBUG 1722 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
// insert shop
2023-08-25T22:40:55.016+09:00 DEBUG 1722 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(186540981<open>)]

2023-08-25T22:40:55.028+09:00 DEBUG 1722 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
// insert shop
2023-08-25T22:40:55.032+09:00 DEBUG 1722 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1454357198<open>)]

2023-08-25T22:40:55.035+09:00 DEBUG 1722 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
// insert 
2023-08-25T22:40:55.038+09:00 DEBUG 1722 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(289049029<open>)]

 (3건의 데이터에 대해 save() 메서드를 반복하는 경우)

 

실제 트랜잭션 생성 여부를 확인해보고 싶다면 해당 과정이 실행되는 부분의 로깅 레벨을 DEBUG로 바꾸고 실행하게 되면 다음과 같은 로깅 결과를 확인하실 수 있습니다.

(적용된 트랜잭션 전파 속성이 PROPAGATION_REQUIRED 인 것도 확인할 수 있습니다.)

 

 

 

< 참고 자료 >

https://sas-study.tistory.com/388
https://deveric.tistory.com/86

 

 

< Spring AOP 관련 포스팅 >

2021.08.31 - [Programming/Spring Boot] - 관점 지향 프로그래밍 Spring AOP 개념과 사용법 - 1

반응형