[Spring Data JPA] save와 saveAll의 성능 차이에 대한 실험과 결과!(스프링 프록시, @Transactional)

안녕하세요. 오늘은 Spring Data JPA를 활용할때 기본 Insert로 사용하는 save 기능과 saveAll 기능에 대한 성능 실험을 해보려고 합니다. 

 

Spring Data JPA의 경우 Bulk Insert의 경우 많은 성능 이슈를 발생시켜서 JPA로 bulk Insert를 하는 것을 권장하지는 않은 것으로 알려져 있는 것으로 알고 있습니다.

 

하지만 어느정도 로직이 있다면 이를 해소해볼 수 있을까? 하는 생각에 재미삼아 해보았으니 결과만 즐겨보시기 바랍니다.

 

어플리케이션과 서버의 스펙에 따라 차이가 발생할 수 있으니 제 스펙을 적어보겠습니다.

 

실험 스펙

- LG 그램 17 2019년형

- intel cpu i7 8세대

- Ram 16GB

- Spring Boot 2.3 버전, Maven

- Starter-data-jpa 2.2.8 버전

- 실험 Database : PostgreSQL 9.6.14, compiled by Visual C++ build 1800, 64-bit

 


 

해당 테스트는 Qna 라는 단일 엔티티를 가지고 해보았습니다. 예제 코드는 다음과 같습니다.

@NoArgsConstructor
@AllArgsConstructor
@Getter
@EqualsAndHashCode(of = "seq")
@Entity
@Table(name = "user_qna")
public class Qna extends DefaultEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;

    @Column(name = "content", columnDefinition = "text")
    private String content;

    public Qna(String content) {
        this.content = content;
    }

}

public class QnaServiceTests {

    @Autowired
    private QnaRepository qnaRepository;
	List<Qna> list;

    @BeforeEach
    void setup() {
    	list = new ArrayList<>();
        for(int i = 0; i < 100000; i++) {
            list.add(new Qna("qna 내용입니다."));
        }
    }

    @DisplayName("10만건 saveAll")
    @Test
    public void BulkEntityAllSaveTest() throws Exception {
        qnaRepository.saveAll(list);
    }

    @DisplayName("10만건 하나씩 save")
    @Test
    public void BulkEntityOneSaveTest() throws Exception {
        list.forEach(entity -> qnaRepository.save(entity));
    }
    
}

 

- 10만건을 saveAll로 저장하는 것과 loop를 돌면서 단건씩 저장하는 것의 결과 saveAll 의 결과가 대략 2배정도 성능이 좋은 것을 확인했습니다. 해당 테스트 이전 1만, 5만 건을 차례로 진행해보았던 것과 확연히 차이가 나는것 같아요!

 

 

 

save와 saveAll의 성능차이 발생 이유

실제로 save는 10만번 호출되고 saveAll은 1번만 호출됩니다. 이러한 호출횟수와 크게 무관한 것은 아니지만 이렇게 단순한 이유는 아닐것 같아서 현재 Spring data JPA의 의존성을 뜯어보았는데요. save 메소드는 아래와  같이 구현되어 있었습니다.

 

 

save 메소드

 

해당 메소드는 @Transactional로 감싸져 있으며 @Transaction은 많은 스프링 개발자분들이 이미 알고계실테지만 프록시 기반 동작입니다. 즉, 10만번동안 save 메소드가 호출될 때 불필요한 프록시 과정을 거치게 되며 이에 대한 차이가 성능차이가 발생하는 이유로 보여집니다. 

 

그리고 또 한가지 중요한 부분이 보여지는데요. 이는 saveAll 메소드를 확인하면서 설명드리도록 하겠습니다.

 

saveAll 메소드

 

saveAll 메소드 또한 @Transactional 어노테이션에 의해 프록시로 동작되며 내부에서는 save 함수를 여러번 호출하는 것으로 확인되었습니다. 이는 결국 saveAll 또한 마찬가지로 10만번의 save 호출을 진행한다는 의미가 됩니다.

 


 

그러면 왜?! 10만번 호출되는 것은 똑같은데 이러한 차이가 발생하는 것일까요??

 

일단 this. 키워드로 접근했다는 것과 과연 저 호출이 save 메소드의 @Transactional 프록시를 동작하게 하였는가를 알아야 합니다.

 

스프링의 빈은 대부분 프록시 객체로 채워져있습니다. @Transactional 어노테이션은 이러한 프록시 객체의 참조로 실행되어야 제 역할을 하게 되는데요. this 키워드로 호출한 경우 단지 내부 메소드를 호출한 효과를 주게되어 프록시 참조를 통한 호출이 아니게 되는 것입니다.

 

 

실제 성능에 대한 비교 예제 : www.baeldung.com/spring-data-save-saveall

댓글

Designed by JB FACTORY