Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

개발자되기 프로젝트

JPA Fetch type, N + 1 이슈 본문

JPA/Trouble shooting

JPA Fetch type, N + 1 이슈

Seung__ 2021. 7. 11. 16:10

1. Fetch Type이란?


 

Entity 를 조회할 경우 해당 Entity와 연관관계에 있는 Entity를 어떻게 가져올 것인지 설정하는 값이다.

 

 - 연관 관계에 있는 Entity 를 모두 가져온다 → Eager

 - 연관 관계에 있는 Entity 를 getter 로 접근할 때 가져온다 → Lazy 

 

 

2. Lazy fetch


Lazy Fetch는 연관관계에 있는 Entity를 바로 가져오는 것이 아니라 연관관계의 entity를 getter로 호출할 때만 가져온다.

 

User class를 보자. user와 userHIstory는 1:N관계이다.

 

User를 호출할 때 마다 UserHistory도 항상 불러와야 할까?? 항상 필요하지도 않은데..?

 

UserHistory가 필요한 시점에만 쿼리를 실행하면 되지 않을까?

 

그렇다면 언제 UserHistory가 필요하다고 판단할까?

 

getter를 통해 UserHistory를 불러오는 경우에 JPA에서 UserHistory가 필요하다고 판단이 가능.

ex> user.getUserHistoris()

 

 

하지만 lazy fetch는 항상 가능한 것이 아니라,

session(영속성 컨텍스트가 entity관리하는 상태, @Transactional)이 열려있을 경우만 가능하다.

 

 

2-1.  주의 사항


 

예를들어, Review 객체에 id, title, scroe, content, book, user 등의 filed가 있다고 하자.

 

review 객체에 대해 ToString을 실행하면 각 filed를 모두 조회한다.

 

이 때 연관관계에 있는 entity까지 모두 조회하는 query가 실행된다.

 

이처럼 Lazy fetch를 적용하더라도, 불필요한 query가 실행될 수 있다.

 

이러한 상황을 막기위해 ToString시 필요가 없는 entity에 대해서는 @ToString.Exclude를 붙여주자.

 

Lazy fetch의 장점을 최대한 활용하기 위함이다.

    @ManyToOne
    @ToString.Exclude
    private Book book;

    @ManyToOne
    @ToString.Exclude
    private User user;

 

추가로 @ToString.Exclude는 연관관계를 가진 entity들에 대해 ToString시 순환참조를 막기위한 방법으로 사용된다.

 

 

 

3. Eager fetch


Eager fetch는 연관관계의 entity를 바로바로 호출하는 것이다.

 

예를들어 User와 UserHistory는 1:N 연관관계이다.

Lazy type은 userHistory를 user에서 getter를 통해 호출될 때 만 userHistory 를 호출하지만.

 

Eager type은 항상 user호출 시 userHistory도 같이 호출된다.

 

 

 

4. N + 1 Issue


A와 B가 1 : N 관계라고 하자.

 

1개의 A entity를 조회할 경우 A에 연관된 n개의 B entity가 독릭접인 쿼리를 통해 조회되는 현상이다.

 

즉, 예상하기로는 A를 조회하는 1개의 쿼리로 B까지 오겠지...??

 

하지만 현실을 n개의 B entity를 별도로 조회하는 쿼리가 돌아간다..

 

 

5. N+1 issue 만들기..?


강제로  N+1 issue 상황을 만들어보자.

 

<data.sql>

insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values (1, 'JPA', '오우야', 5.0, 1, 1);
insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values (2, '백엔드', 'ㄷㄷㄷ', 1.0, 2, 2);

 

<Comment class> : comment ~ review 연관관계

@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String comment;

    @ManyToOne
    private Review review;


}

 

<Review class>   : comment ~ review 연관관계

  @OneToMany
  @JoinColumn(name = "review_id")
  private List<Comment> comments;

 

 

1:N 연관관계는 아래 글 참고

 

1 : N 연관관계 - 1

이전에 만들어둔 User entity와 User History는 1:N연관관계를 갖는다. User는 현재 회원정보를 가지고 있는 테이블이고 UserHistory는 특정 user id에 저장된 값이 변경된 내역을 저장하는 table이다. 1. test @..

bsh-developer.tistory.com

 

<Test>

  @Test
    void reviewTest(){

        List<Review> reviews = reviewRepository.findAll();

        System.out.println(reviews);
    }

에러가 발생한다.

could not initialize proxy - no Session

 no Session이기 때문에 Test code에 @Transactional을 붙여주자.

@Test
@Transactional
void reviewTest(){

  List<Review> reviews = reviewRepository.findAll();

  System.out.println(reviews);
}

 

참고로 연관관계에 따라  기본 fetch 설정이 다르다.

@ManyToOne EAGER
@OneToMany LAZY
@OneToOne EAGER

 

따라서 Review class에서 @ManyToOne에 fetch type을 lazy로 변경해주자.

 

추가로 @OneToMany인 comments에 대해서는 EAGER로 변경.

@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;

@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;

@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
private List<Comment> comments;

이제 review를 조회할 때 Book과 User를 호출하는 query가 동작하지 않을 것이다.

 

Review를 조회하고 review id에 해당하는 comments를 조회했다. 

 

짠! Review만 쿼리돌아가겠지 했는데, n개의 comment 쿼리까지 돌아갔따. N+1 상황이다.

Hibernate: 
    select
        review0_.id as id1_6_,
        review0_.created_at as created_2_6_,
        review0_.updated_at as updated_3_6_,
        review0_.book_id as book_id7_6_,
        review0_.content as content4_6_,
        review0_.score as score5_6_,
        review0_.title as title6_6_,
        review0_.user_id as user_id8_6_ 
    from
        review review0_
        
Hibernate: 
    select
        comments0_.review_id as review_i3_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.comment as comment2_4_1_,
        comments0_.review_id as review_i3_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
        
Hibernate: 
    select
        comments0_.review_id as review_i3_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.comment as comment2_4_1_,
        comments0_.review_id as review_i3_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
        
[Review(super=BaseEntity(createdAt=2021-07-11T14:27:48.128183, updatedAt=2021-07-11T14:27:48.128183), id=1, title=JPA, content=오우야, score=5.0, comments=[]), Review(super=BaseEntity(createdAt=2021-07-11T14:27:48.130341, updatedAt=2021-07-11T14:27:48.130341), id=2, title=백엔드, content=ㄷㄷㄷ, score=1.0, comments=[])]

 

N+1 issue 준비가 끝났다.

 

 

그리고 EAGER와 LAZY는 쿼리 시점의 차이이지 N+1 쿼리 횟수에 대한 영향을 끼치지 않음.

 

 

 

6. EAGER, LAZY 차이점


lazy type은   필요한 시점에 쿼리가 실행한다.

EAGER 대비 쿼리 성능에 관련된 이점을 얻을 수 있다.

Fetch Type은 쿼리 시점에 대한 차이이지 N+1에대한 해법은 아니다.

 

 

<Review class> fetch type 확인!

@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;

@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;

@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
private List<Comment> comments;

<Test>

@Test
@Transactional
void reviewTest(){

    List<Review> reviews = reviewRepository.findAll();

    //System.out.println(reviews);

    System.out.println("전체를 가져왔다");

    System.out.println(reviews.get(0).getComments());

    System.out.println("첫 번째 리뷰의 코멘트들을 가져옴");

    System.out.println(reviews.get(1).getComments());

    System.out.println("두 번째 리뷰의 코멘트들을 가져옴");
}

 

<결과>

Hibernate: 
    select
        review0_.id as id1_6_,
        review0_.created_at as created_2_6_,
        review0_.updated_at as updated_3_6_,
        review0_.book_id as book_id7_6_,
        review0_.content as content4_6_,
        review0_.score as score5_6_,
        review0_.title as title6_6_,
        review0_.user_id as user_id8_6_ 
    from
        review review0_
Hibernate: 
    select
        comments0_.review_id as review_i3_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.comment as comment2_4_1_,
        comments0_.review_id as review_i3_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
Hibernate: 
    select
        comments0_.review_id as review_i3_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.comment as comment2_4_1_,
        comments0_.review_id as review_i3_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
전체를 가져왔다
[]
첫 번째 리뷰의 코멘트들을 가져옴
[]
두 번째 리뷰의 코멘트들을 가져옴

먼저 review 조회하는 query가 실행되었따.

 

그리고 comments를 가져오는 쿼리는 별도로 실행되지 않고 캐시에서 불러온 것을 알 수 있다.

(왜 비었지..??)

 

<Review class> : comment의 fetch타입을 lazy로 변경

@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;

@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;

@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
private List<Comment> comments;

<결과> getComments()시점에 comment 조회하는 query가 실행되었다.

Hibernate: 
    select
        review0_.id as id1_6_,
        review0_.created_at as created_2_6_,
        review0_.updated_at as updated_3_6_,
        review0_.book_id as book_id7_6_,
        review0_.content as content4_6_,
        review0_.score as score5_6_,
        review0_.title as title6_6_,
        review0_.user_id as user_id8_6_ 
    from
        review review0_
전체를 가져왔다
Hibernate: 
    select
        comments0_.review_id as review_i3_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.comment as comment2_4_1_,
        comments0_.review_id as review_i3_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
[]
첫 번째 리뷰의 코멘트들을 가져옴
Hibernate: 
    select
        comments0_.review_id as review_i3_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.comment as comment2_4_1_,
        comments0_.review_id as review_i3_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
[]
두 번째 리뷰의 코멘트들을 가져옴

 

즉, lazy type은  필요한 시점에 쿼리가 실행한다.

EAGER 대비 쿼리 성능에 관련된 이점을 얻을 수 있다.

 

 

7. N+1 Issue 해결


1) @Query를 통해 fetch join 쿼리를 custom으로 만듦.

 

<BookRepository>

public interface ReviewRepository extends JpaRepository<Review, Long> {

    @Query("select distinct r from Review r join fetch r.comments")
    List<Review> findAllByFetchJoin();

}

inner join이 들어가서 comments까지 같이 조회가 가능하다.

 

즉 한 번의 쿼리로 끝!

Hibernate: 
    select
        review0_.id as id1_6_0_,
        comments1_.id as id1_4_1_,
        review0_.created_at as created_2_6_0_,
        review0_.updated_at as updated_3_6_0_,
        review0_.book_id as book_id7_6_0_,
        review0_.content as content4_6_0_,
        review0_.score as score5_6_0_,
        review0_.title as title6_6_0_,
        review0_.user_id as user_id8_6_0_,
        comments1_.created_at as created_2_4_1_,
        comments1_.updated_at as updated_3_4_1_,
        comments1_.comment as comment4_4_1_,
        comments1_.review_id as review_i5_4_1_,
        comments1_.review_id as review_i5_4_0__,
        comments1_.id as id1_4_0__ 
    from
        review review0_ 
    left outer join
        comment comments1_ 
            on review0_.id=comments1_.review_id
            
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.833729, updatedAt=2021-07-11T16:06:12.833729), id=1, title=JPA, content=오우야, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.836600, updatedAt=2021-07-11T16:06:12.836600), id=1, comment=킹치만..), Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.839291, updatedAt=2021-07-11T16:06:12.839291), id=2, comment=오히려좋아)])
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.835227, updatedAt=2021-07-11T16:06:12.835227), id=2, title=백엔드, content=ㄷㄷㄷ, score=1.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.840736, updatedAt=2021-07-11T16:06:12.840736), id=3, comment=오히려안좋아)])

 

 

2) @EntityGraph

 @EntityGraph(attributePaths = "comments")
    @Query("select r from Review r")
    List<Review> findAllByEntityGraph();

 

Hibernate: 
    select
        review0_.id as id1_6_0_,
        comments1_.id as id1_4_1_,
        review0_.created_at as created_2_6_0_,
        review0_.updated_at as updated_3_6_0_,
        review0_.book_id as book_id7_6_0_,
        review0_.content as content4_6_0_,
        review0_.score as score5_6_0_,
        review0_.title as title6_6_0_,
        review0_.user_id as user_id8_6_0_,
        comments1_.created_at as created_2_4_1_,
        comments1_.updated_at as updated_3_4_1_,
        comments1_.comment as comment4_4_1_,
        comments1_.review_id as review_i5_4_1_,
        comments1_.review_id as review_i5_4_0__,
        comments1_.id as id1_4_0__ 
    from
        review review0_ 
    left outer join
        comment comments1_ 
            on review0_.id=comments1_.review_id
            
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.833729, updatedAt=2021-07-11T16:06:12.833729), id=1, title=JPA, content=오우야, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.836600, updatedAt=2021-07-11T16:06:12.836600), id=1, comment=킹치만..), Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.839291, updatedAt=2021-07-11T16:06:12.839291), id=2, comment=오히려좋아)])
Review(super=BaseEntity(createdAt=2021-07-11T16:06:12.835227, updatedAt=2021-07-11T16:06:12.835227), id=2, title=백엔드, content=ㄷㄷㄷ, score=1.0, comments=[Comment(super=BaseEntity(createdAt=2021-07-11T16:06:12.840736, updatedAt=2021-07-11T16:06:12.840736), id=3, comment=오히려안좋아)])

 

8. GitHub : 210711 fetch type, N+1


 

bsh6463/BookManager

Contribute to bsh6463/BookManager development by creating an account on GitHub.

github.com

 

 

'JPA > Trouble shooting' 카테고리의 다른 글

Dirtycheck, 성능이슈  (0) 2021.07.12
영속성 컨텍스트로 인해 발생하는 issue  (0) 2021.07.11
Comments