N+1 문제에 대해서 생각없이 구현을 진행하다가, 친구와 얘기하다가 내가 구현한 부분에서 N+1문제가 일어날 거 같다는 말을 듣고, N+1 문제에 대해서 다시 공부하고, 정리를 해보려고 한다.
Review라는 엔티티가 있다.
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Review extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
private Shop shop;
@Embedded
private ReviewContent reviewContent;
@Embedded
private List<ReviewImage> reviewImages;
@Embedded
private ReviewScore reviewScore;
@OneToMany(mappedBy = "review")
private List<Empathy> empathies = new ArrayList<>();
}
ReviewImage는 라는 엔티티가 있다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ReviewImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String url;
@ManyToOne(fetch = FetchType.LAZY)
private Review review;
public ReviewImage(String url) {
this.url = url;
}
public void addReview(Review review) {
this.review = review;
}
}
Review와 ReviewImage는 일대다 관계를 가지고 있다.
@Query("select r from Review r where r.reviewContent.content like :keyword or r.reviewContent.tip like :keyword")
List<Review> findAllByReviewContentContaining(@Param("keyword") String keyword);
keyword를 통해서 review를 가져오는 query이다.
그리고 가져온 Review에서 lazyLoading을 통해서, reviewImage를 가져오는 과정이 있다.
이때, Review를 100개 가져왔다면, 100개를 가져오는 위에 나온 query한번만 일어나는 것이 아닌, 위에 일어나는 query가 한번 일어난 후, reviewImage를 가져오는 query가 각 리뷰마다 일어난다. 각 리뷰마다 일어나기 때문에, 한번이 아닌 101번의 query가 일어나는 것이다. 예를 들어, review가 100만개라면, 100만개 리뷰를 가져오는 query한번 각 리뷰가 reviewImage를 가져오는 query 한번씩, 총 100만번이 더 일어날 것이다. 이 문제가 N+1 문제이다. (1+N문제라고 하는 것이 실행 순서에 맞을 거 같긴하다)
위 문제는 프로그램의 성능을 느려지게 하는 원인이 될 것이다.
그럼 N+1 문제를 어떻게 해결할까❓
join fetch를 사용하자.
@Query("select r from Review r join fetch r.reviewImage where r.reviewContent.content like :keyword or r.reviewContent.tip like :keyword")
List<Review> findAllByReviewContentContaining(@Param("keyword") String keyword);
이렇게 사용하면, query를 한번 날릴 때, join을 하기 때문에 문제를 해결할 수 있다!!
@EntityGraph를 사용하자
@EntityGraph(attributePaths = {"reviewImages"})
@Query("select r from Review r where r.reviewContent.content like :keyword or r.reviewContent.tip like :keyword")
List<Review> findAllByReviewContentContaining(@Param("keyword") String keyword);
@EntityGroup을 사용하면, 쿼리 원본에 손상없이, Eager로 쿼리를 가져올 수 있다!!!
문제점
join fetch는 inner join을 사용하고, EntityGroup은 outer join을 사용한다.
두 방법 다 조인 테이블을 그대로 들고오기 때문에, 카테시안 곱이 발생하여, 중복된 값을 가져온다!!
이 문제를 해결하기 위해, distinct를 사용하거나 필드 타입을 List가 아닌, Set으로 설정을 하여, 중복을 제거할 수 있다!!
내가 겪은 문제와 해결 방법,,,,
만약 Entity내에서 일급컬렉션과 임베디드 타입을 사용을 해서, OneToMany 관계를 관리하고 있다면, JPQL로 join fetch를 사용시에, 임베디드 타입을 조인을 하면 조인이 안된다. 이때는 임베디드 타입안에 있는 List를 조인을 해야지 fetch join이 된다. 이때, distinct를 사용하지 않으면 중복되서 나오기 때문에, distinct를 사용해야된다!!
Reference
JPA N+1 문제 및 해결방안
안녕하세요? 이번 시간엔 JPA의 N+1 문제에 대해 이야기 해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용을 정리하는 Github와 세미
jojoldu.tistory.com
'✍🏻study > 📒 jpa' 카테고리의 다른 글
영속성 관리 (0) | 2022.04.15 |
---|---|
연관 관계 편의 메소드❓ (0) | 2022.03.24 |