User
엔티티가 10개 있고, 각 User
에 연관된 Post
엔티티를 조회할 경우, 초기 User
조회를 위한 1회 쿼리에 더해 각 User
의 Post
를 조회하는 10회의 추가 쿼리가 발생한다.User
조회 결과가 10만 개라면, 상황은 매우 심각해진다.@Entity
@Table(name = "users")
public class UserJpaEntity {
@Id
@GeneratedValue
private Long id;
private String email;
private String password;
private String nickname;
private String name;
@Builder.Default
@OneToMany(mappedBy = "userJpaEntity", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<PostJpaEntity> postJpaEntities = new ArrayList<>();
public void addPost(PostJpaEntity postJpaEntity) {
postJpaEntities.add(postJpaEntity);
}
}
@Entity
@Table(name = "posts")
public class PostJpaEntity {
@Id
@GeneratedValue
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private UserJpaEntity userJpaEntity;
public void addUser(UserJpaEntity userJpaEntity) {
this.userJpaEntity = userJpaEntity;
}
}
@Transactional
@SpringBootTest
@DisplayName("1:N 관계에서 N에서 LAZY Loading 을 사용할 때 발생하는 N+1 문제를 확인합니다.")
public class JPA_OneToMany_Lazy_Loading_Test {
@BeforeEach
public void setup() {
for (int i = 1; i <= 10; i++) {
// UserJpaEntity 객체 생성
UserJpaEntity userJpaEntity = UserJpaEntity.builder()
.email("user" + i + "@example.com")
.password("password")
.nickname("nickname" + i)
.name("User " + i)
.build();
// PostJpaEntity 객체 생성
PostJpaEntity postJpaEntity1 = PostJpaEntity.builder()
.title("Title " + i)
.content("Content " + i)
.build();
PostJpaEntity postJpaEntity2 = PostJpaEntity.builder()
.title("Title " + i)
.content("Content " + i)
.build();
// UserJpaEntity에 PostJpaEntity를 추가
userJpaEntity.addPost(postJpaEntity1);
userJpaEntity.addPost(postJpaEntity2);
// PostJpaEntity에 UserJpaEntity를 추가
postJpaEntity1.addUser(userJpaEntity);
postJpaEntity2.addUser(userJpaEntity);
// UserJpaEntity를 저장 (CascadeType.ALL에 의해 PostJpaEntity도 저장됩니다)
userJpaRepo.save(userJpaEntity);
}
}
}
@Test
@DisplayName("Lazy Loading을 사용할 때 발생하는 N+1 문제를 확인합니다.")
public void test1() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
System.out.println("------------ USER 전체 조회 요청 ------------");
List<UserJpaEntity> userJpaEntities = userJpaRepo.findAll();
System.out.println("------------ USER 전체 조회 완료. [1번의 쿼리 발생]------------\n");
System.out.println("------------ USER와 연관관계인 POST 내용 조회 요청 ------------");
userJpaEntities.forEach(userJpaEntity -> System.out.println("USER IN POST ID: " + userJpaEntity.getPostJpaEntities().get(0).getId()));
System.out.println("------------ USER와 연관관계인 POST 내용 조회 완료 [조회된 USER 개수(N=10) 만큼 추가적인 쿼리 발생] ------------\n");
}
------------ 영속성 컨텍스트 비우기 -----------
------------ USER 전체 조회 요청 ------------
Hibernate: select userjpaent0_.id as id1_6_, userjpaent0_.email as email2_6_, userjpaent0_.name as name3_6_, userjpaent0_.nickname as nickname4_6_, userjpaent0_.password as password5_6_ from users userjpaent0_
------------ USER 전체 조회 완료. [1번의 쿼리 발생]------------
------------ USER와 연관관계인 POST 내용 조회 요청 ------------
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
Hibernate: select postjpaent0_.user_id as user_id4_3_0_, postjpaent0_.id as id1_3_0_, postjpaent0_.id as id1_3_1_, postjpaent0_.content as content2_3_1_, postjpaent0_.title as title3_3_1_, postjpaent0_.user_id as user_id4_3_1_ from posts postjpaent0_ where postjpaent0_.user_id=?
------------ USER와 연관관계인 POST 내용 조회 완료 [조회된 USER 개수(N=10) 만큼 추가적인 쿼리 발생] ------------
public interface UserJpaRepo extends JpaRepository<UserJpaEntity, Long> {
@Query("select u from UserJpaEntity u join fetch u.postJpaEntities")
List<UserJpaEntity> findAllByFetchJoin();
}
Fetch Join으로 가져오기 위해, 네이티브 쿼리에 형태로 가져온다.
@Test
@DisplayName("Fetch Join을 사용하여 N+1 문제를 해결합니다.")
public void test2() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
System.out.println("------------ USER 전체 조회 요청 ------------");
List<UserJpaEntity> userJpaEntities = userJpaRepo.findAllByFetchJoin();
System.out.println("------------ USER 전체 조회 완료. [1번의 쿼리 발생]------------\n");
System.out.println("------------ USER와 연관관계인 POST 내용 조회 요청 ------------");
userJpaEntities.forEach(userJpaEntity -> System.out.println("USER IN POST ID: " + userJpaEntity.getPostJpaEntities().get(0).getId()));
System.out.println("------------ USER와 연관관계인 POST 내용 조회 완료 [Fetch Join을 사용하여 추가적인 쿼리 발생하지 않음] ------------\n");
System.out.println("------------ USER SIZE 확인 ------------");
System.out.println("userJpaEntities.size() : " + userJpaEntities.size());
System.out.println("------------ USER SIZE 확인 완료 [20개가 조회!?, Fetch Join(Inner Join)은 카테시안 곱이 발생하여 POST의 수만큼 USER가 중복이 발생]------------\n");
}
나오는 출력물은 아래와 같다. Sout을 제외하고 Hibernate만 뽑아서 보여주겠다.
------------ USER 전체 조회 요청 ------------
Hibernate: select userjpaent0_.id as id1_6_0_, postjpaent1_.id as id1_3_1_, userjpaent0_.email as email2_6_0_, userjpaent0_.name as name3_6_0_, userjpaent0_.nickname as nickname4_6_0_, userjpaent0_.password as password5_6_0_, postjpaent1_.content as content2_3_1_, postjpaent1_.title as title3_3_1_, postjpaent1_.user_id as user_id4_3_1_, postjpaent1_.user_id as user_id4_3_0__, postjpaent1_.id as id1_3_0__ from users userjpaent0_ inner join posts postjpaent1_ on userjpaent0_.id=postjpaent1_.user_id
------------ USER 전체 조회 완료. [1번의 쿼리 발생]------------
------------ USER와 연관관계인 POST 내용 조회 요청 ------------
------------ USER와 연관관계인 POST 내용 조회 완료 [Fetch Join을 사용하여 추가적인 쿼리 발생하지 않음] ------------
------------ USER SIZE 확인 ------------
userJpaEntities.size() : 20
------------ USER SIZE 확인 완료 [20개가 조회!?, Fetch Join(Inner Join)은 카테시안 곱이 발생하여 POST의 수만큼 USER가 중복이 발생]------------
총 쿼리는 1(전체 조회)번 진행이 된다.
Fetch Join을 통해 User, Post를 한 쿼리에 다 가져온다.
하지만 Fetch Join에도 단점이 존재하는데, Fetch Join은 카테시안 곱이 적용되어 Subject(Post)의 수 만큼 User가 중복 조회된다.
이 방법을 해결하기 위해 @Query("select u from UserJpaEntity u join fetch u.postJpaEntities")
를 → @Query("select DISTINCT u from UserJpaEntity u join fetch u.postJpaEntities")
로 수정하여 중복을 제거하고 뽑아낼 수 있다.
public interface UserJpaRepo extends JpaRepository<UserJpaEntity, Long> {
@EntityGraph(attributePaths = {"postJpaEntities"})
@Query("select u from UserJpaEntity u")
List<UserJpaEntity> findAllByEntityGraph();
}
EntityGraph는 Entity가 어떻게 로드 될지 제어하는데 사용된다.
attributePaths
속성에 postJpaEntities
를 지정하여, UserJpaEntity
를 조회할 때 연관된 postJpaEntities
컬렉션도 함께 즉시 로드(Eager Loading)하도록 지시합니다.
@Test
@DisplayName("EntityGraph를 사용하여 N+1 문제를 해결합니다.")
public void test3() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
System.out.println("------------ USER 전체 조회 요청 ------------");
List<UserJpaEntity> userJpaEntities = userJpaRepo.findAllByEntityGraph();
System.out.println("------------ USER 전체 조회 완료. [1번의 쿼리 발생]------------\n");
System.out.println("------------ USER와 연관관계인 POST 내용 조회 요청 ------------");
userJpaEntities.forEach(userJpaEntity -> System.out.println("USER IN POST ID: " + userJpaEntity.getPostJpaEntities().get(0).getId()));
System.out.println("------------ USER와 연관관계인 POST 내용 조회 완료 [Fetch Join을 사용하여 추가적인 쿼리 발생하지 않음] ------------\n");
System.out.println("------------ USER SIZE 확인 ------------");
System.out.println("userJpaEntities.size() : " + userJpaEntities.size());
System.out.println("------------ USER SIZE 확인 완료 [20개가 조회!?, EntityGraph(Outer Join)도 카테시안 곱이 발생하여 POST의 수만큼 USER가 중복이 발생]------------\n");
}
나오는 출력물은 아래와 같다. Sout을 제외하고 Hibernate만 뽑아서 보여주겠다.
------------ USER 전체 조회 요청 ------------
Hibernate: select userjpaent0_.id as id1_6_0_, postjpaent1_.id as id1_3_1_, userjpaent0_.email as email2_6_0_, userjpaent0_.name as name3_6_0_, userjpaent0_.nickname as nickname4_6_0_, userjpaent0_.password as password5_6_0_, postjpaent1_.content as content2_3_1_, postjpaent1_.title as title3_3_1_, postjpaent1_.user_id as user_id4_3_1_, postjpaent1_.user_id as user_id4_3_0__, postjpaent1_.id as id1_3_0__ from users userjpaent0_ left outer join posts postjpaent1_ on userjpaent0_.id=postjpaent1_.user_id
------------ USER 전체 조회 완료 [1번의 쿼리 발생]------------
------------ USER와 연관관계인 POST 내용 조회 요청 ------------
------------ USER와 연관관계인 POST 내용 조회 완료 [EntityGraph 을 사용하여 추가적인 쿼리 발생하지 않음] ------------
------------ USER SIZE 확인 ------------
userJpaEntities.size() : 10
------------ USER SIZE 확인 완료 [10개가 조회]------------
위 처럼 쿼리는 1번(전체 조회) 조회가 된다.
EntityGraph도 Fetch Join과 마찬가지로 Eager Join으로 모든 데이터를 항상 가져오므로 성능에 부정적인 영향을 끼칠수도 있다.
마찬가지로 로드된 엔티티들은 모두 영속성 컨텍스트에서 보관되므로, 큰 데이터 세트의 경우 메모리 사용량이 크게 증가한다.
@Builder.Default
@BatchSize(size = 5)
@OneToMany(mappedBy = "userJpaEntity", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<PostJpaEntity> postJpaEntities = new ArrayList<>();
@Test
@DisplayName("BatchSize를 사용하여 N+1 문제를 해결합니다.")
public void test4() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
System.out.println("------------ USER 전체 조회 요청 ------------");
List<UserJpaEntity> userJpaEntities = userJpaRepo.findAll();
System.out.println("------------ USER 전체 조회 완료. [1번의 쿼리 발생]------------\n");
System.out.println("------------ USER와 연관관계인 POST 내용 조회 요청 ------------");
userJpaEntities.forEach(userJpaEntity -> System.out.println("USER IN POST ID: " + userJpaEntity.getPostJpaEntities().get(0).getId()));
System.out.println("------------ USER와 연관관계인 POST 내용 조회 완료 ------------\n");
}
------------ 영속성 컨텍스트 비우기 -----------
------------ USER 전체 조회 요청 ------------
Hibernate: select userjpaent0_.id as id1_6_, userjpaent0_.email as email2_6_, userjpaent0_.name as name3_6_, userjpaent0_.nickname as nickname4_6_, userjpaent0_.password as password5_6_ from users userjpaent0_
------------ USER 전체 조회 완료. [1번의 쿼리 발생] ------------
------------ USER와 연관관계인 POST 내용 조회 요청 ------------
Hibernate: select postjpaent0_.user_id as user_id4_3_1_, postjpaent0_.id as id1_3_1_, postjpaent0_.id as id1_3_0_, postjpaent0_.content as content2_3_0_, postjpaent0_.title as title3_3_0_, postjpaent0_.user_id as user_id4_3_0_ from posts postjpaent0_ where postjpaent0_.user_id in (?, ?, ?, ?, ?)
USER IN POST ID: 2
USER IN POST ID: 5
USER IN POST ID: 8
USER IN POST ID: 11
USER IN POST ID: 14
Hibernate: select postjpaent0_.user_id as user_id4_3_1_, postjpaent0_.id as id1_3_1_, postjpaent0_.id as id1_3_0_, postjpaent0_.content as content2_3_0_, postjpaent0_.title as title3_3_0_, postjpaent0_.user_id as user_id4_3_0_ from posts postjpaent0_ where postjpaent0_.user_id in (?, ?, ?, ?, ?)
USER IN POST ID: 17
USER IN POST ID: 20
USER IN POST ID: 23
USER IN POST ID: 26
USER IN POST ID: 29
------------ USER와 연관관계인 POST 내용 조회 완료 ------------
@BatchSize
를 사용함으로써, 연관 엔티티를 로드하는 데 필요한 데이터베이스 쿼리 수를 크게 줄일 수 있다. 이는 특히 많은 수의 연관 엔티티가 있는 경우에 성능을 크게 개선할 수 있다.@BatchSize
는 개별 엔티티 또는 컬렉션 필드에 적용할 수 있어, 어플리케이션의 특정 부분에 대해 세밀한 성능 튜닝을 할 수 있다.size
값은 한 번에 더 많은 데이터를 로드하지만, 메모리 사용량과 처리 시간이 증가할 수 있으므로, 적절한 배치 크기를 선택하는 것이 중요하다.Fetch Join
과 EntityGraph
모두 내부적으로 SQL 조인을 사용한다.사용 방식
복잡한 쿼리:
Hibernate
// **Fetch Join**
Hibernate:
select
userjpaent0_.id as id1_6_0_,
postjpaent1_.id as id1_3_1_,
userjpaent0_.email as email2_6_0_,
userjpaent0_.name as name3_6_0_,
userjpaent0_.nickname as nickname4_6_0_,
userjpaent0_.password as password5_6_0_,
postjpaent1_.content as content2_3_1_,
postjpaent1_.title as title3_3_1_,
postjpaent1_.user_id as user_id4_3_1_,
postjpaent1_.user_id as user_id4_3_0__,
postjpaent1_.id as id1_3_0__
from
users userjpaent0_
inner join
posts postjpaent1_
on userjpaent0_.id=postjpaent1_.user_id
// **EntityGraph**
Hibernate:
select
userjpaent0_.id as id1_6_0_,
postjpaent1_.id as id1_3_1_,
userjpaent0_.email as email2_6_0_,
userjpaent0_.name as name3_6_0_,
userjpaent0_.nickname as nickname4_6_0_,
userjpaent0_.password as password5_6_0_,
postjpaent1_.content as content2_3_1_,
postjpaent1_.title as title3_3_1_,
postjpaent1_.user_id as user_id4_3_1_,
postjpaent1_.user_id as user_id4_3_0__,
postjpaent1_.id as id1_3_0__
from
users userjpaent0_
left outer join
posts postjpaent1_
on userjpaent0_.id=postjpaent1_.user_id
Inner Join
은 두 테이블에서 일치하는 데이터만 반환하는 반면, Outer Join
은 한쪽 테이블에 데이터가 없는 경우에도 결과를 포함시킵니다.Inner Join
은 일반적으로 Outer Join
보다 결과 집합의 크기가 작다. 왜냐하면 Outer Join
은 일치하지 않는 행까지 포함하기 때문이다.@Entity
@Table(name = "categorys")
public class Category {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "category", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Product> products = new ArrayList<>();
}
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private Double price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews = new ArrayList<>();
}
@Entity
@Table(name = "reviews")
public class Review {
@Id
@GeneratedValue
private Long id;
private String content;
private int rating;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews = new ArrayList<>();
}
Category
는 여러 Product
를 가질 수 있습니다.Product
는 여러 Review
를 가질 수 있습니다.Customer
는 여러 Review
를 작성할 수 있습니다.Product
와 Customer
와의 다대일(N:1) 관계이다.@Transactional
@SpringBootTest
public class Multiple_JPA_OneToMany_Lazy_Loding_Test {
@BeforeEach
void setup() {
for (int i = 0; i < 2; i++) {
Category category = Category.builder()
.name("category_name" + i)
.build();
categoryRepo.save(category);
for (int j = 0; j < 2; j++) {
Product product = Product.builder()
.name("product_name" + j)
.price(10.0)
.category(category)
.build();
productRepo.save(product);
for (int k = 0; k < 2; k++) {
Customer customer = Customer.builder()
.name("customer_name" + k)
.email("customer_email" + k)
.build();
customerRepo.save(customer);
for (int l = 0; l < 2; l++) {
Review review = Review.builder()
.content("review_content" + l)
.rating(5)
.product(product)
.customer(customer)
.build();
reviewRepo.save(review);
}
}
}
}
}
}
Category
당 2개씩)Product
당 2개씩)Customer
당 2개씩)@Test
@DisplayName("Lazy Loading을 사용할 때 발생하는 N+1 문제를 확인합니다.")
void test1() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
System.out.println("--------------PRODUCT 조회--------------");
List<Product> products = productRepo.findAll();
System.out.println("--------------PRODUCT 조회 끝---------------\n");
for (Product product : products) {
System.out.println("PRODUCT : " + product.getName());
System.out.println("CATEGORY : " + product.getCategory().getName()); // 2번
for (Review review : product.getReviews()) { // 4번
System.out.println("REVIEW : " + review.getContent());
System.out.println("CUSTOMER : " + review.getCustomer().getName()); // 8번
}
}
}
------------ 영속성 컨텍스트 비우기 -----------
--------------PRODUCT 조회--------------
Hibernate: select product0_.id as id1_4_, product0_.category_id as category4_4_, product0_.name as name2_4_, product0_.price as price3_4_ from products product0_
--------------PRODUCT 조회 끝---------------
Hibernate: select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from categorys category0_ where category0_.id=?
Hibernate: select reviews0_.product_id as product_5_5_0_, reviews0_.id as id1_5_0_, reviews0_.id as id1_5_1_, reviews0_.content as content2_5_1_, reviews0_.customer_id as customer4_5_1_, reviews0_.product_id as product_5_5_1_, reviews0_.rating as rating3_5_1_ from reviews reviews0_ where reviews0_.product_id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
Hibernate: select reviews0_.product_id as product_5_5_0_, reviews0_.id as id1_5_0_, reviews0_.id as id1_5_1_, reviews0_.content as content2_5_1_, reviews0_.customer_id as customer4_5_1_, reviews0_.product_id as product_5_5_1_, reviews0_.rating as rating3_5_1_ from reviews reviews0_ where reviews0_.product_id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
Hibernate: select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from categorys category0_ where category0_.id=?
Hibernate: select reviews0_.product_id as product_5_5_0_, reviews0_.id as id1_5_0_, reviews0_.id as id1_5_1_, reviews0_.content as content2_5_1_, reviews0_.customer_id as customer4_5_1_, reviews0_.product_id as product_5_5_1_, reviews0_.rating as rating3_5_1_ from reviews reviews0_ where reviews0_.product_id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
Hibernate: select reviews0_.product_id as product_5_5_0_, reviews0_.id as id1_5_0_, reviews0_.id as id1_5_1_, reviews0_.content as content2_5_1_, reviews0_.customer_id as customer4_5_1_, reviews0_.product_id as product_5_5_1_, reviews0_.rating as rating3_5_1_ from reviews reviews0_ where reviews0_.product_id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
Hibernate: select customer0_.id as id1_2_0_, customer0_.email as email2_2_0_, customer0_.name as name3_2_0_ from customers customer0_ where customer0_.id=?
productRepo.findAll()
→ 1번 실행product.getCategory().getName()
→ 2번 실행product.getReviews()
→ 4번 실행review.getCustomer().getName()
→ 8번 실행public interface ProductRepo extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p JOIN FETCH p.category JOIN FETCH p.reviews r JOIN FETCH r.customer")
List<Product> findAllByMultiFetchJoin();
}
@Test
@DisplayName("Lazy Loading을 사용할 때 발생하는 N+1 문제를 확인합니다.")
void test2() {
em.flush();
em.clear();
System.out.println("------------ 영속성 컨텍스트 비우기 -----------\n");
System.out.println("--------------PRODUCT 조회--------------");
List<Product> products = productRepo.findAllByMultiFetchJoin();
System.out.println("--------------PRODUCT 조회 끝---------------\n");
for (Product product : products) {
System.out.println("PRODUCT : " + product.getName());
System.out.println("CATEGORY : " + product.getCategory().getName());
for (Review review : product.getReviews()) {
System.out.println("REVIEW : " + review.getContent());
System.out.println("CUSTOMER : " + review.getCustomer().getName());
}
}
}
------------ 영속성 컨텍스트 비우기 -----------
--------------PRODUCT 조회--------------
Hibernate:
select
product0_.id as id1_4_0_,
category1_.id as id1_1_1_,
reviews2_.id as id1_5_2_,
customer3_.id as id1_2_3_,
product0_.category_id as category4_4_0_,
product0_.name as name2_4_0_,
product0_.price as price3_4_0_,
category1_.name as name2_1_1_,
reviews2_.content as content2_5_2_,
reviews2_.customer_id as customer4_5_2_,
reviews2_.product_id as product_5_5_2_,
reviews2_.rating as rating3_5_2_,
reviews2_.product_id as product_5_5_0__,
reviews2_.id as id1_5_0__,
customer3_.email as email2_2_3_,
customer3_.name as name3_2_3_
from
products product0_
inner join
categorys category1_
on product0_.category_id=category1_.id
inner join
reviews reviews2_
on product0_.id=reviews2_.product_id
inner join
customers customer3_
on reviews2_.customer_id=customer3_.id
--------------PRODUCT 조회 끝---------------
productRepo.findAllByMultiFetchJoin()
→ 1번 실행Product
엔티티와 연관된 여러 Review
엔티티가 있을 때, Batch Size가 설정되면 JPA는 한 번의 쿼리로 설정된 수만큼의 Review
를 함께 로드한다.Review
에 대해 개별적인 쿼리를 실행하는 대신, 더 큰 단위의 데이터를 한꺼번에 가져와서 데이터베이스에 대한 쿼리 호출 수를 줄이는 효과를 가져온다.