대규모 데이터를 가지고서 SQL을 활용하여 데이터의 성능을 확인해보고자 할떄, 테스트를 위한 대규모의 데이터들이 필요합니다.

JPA Hibernate 가 Save / SaveAll 에서 JdbcTemplate의 Hibernate가 비활성화된 상태로 사용하기 위해 JdbcTemplate의 Batch Insert를 지원하는 batchUpdate를 활용하여 빠른 시간안에 데이터들을 넣어보겠습니다.

( 사족을 붙여보자면,  처음에 JPA를 활용하여 100만건을 넣어보고 어느정도 걸리나 궁금하여 확인해보았더니 57분이 걸려있었습니다.. ) 

 

 

1. 이번 테스트에 사용할 Entity 구조입니다.

Member Entity

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"member_role_set", "member_seminar_list"})
public class Member extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_no")
    private Long member_no;

    @Column(length = 100, nullable = false, unique = true, name = "member_id")
    private String member_id;

    @Column(length = 100, nullable = false)
    private String member_password;

    @Column(length = 100, nullable = true)
    private String member_nickname;

    @Column(nullable = false)
    private boolean member_from_social;

    @Column()
    private LocalDateTime del_dt;

    @OneToMany(mappedBy = "member")
    private List<Member_Seminar> member_seminar_list;

}

 

Seminar Entity

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"member_seminar_list"})
public class Seminar extends BaseEntity {

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

    @Column(length = 100, nullable = false, unique = true)
    private String seminar_name;

    @Column(length = 500)
    private String seminar_explanation;

    @Column(nullable=true)
    private LocalDateTime del_dt;

    @OneToMany(mappedBy = "seminar", fetch = FetchType.LAZY)
    private List<Member_Seminar> member_seminar_list;

}

 

Member_Seminar Entity

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"member", "seminar"})
public class Member_Seminar extends BaseEntity {

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

    @ManyToOne(
            targetEntity = Member.class,
            fetch = FetchType.LAZY
    )
    @JoinColumn(name = "member_no")
    private Member member;

    @ManyToOne(
            targetEntity = Seminar.class,
            fetch = FetchType.LAZY
            )
    @JoinColumn(name = "seminar_no")
    private Seminar seminar;

    @Column(nullable=true)
    private LocalDateTime del_dt;

    public void setDel_dt(LocalDateTime del_dt){ this.del_dt = del_dt; }

}

 

2. 100 만건 기준으로 Spring JPA를 활용하여 SaveAll을 활용하는경우.

@DisplayName("dummyInsertWithJPAHibernateSaveAll")
@Test
public void dummyInsertWithJPAHibernateSaveAll(){
    List<Member_Seminar> member_seminarList = new LinkedList<>();
    Optional<Seminar> seminar = seminarRepository.findBySeminar_name("SeminarDummyIndex"+0);;
    for(int i=0;i<100; i++){
        Optional<Member> member = memberRepository.findByMember_id("MemberDummyIndex"+i);
        int cnt=0;
        for(int j=0;j<10000;j++){
            if(j == 0){
                seminar = seminarRepository.findBySeminar_name("SeminarDummyIndex"+j);
            }
            Member_Seminar member_seminar = Member_Seminar.builder()
                    .member(member.get())
                    .seminar(seminar.get())
                    .build();
            member_seminarList.add(member_seminar);
        }
    }
    Long startTime = System.currentTimeMillis();
    memberSeminarRepository.saveAll(member_seminarList);
    Long endTime = System.currentTimeMillis();
    System.out.println("Execution Time:"+ (endTime - startTime) + "ms");
}

84초 644 ms 가 걸렸습니다.

왜 이렇게 오래걸리는 것일까요?

 

먼저 오래걸리는 SAVE 함수부터 살펴보겠습니다.

 

1. SimpleJpaRepository.java - save Method

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

 

위의 save 코드를 확인해보겠습니다.

List 형태로 모든 삽입마다 save를 실행한다면 어떻게 될까요?

각 Transaction이 모두 생성되고, 실행됩니다.

 

2. SimpleJpaRepository.java - saveAll Method

 

@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 코드를 확인해보겠습니다.

List 형태로 모든 save를 실행한다면 어떻게 될까요?

하나의 Trnasaction에서 모든 Entity가 삽입됩니다.

( Transaction에 대해서는 이후에 추가적으로 더 자세하게 알아봐야할 필요가 있을 것 같습니다. )

 

 

추가적으로, 

Member_Seminar Entity의 @Generated 정책을 보면, Identity 타입으로써 Auto_Increment 하는 구조입니다.

이 구조에서는 Entity를 Insert를 하기전에 member_seminar_no가 유일한지, 증가하는지의 충족을 확인하므로 간 Insert마다 모두 SELECT 구조가 실행되므로 오랜 시간이 걸리는 것입니다.

 

즉, 만약에 저처럼 Table의 GenerateValue ( 테이블 ID 생성 전략 ) 이 있지 않다면,  JPA에서도 Bulk Insert 설정을 통해 진행할 수 있지만 저처럼 Identity가 설정되어있다면 JDBCTemplate이 활용하기 좋습니다.

 

3. 100만건 기준으로 JDBCTemplate의 batchUpdate를 활용할경우

3-1. 먼저 application.yaml 에 Bulk Insert를 활용하기 위한 설정값을 넣어줍니다.

datasource:
  driver-class-name: org.mariadb.jdbc.Driver
  url: jdbc:mariadb://BulkHost:3306/BulkTestDB?rewriteBatchedStatements=true&useSSL=false&characterEncoding=UTF-8&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=200
  • 위와 같이 url에 파라미터를 함꼐 넣어줍니다.

JdbcBulkInsert 함수를 선언합니다.

public void jdbcBulkInsert(List<Member_SeminarDTO> member_seminar_list){
    String sql = "insert into member_seminar (member_no, seminar_no) VALUES (?, ?)";
    jdbcTemplate.batchUpdate(sql,
            member_seminar_list,
            member_seminar_list.size(),
            (PreparedStatement ps, Member_SeminarDTO member_seminarDTO) ->{
                ps.setLong(1, member_seminarDTO.getMember_no());
                ps.setLong(2, member_seminarDTO.getSeminar_no());
            });
}

테스트코드를 선언합니다.

@DisplayName("dummyInsertWithJdbcTemplate")
@Test
public void dummyInsertWithJdbcTemplate(){
    List<Member_SeminarDTO> member_seminarDTOList = new LinkedList<>();
    for(int i=1;i<11; i++){
        for(int j=1;j<10001;j++){
            Member_SeminarDTO memberSeminarDTO = Member_SeminarDTO.builder()
                    .member_no((long) i)
                    .seminar_no((long) j)
                    .build();
            member_seminarDTOList.add(memberSeminarDTO);
        }
    }
    Long startTime = System.currentTimeMillis();
    memberSeminarQuerydslRepository.jdbcBulkInsert(member_seminarDTOList);
    Long endTime = System.currentTimeMillis();
    System.out.println("Execution Time:"+ (endTime - startTime) + "ms");

}

 

1초 252ms 가 걸렸습니다.

위의 코드를 살펴보겠습니다. JDBCTemplate의 batchUpdate를 활용하는 경우 JPA를 활용하여 테스트할떄랑 코드가 많이 다른것을 확인할 수 있습니다.

아래의 2가지가 다릅니다.

1. Member_SeminarDTO로 member_seminarDTOList를 넘기고 잇다.

2. member_no와 seminar_no FK 값을 직접 세팅해주고 있다.

왜 위의 2가지처럼 구조를 설정했을까요?

-> Member_Seminar 구조는 N:1 의 관계로 Member와 관계하고,

-> N:1의 관계로 Seminar와 관계하기 떄문입니다.

만약 이 같은 구조에서 해당 Entity를 찾아서 호출할경우, fetchType.LAZY이기에 각각의 호출마다 모든 Entity를 다시 호출하게 됩니다. 

즉, 모든 List를 만들어줄떄마다 JPA에서 Member와 Seminar 의 Entity를 조회하는 쿼리가 다시 실행된다는 것입니다.

그렇게 된다면, Bulk Insert를 하는 장점이 사라집니다.

즉, ManyToOne 관계에서는 JDBC를 활용할경우, 연관관계의 호출을 막기 위해

직접 FK를 처리함으로써 SQL을 빠르게 넣을 수 있습니다.

 

 

 

+ Recent posts