대규모 데이터를 가지고서 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을 빠르게 넣을 수 있습니다.