메이쁘

[Spring] JPA의 save와 saveAll의 성능 차이, 그리고 원인. 본문

Technology/Web - Spring

[Spring] JPA의 save와 saveAll의 성능 차이, 그리고 원인.

메이쁘 2021. 12. 31. 13:49

안녕하세요?

 

거두절미하고 바로 작성해보겠습니다.

 

 

JPA에서 데이터를 DB에 INSERT하기 위해 사용하는 save와 saveAll이 있습니다.

 

이 두 가지의 큰 차이는

 

save는 1개를 저장하는 것이고,

saveAll은 안에 들어가있는 N개의 데이터를 저장하는 것입니다.

 

 

그럼, 같은 N건을

save와 saveAll을 사용해서 저장하게 되면

어떤 성능 차이가 있을까요?

 

0. 테스트

@Nested
    @DisplayName("save와 saveAll 성능 비교 테스트")
    class SaveTest {

        @Transactional
        @Test
        void SAVE_시간_측정_테스트() {
            long start = System.currentTimeMillis();

            int count = 100000;
            String memberId = "memberId";
            String pwd = "temp";
            String name = "이름";

            while(count --> 0) {
                Member member = Member.builder()
                        .memberId(memberId + count)
                        .pwd(pwd)
                        .name(name + count)
                        .build();

                memberRepository.save(member);
            }

            System.out.println("elapsed time : "  + (System.currentTimeMillis() - start) + "ms.");
        }

        @Transactional
        @Test
        void SAVEALL_시간_측정_테스트() {
            long start = System.currentTimeMillis();

            int count = 100000;
            String memberId = "memberId";
            String pwd = "temp";
            String name = "이름";

            List<Member> members = new ArrayList<>();
            while(count --> 0) {
                Member member = Member.builder()
                        .memberId(memberId + count)
                        .pwd(pwd)
                        .name(name + count)
                        .build();

                members.add(member);
            }

            memberRepository.saveAll(members);
            System.out.println("elapsed time : "  + (System.currentTimeMillis() - start) + "ms.");
        }
    }

위와 같이, 하나의 클래스를 만들고 

안에 save와 saveAll 두 개의 함수를 만들었습니다.

 

System.currentTimeMillis()를 활용해 실행 시간을 측정하는데요.

 

Member Entity에는 간단하게 id, pwd, name이 담겨있습니다.

이를 JpaRepository의 save와 saveAll 함수를 호출해서 총 10만 건을 저장하는데요.

 

 

위 Unit Test 결과!

→ save() 결과 : 2007ms

 

 

→ saveAll() 결과 : 731ms

 

 

거의 2.5배 정도의 실행시간 차이가 발생합니다.

지금은 10만건이지만, 100, 500만 건 정도 데이터의 개수가 늘어날수록 성능차이는 더 커지게 됩니다.

 

 

그럼, 결론은

 

다 건 데이터를 Insert할 때는

for loop로 하나씩 save하는 것 보단

List에 entity를 전부 담아서 한 번의 saveAll이 더 성능에 좋습니다.

 

 

음.. 원인이 무엇일까요?

 

 


1. save와 saveAll

각각의 함수가 어떻게 동작하는지 파도타고 찾아봤습니다.

 

 

SimpleJpaRepository.java

JpaRepository의 save 함수 입니다.

 

saveAll 함수 입니다.

 

 

코드를 보면, 두 개의 차이를 아시나요?

 

둘 다 @Transactional이 적용되어 있습니다.

하지만, saveAll같은 경우에는 for Loop를 통해 entity 하나 씩 save() 함수를 호출해서 insert 하지만, 같은 인스턴스 내의 함수인 save를 호출합니다.

 

이 차이 때문에 성능에서 크게 차이가 납니다.

 

 

정리하자면,

 

10만 건 save 시

1) save → 1건 마다 save() 함수 호출. 

2) saveAll → 1건 마다 인스턴스 내부의 save() 함수 호출.

 

입니다.

 

 


2. 원인

위 정리에 따르면, 둘 다 save() 함수 호출 개수는 동일한거 아니야?

근데 왜 성능 차이가 나지?

 

까지도 생각날 수 있습니다.

 

 

하지만, @Transactional을 알고 계신다면, 위 생각을 쉽게 접게 됩니다.

 

 

@Transactional은 해당 메소드(클래스, 인터페이스도 가능합니다.) 에 트랜잭션을 생성해주는 어노테이션입니다.

이러한 트랜잭션 덕분에 메소드 내부의 DB 로직은 하나의 트랜잭션으로 묶여서 처리할 수 있고요.

 

@Transactional은 AOP 프록시 기반으로, 외부 Bean 객체가 있고, 이 객체의 함수를 호출해야 Intercept가 되어 트랜잭션으로 묶이게 됩니다.

 

그렇기 때문에 Bean 객체 내부에서 내부함수 호출하게 되는 경우 @Transactional이 적용이 되지 않습니다.

 

 

save() 호출 시 상위에 Transaction이 존재하는 경우, 해당 Transaction에 참여하게 됩니다.

하지만, 존재하지 않는 경우 새로 Transaction을 생성하고, save 후 commit합니다.

 

기존 Transaction에 참여하게 되더라도, 외부 Bean(repository) 객체의 save 함수를 호출하는 것이기 때문에, 위 과정이 생겨 비용이 발생합니다.

이는 개수가 많아질수록 비용은 점점 커질거고, 그만큼 시간도 더 오래 걸리게 되죠.

 

 

 

반면, saveAll() 같은 경우는 Bean 객체의 내부함수를 호출하기 때문에 save() 호출마다 트랜잭션이 생성되거나 참여하는 프록시 로직을 전혀 타지 않고, 단순한 메소드 호출만 하기 때문에 위와 같은 비용이 발생하지 않습니다.

 

 

Transaction 정리 및 내부 Transaction 범위 관련 재공부는 아래 링크에서 확인하세요!

https://maivve.tistory.com/337

 

[Spring] 예상 Q&A 공부(Transactional 트랜잭션 적용 범위)

안녕하세요. CS관련 질문과 면접에 맞는 답변을 작성하면서 지식도 쌓고 면접도 대비하는 시간을 가지려고 합니다. 틈틈히 게시글을 작성하며 면접 대비 데이터셋을 확보해둔 다음 언젠간 면접

maivve.tistory.com

 

 


3. 결론

 

save 함수 한 번당 트랜잭션 Proxy 로직을 태웁니다.

  → 그렇기 때문에, 기존 트랜잭션이 있는 경우 해당 트랜잭션에 참여하게 되지만, 이러한 경우에도 어느정도 비용이 발생합니다.

  → 그래서, 다 건 조회 시 매번마다 위 비용이 발생하기 때문에 시간이 오래 걸립니다.

 

 

반면, saveAll 함수 전체에 하나의 트랜잭션이 존재하고, save는 내부 메소드 호출로 트랜잭션 Proxy 로직을 태우지 않습니다.

  → 그렇기 때문에, 트랜잭션 관련 비용이 발생하지 않습니다.

  → 그래서, 다 건 조회 시에 적합합니다.

 

 

 

 

 

 

이상입니다.

 

감사합니다!

 

Comments