MySql에서 JPA bulk insert가 안되는 이유

JPA bulk insert가 안되는 이유

Featured image

개요

상품리스트 페이지에서 각 상품별로 최대 20개의 상품카테고리를 등록시키는 기능을 개발하였다. 상품리스트 페이지에서는 최대 200개까지 상품을 리스트형태로 노출시켜주고 있었다. 즉, 최대 4000개의 data row가 insert 될 수 있는 구조였다.

테이블 구조

@Entity
@Table(name = "pd_mall_product")
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
public class MallProduct {
    
  //...

  @ManyToMany(fetch = FetchType.EAGER)
  @JoinTable(name = "pd_mall_product_category_mapping", joinColumns = @JoinColumn(name = "mall_product_no"), inverseJoinColumns = @JoinColumn(name = "display_category_no"))
  @Fetch(FetchMode.SUBSELECT)
  private List<DisplayCategory> displayCategories;

  //...
}
@Immutable
@Entity
@Table(name = "pd_mall_product_category_mapping")
@Getter
@Setter
public class MallProductDisplayCategoryMapping {

    @Column(name = "mall_product_no")
    private int mallProductNo;
    
    @Id
    @Column(name = "display_category_no")
    private int displayCategoryNo;
    
    //...
}

코드는 displayCategoryNo에만 @Id가 설정돼있지만 사실상 mallProductNo와 displayCategoryNo가 복합키 형태를 가져야 하기 때문에 아래와 같이 @IdClass를 활용한 복합키 설정을 해줬어야 한다.(복합키 설정 없이 매핑 테이블에 bulk insert 적용 시 에러가 발생한다.)

@Immutable
@Entity
@Table(name = "pd_mall_product_category_mapping")
@Getter
@Setter
@IdClass(MallProductDisplayCategoryMapping.MallProductDisplayCategoryMappingPK.class)
public class MallProductDisplayCategoryMapping {

    @Id
    @Column(name = "mall_product_no")
    private int mallProductNo;
    @Id
    @Column(name = "display_category_no")
    private int displayCategoryNo;
    
}

@Getter
@Setter
public static class MallProductDisplayCategoryMappingPK implements Serializable {
  private int mallProductNo;
  private int displayCategoryNo;
}

이외에 @EmbeddedId, @Embeddeable를 활용하여 복합키를 설정하는 방법도 있다.

@Entity
@Table(name = "pd_display_category")
@Data
@NoArgsConstructor
public class DisplayCategory {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "display_category_no", nullable = false)
    private int displayCategoryNo;
    
    @JsonIgnore
    @ManyToMany(mappedBy = "displayCategories", fetch = FetchType.LAZY)
    private List<MallProduct> mallProducts;
    
    //...
}

현재 테이블 설계는 상품 테이블인 MallProduct와 카테고리 테이블인 DisplayCategory 사이에 다대다 관계로 매핑돼있고, MallProduct에서 @JoinTable을 통해 상품과 카테고리 테이블을 서로 매핑해주는 MallProductDisplayCategoryMapping 테이블을 통해 연관관계를 관리한다.
mallProductNo는 MallProduct 테이블의 외래키, displayCategoryNo는 DisplayCategory의 외래키 구조를 갖게 된다.

이슈

MallProductDisplayCategoryMapping 테이블에 1개의 상품당 최대 20개의 displayCategoryNo가 insert되게 된다. 따라서 위에서 말했던것과 같이 한번에 이벤트에 최대 4000개의 row data가 저장되게 된다.
여기서 일반적인 형태로 MallProductDisplayCategoryMappingRepository.save(); 형태로 data를 저장하게 되면 아래와 같이 row 별로 일일히 persist(), flush() 과정을 거치게 되면서 처리 시간이 40초 가량 소요되었다.(해당 데이터 뿐만이 아닌 History 테이블 등등에도 insert작업이 있었다.)

log

그리하여, 성능을 개선할 수 있는 방법으로 JPA bulk insert 방법을 먼저 적용해보았다.

JPA bulk insert

  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
        format_sql: true
        physical_naming_strategy: com.***.CamelCaseColumnStrategy
        implicit_naming_strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl
        use_sql_comments: true
        show_seq: true
        jdbc:
         batch_size: 500 *
           batch_versioned_data: true
         order_inserts: true *
         order_updates: true *
         enable_lazy_load_no_trans: true

위와 같이 설정 후 save처리를 하게 되면 아래와 같이 500개씩의 row data가 한꺼번 table에 insert되게 된다.

log1 log2

하지만 소요시간을 재보니 19초가 소요된다. 뭔가 이상하다. log3

그 이유를 알아본 결과 jpa bulk insert를 사용하기 위해서 몇가지 제약 조건이 있었다.

결론

MySql에서 JPA bulk insert를 활용해보려면 auto increment(IDENTITY)타입으로 id를 관리하는 것이 아닌 TABLE 타입을 활용해야 한다.
아니면 spring JDBC, MyBatis, JOOQ를 활용하는 것도 방법이다.

알아둘 점

  1. 다대다 매핑은 @ManyToMany보다는 중간에 매핑테이블과 서로 @OneToMany, @ManyToOne으로 연관관계를 매핑해주는 형태가 되게 된다.
  2. @ManyToMany를 사용하게 된다면 List보다는 Set을 사용하는 것이 성능상 이점이 있다.