들어가기 전에
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번 나가는 것을 확인할 수 있습니다.
'SpringBoot' 카테고리의 다른 글
[SpringBoot] 3.x OAuth2 invalid_token_response 문제 (0) | 2023.01.13 |
---|