jpa n+1

9
JPA N+1 2017.02.04( 토 ) 토토토 Study

Upload: nim-bae

Post on 13-Apr-2017

146 views

Category:

Internet


0 download

TRANSCRIPT

불타는Study

01 JPA N+1

01 JPA N+1

-N+1 Probleam?

JPA 를 사용하면 , 성능상 가장 조심해야 하는 N+1 문제 .JPA 로 쿼리 실행시 , 데이터 수만큼 다시 SQL 을 사용해서 조회하는 문제를 N+1 문제라고 한다 .결과적으로 , 여러 번 DB 로 쿼리를 날리기 때문에 데이터가 많으면 많아질수록 SQL 호출횟수가 늘어나므로 , 조회 성능이 떨어진다 . @Getter

@Setter@Entity@Table(name = "orders")public class OrderDomain { @Id @Column(name = "order_id") private int orderId;

// default fetch type = EAGER @ManyToOne(optional = false) @JoinColumn(name = "customer_id") CustomersDomain customer;

// default fetch type = LAZY @OneToMany @JoinColumn(name = "order_id") List<OrderItemDomain> or-derItems;}

@Getter@Setter@Entity@Table(name = "customers")public class CustomersDomain {

@Id @Column(name = "customer_id") private int customerId;

String customerName;}

위와 같은 도메인이 존재한다 . customer 엔티티를 로딩하는 과정에서 N+1 문제가 발생을 한다 .일단 기본적으로 , @ManyToOne 의 페치전략은 EAGER 즉시로딩 방식이다 .Hibernate: select orderdomai0_.order_id as order_id1_4_, orderdomai0_.customer_id as customer2_4_ from orders orderdomai0_Hibernate: select customersd0_.customer_id as customer1_1_0_, customersd0_.customer_name as customer2_1_0_ from customers customersd0_ where customersd0_.customer_id=?Hibernate: select customersd0_.customer_id as customer1_1_0_, customersd0_.customer_name as customer2_1_0_ from customers customersd0_ where customersd0_.customer_id=?Hibernate: select customersd0_.customer_id as customer1_1_0_, customersd0_.customer_name as customer2_1_0_ from customers customersd0_ where customersd0_.customer_id=?… 이하 생략

01 JPA N+1

-N+1 Probleam?

JPA 를 사용하면 , 성능상 가장 조심해야 하는 N+1 문제 .JPA 로 쿼리 실행시 , 데이터 수만큼 다시 SQL 을 사용해서 조회하는 문제를 N+1 문제라고 한다 .결과적으로 , 여러 번 DB 로 쿼리를 날리기 때문에 데이터가 많으면 많아질수록 SQL 호출횟수가 늘어나므로 , 조회 성능이 떨어진다 .@Getter@Setter@Entity@Table(name = "orders")public class OrderDomain { @Id @Column(name = "order_id") private int orderId;

// default fetch type = EAGER @ManyToOne(optional = false) @JoinColumn(name = "customer_id") CustomersDomain customer;

// default fetch type = LAZY @OneToMany @JoinColumn(name = "order_id") List<OrderItemDomain> or-derItems;}

@Getter@Setter@Entity@Table(name = "customers")public class CustomersDomain {

@Id @Column(name = "customer_id") private int customerId;

String customerName;}

위와 같은 도메인이 존재한다 . customer 엔티티를 로딩하는 과정에서 N+1 문제가 발생을 한다 .일단 기본적으로 , @ManyToOne 의 페치전략은 EAGER 즉시로딩 방식이다 .N+1 문제는 컬렉션에 대한 EAGER 로딩 때문에 발생하니까 , 페치전략을 바꾸면 어떤 결과가 나올까 ? 응 ? 쿼리를 한번만 날려서 잘 조회를 해온다 .* JPQL 로 SQL 을 생성하면 , 글로벌 fetch 전략을 참고하지 않고 오직 JPQL 자체만 사용하므로 , 지연로딩이던 즉시로딩이든 구분을 안하니 주의 !!Hibernate: select orderdomai0_.order_id as order_id1_4_, orderdomai0_.customer_id as customer2_4_ from orders orderdomai0_

@Getter@Setter@Entity@Table(name = "orders")public class OrderDomain { @Id @Column(name = "order_id") private int orderId;

// default fetch type = EAGER @ManyToOne(optional = false, fetch = FetchType.LAZY) @JoinColumn(name = "customer_id") CustomersDomain customer;

// default fetch type = LAZY @OneToMany @JoinColumn(name = "order_id") List<OrderItemDomain> orderItems;}

01 JPA N+1

-N+1 해결방안 ?

1. fetch join 을 사용 .

< 페치 조인과 일반 조인의 차이 >일반 조인은 결과를 반환할 때 연관관계까지는 고려하지 않는다 . 단지 SELCT 절에 지정한 엔티티만 조회할 뿐이다 . 따라서 Parent 엔티티만 조회할 때 그와 연관된 Child 엔티티는 조회하지 않는다 .반면에 페치 조인을 사용하면 연관된 엔티티도 함께 조회하여 결과를 얻어 올 수 있다 .

public interface OrderRepository extends JpaRepository<OrderDomain, Integer> { OrderDomain findOne(int orderId);

//fetch 조인을 이용 .(JPQL 에서 제공하는 성능 최적화 기능 ) //연관된 엔티티 또는 컬렉션을 한번에 같이 조회 하는 기능 . join fetch 사용 . //페치 조인은 글로벌 로딩전략보다 우선한다 . @Query("select a from OrderDomain a inner join fetch a.customer inner join fetch a.orderItems") List<OrderDomain> getOrderList();

@Query("select a from OrderDomain a inner join a.customer b inner join a.orderItems c") List<OrderDomain> getOrderListNormalJoin();}

01 JPA N+1

-N+1 해결방안 ?

2. @BatchSize 어노테이션 사용 .

하이버네이트에서 제공하는 BatchSize 어노테이션을 넣어주면 끝이다 . size 속성은 한번에 가져오는 개수를 말한다 .* spring boot 에서 저렇게 하면 , 밑에와 같이 Query 과 생성이 안되는 경우가 있는데 이럴경우application.properties 에 spring.jpa.properties.hibernate.default_batch_fetch_size=5 를 추가해주면 된다 .

@Getter@Setter@Entity@Table(name = "orders")public class OrderDomain { @Id @Column(name = "order_id") private int orderId;

// default fetch type = EAGER @ManyToOne(optional = false) @JoinColumn(name = "customer_id") @BatchSize(size = 5) private CustomersDomain customer;

// default fetch type = LAZY @OneToMany @JoinColumn(name = "order_id") private List<OrderItemDomain> orderItems;}

Hibernate: select orderdomai0_.order_id as order_id1_4_, orderdomai0_.customer_id as customer2_4_ from orders orderdomai0_Hibernate: select customersd0_.customer_id as customer1_1_0_, customersd0_.customer_name as customer2_1_0_ from customers customersd0_ where customersd0_.customer_id in (?, ?, ?, ?, ?)Hibernate: select customersd0_.customer_id as customer1_1_0_, customersd0_.customer_name as customer2_1_0_ from customers customersd0_ where customersd0_.customer_id in (?, ?)

01 JPA N+1

-N+1 해결방안 ?

3. @Fetch(FetchMode.SUBSELECT) 어노테이션 사용 .

이것도 사용법은 간단하다 . subQuery 를 날려서 한번에 조회를 해온다 .

Hibernate: select orderdomai0_.order_id as order_id1_4_, orderdomai0_.customer_id as customer2_4_ from orders orderdomai0_Hibernate: select customersd0_.customer_id as customer1_1_0_, customersd0_.customer_name as customer2_1_0_ from customers customersd0_ where customersd0_.customer_id in (?, ?, ?, ?, ?)Hibernate: select customersd0_.customer_id as customer1_1_0_, customersd0_.customer_name as customer2_1_0_ from customers customersd0_ where customersd0_.customer_id in (?, ?)Hibernate: select orderitems0_.order_id as order_id3_3_1_, orderitems0_.order_line_id as order_li1_3_1_, orderitems0_.order_line_id as order_li1_3_0_, or-deritems0_.item_id as item_id2_3_0_ from order_items orderitems0_ where orderitems0_.order_id in (select orderdomai0_.order_id from orders or-derdomai0_)

@Getter@Setter@Entity@Table(name = "orders")public class OrderDomain { @Id @Column(name = "order_id") private int orderId;

// default fetch type = EAGER @ManyToOne(optional = false) @JoinColumn(name = "customer_id") @BatchSize(size = 5) private CustomersDomain customer;

// default fetch type = LAZY @OneToMany(fetch = FetchType.EAGER) @JoinColumn(name = "order_id") @Fetch(FetchMode.SUBSELECT) private List<OrderItemDomain> orderItems;}

01 JPA N+1

-N+1 해결방안 ?

4. 글로벌 페치 전략 - LAZY 사용@OneToOne, @ManyToOne : EAGER@OneToMany, @ManyToMany : LAZYFetch Join 을 사용하더라도 글로벌 페치 전략이 EAGER 로 설정된 연관관계에 대해서는 즉시 로딩을 위해 추가로 쿼리들이 실행된다 .따라서 , 즉시 로딩이 필요하지 않은 @OneToOne, @ManyToOne 연관관계에 대해서는 글로벌 페치 전략을 LAZY 로 변경해서 불필요한 쿼리 실행 방지Lazy : 지연로딩 , 엔티티를 조회할 때 연관된 엔티티를 실제 사용할 때 조회 . Eager : 즉시로딩 , 엔티티를 조회할 때 연관된 엔티티도 함께 조회 .

결론 .글로벌 페치 전략을 LAZY 로 사용 .( 지연로딩 ) -> 즉시로딩은 성능개선도 어렵고 , 예상치 못한 SQL 이 실행이 될 수도 있음 .그 후에 , 적절한 fetch join 을 통해서 성능을 개선 .따라서 , 모두 지연로딩으로 설정 . 성능 최적화가 꼭 필요한 곳에는 JPQL 페치 조인을 사용 .

해당 프로젝트 githubhttps://github.com/phpbae/JPAStudy1

참조사이트 : http://peyton.tk/index.php/post/975http://meetup.toast.com/posts/87http://www.egovframe.go.kr/wiki/doku.php?id=egovframework:rte:psl:orm:fetch_strategy

Thanks. Have a good day.