SpringBoot

[SpringBoot] JPA N+1 문제를 @EntityGraph로 해결하기

진세박 2023. 9. 16. 03:39

들어가기 전에

JPA N+1 문제를 해결하기 위해 고민하던 도중 @EntityGraph로 해결하니 너무 편해서 글을 쓰게 되었습니다.

물론 query가 복잡해지는 경우 이 어노테이션으로도 해결할 수 없는 부분이 있겠지만 간단한 query일 경우

이 @EntityGraph로 한번 해결 해 봤으면 하는 마음에 공유합니다.


@EntityGraph란?

@EntityGraph 어노테이션은 JPA에서 제공하는 기능을 활용하여 엔티티 그래프를 정의하는 데 사용됩니다.

JPA는 연관된 엔티티를 로드할 때 Lazy Loading으로 설정해 놓았으면 연관된 엔티티가 실제로 필요한 시점에만 데이터베이스에서 가져옵니다. 이는 N+1 query 문제와 같은 성능 문제를 야기합니다.

@EntityGraph 어노테이션을 사용하면 지연 로딩 대신에 Eager Loading을 수행하거나, 특정 연관 관계만 선택적으로 로딩할 수 있습니다. 이렇게 함으로써 관련된 모든 데이터를 한 번의 쿼리로 가져올 수 있으며, 성능을 향상할 수 있습니다.

 

사용 방법

@NoArgsConstructor(access= AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Entity
@Getter
public class Reservation {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "experience_gift_id")
    private ExperienceGift experienceGift;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "sender_id")
    private User sender;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "receiver_id")
    private User receiver;

    @OneToMany(mappedBy = "reservation")
    List<MemoryPhoto> memoryPhotos = new ArrayList<>();

}

이런 Reservation 클래스가 있고 여러 엔티티와 연관 관계가 있는 상황입니다.

그리고 Reservation 안에 experienceGift 또한 연관 관계가 있는 상황일 때입니다.

@NoArgsConstructor(access= AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Getter
@Entity
public class ExperienceGift {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "subtitle_id")
    private Subtitle subtitle; //fk

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "exp_category_id")
    private ExpCategory expCategory;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "stt_category_id")
    private SttCategory sttCategory;

}

이때 @EntityGraph를 붙이지 않고 여러 연관 관계가 얽혀있는 Reservation을 호출 해 보겠습니다.

Hibernate: 
    select
        r1_0.id,
        r1_0.experience_gift_id,
        r1_0.receiver_id,
        r1_0.sender_id,
    from
        reservation r1_0 
    where
        (
            r1_0.status = 'ACTIVE'
        ) 
        and r1_0.sender_id=? 
        and r1_0.reservation_status in (?,?)
        
Hibernate: 
    select
        e1_0.experience_gift_id,
        e1_0.exp_category_id,
        e1_0.stt_category_id,
        e1_0.subtitle_id,
    from
        experience_gift e1_0 
    where
        e1_0.experience_gift_id=?
        
Hibernate: 
    select
        s1_0.subtitle_id,
    from
        subtitle s1_0 
    where
        s1_0.subtitle_id=?
        
Hibernate: 
    select
        u1_0.id,
        u1_0.name,
    from
        user u1_0 
    where
        u1_0.id=? 
        and (
            u1_0.status = 'ACTIVE'
        )

이렇게 총 4개의 쿼리가 나가는 것을 확인할 수 있습니다.

이는 여러 데이터를 조회하게 되면 query가 예측할 수 없게 많이 나가 성능 문제를 야기할 수 있습니다.

 

이를 해결하기 위해 @EntityGraph를 아래와 같이 붙여보겠습니다.

attriputhPaths는 Reservation을 조회할 때 같이 로딩 될 객체를 적어주면 됩니다.

저는 experienceGift를 조회하면서 experienceGift 안에 있는 subtitle, expCategory, sttCatecory 한 번에 불러오도록 했습니다.

public interface ReservationRepository extends JpaRepository<Reservation, Long> {

    @EntityGraph(attributePaths = {"experienceGift", "sender", "receiver", "experienceGift.subtitle",
            "experienceGift.expCategory", "experienceGift.sttCategory"})
    List<Reservation> findReservationByPhoneNumberAndReservationStatusIn(String phoneNUmber, List<ReservationStatus> reservationStatusList);

}

 

아래는 @EntityGraph를 붙이고 Reservation을 호출했을 때 나가는 query입니다.

Hibernate: 
    select
        r1_0.id,
        e1_0.experience_gift_id,
        e2_0.exp_category_id,
        e2_0.exp_category,
        s1_0.stt_category_id,
        s1_0.stt_category,
        s2_0.subtitle_id,
        r2_0.id,
        r2_0.name,
        s3_0.id,
        s3_0.age,
        s3_0.name,
    from
        reservation r1_0 
    left join
        experience_gift e1_0 
            on e1_0.experience_gift_id=r1_0.experience_gift_id 
    left join
        exp_category e2_0 
            on e2_0.exp_category_id=e1_0.exp_category_id 
    left join
        stt_category s1_0 
            on s1_0.stt_category_id=e1_0.stt_category_id 
    left join
        subtitle s2_0 
            on s2_0.subtitle_id=e1_0.subtitle_id 
    left join
        user r2_0 
            on r2_0.id=r1_0.receiver_id 
            and (
                r2_0.status = 'ACTIVE'
            ) 
    left join
        user s3_0 
            on s3_0.id=r1_0.sender_id 
            and (
                s3_0.status = 'ACTIVE'
            ) 
    where
        (
            r1_0.status = 'ACTIVE'
        ) 
        and r1_0.sender_id=? 
        and r1_0.reservation_status in (?,?)

이처럼 query가 단 1번 나가는 것을 확인할 수 있습니다.