한국의 메타몽 2021. 10. 2. 20:43

 

JPA의 N+1 이슈란?

 

2개의 review가 저장된 DB를 findAll 해보자.

 

아래 코드를 보면 총 3번의 Select가 이루어지고있다.

DB에 저장된 리뷰가 총 2개이고, 추가로 최초 조회의 1번이 더해져 3번이 이루어졌다.

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_i5_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.created_at as created_2_4_1_,
        comments0_.updated_at as updated_3_4_1_,
        comments0_.comment as comment4_4_1_,
        comments0_.review_id as review_i5_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?
Hibernate: 
    select
        comments0_.review_id as review_i5_4_0_,
        comments0_.id as id1_4_0_,
        comments0_.id as id1_4_1_,
        comments0_.created_at as created_2_4_1_,
        comments0_.updated_at as updated_3_4_1_,
        comments0_.comment as comment4_4_1_,
        comments0_.review_id as review_i5_4_1_ 
    from
        comment comments0_ 
    where
        comments0_.review_id=?

 

연관관계에서 발생하는 이슈로, 연관관계가 설정된 엔티티들을 조회할 경우

첫조회(1) + 조회한 데이터의 갯수(N)만큼 연과노간계의 조회쿼리가 추가로 발생하는 경우를 의미한다.

 

지금이야 데이터가 2개밖에 없어서 3개의 조회만 이루어졌지만, 만약 DB에 데이터가 10만개일경우 한번의 서비스 로직에서 DB조회만 10만번이 이루어지는 상황이 발생한다.

 

때문에 연관관계가 맺어진 Entity만 한 번에 조회하는 방법이 필요한데, 대표적으로 2가지 방법이 있다.

 

 

첫번째 : fetch join

 

[예시]

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

 

조회시 바로 가져오고 싶은 Entity 필드를 지정하는 것이다.

이렇게하면 깔끔하게 아래와 같이 단 한번의 조회가 이루어진다.

Hibernate: 
    select
        distinct 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_ 
    inner join
        comment comments1_ 
            on review0_.id=comments1_.review_id

다만 이럴경우 어떤 필드는 Eager 조회, 어떤 필드는 Lazy 조회를 해야하므로, 쿼리에서 불필요한 문장들이 추가될 수 있다.

이런 점이 번거롭다면 아래에 언급되는 Entity Graph를 사용하면 된다.

 

+  추가 : Eager와 Lazy의 타입의 차이는 다음과 같다.

    @ManyToOne(fetch = FetchType.LAZY) // 필요할때만 getter로 접근해 Entity를 가져온다
    @ToString.Exclude
    private User user;

    @OneToMany(fetch = FetchType.EAGER) // 연관관계에 있는 모든 Entity를 가져온다
    @JoinColumn(name = "review_id")
    private List<Comment> comments;

 

두번째 : @EntityGraph

 

[예시] 

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

@EntityGraph의 attributePathsdp 쿼리 수행시 반드시 가져올 필명을 지정하면 Lazy가 아닌 Eager 조회로 가져오게 된다.

쿼리문을 보면 불필요한 문장이 추가되지 않도, 원본 쿼리 손상도 없이 데이터를 조회하고 있다.

이렇게 조회하면 위의 fetch join과 동일한 결과가 출력된다.