@Transactional
public List<ProductReviewResponse> getProductReviews(Long accommodationId) {
// ...
private Accommodation getAccommodation(Long accommodationId) {
Accommodation accommodation = accommodationRepository.getAccommodationWithProductsById(accommodationId)
.orElseThrow(() -> new AccommodationException(ACCOMMODATION_NOT_FOUND));
return accommodation;
}
// ...
}
@Repository
public interface AccommodationRepository extends JpaRepository<Accommodation, Long> {
@Query("select a from Accommodation a join fetch a.productList where a.id = :accommodationId")
Optional<Accommodation> getAccommodationWithProductsById(@Param("accommodationId") Long accommodationId);
}
- accommodation_id 를 이용하여 accommdation 엔티티를 조회할 때 fetch join 을 이용하여 accommdation 엔티티와 연관된 product 엔티티들도 함께 로딩한다.
( 영속성 컨텍스트에 저장 )- 이후 accommodation 엔티티나 product 엔티티에 접근할 때, 데이터베이스에 대한 추가 쿼리가 발생하지 않는다. 영속성 컨텍스트에 저장된 데이터를 사용한다.
해당 메서드 호출 시 hibernate 가 생성한 sql 쿼리를 콘솔에서 확인해보면 다음과 같다.
Hibernate:
select
a1_0.accommodation_id,
a1_0.address,
a1_0.area_code,
a1_0.category_id,
a1_0.accommodation_facility_id,
a1_0.latitude,
a1_0.longitude,
a1_0.name,
a1_0.phone,
p1_0.accommodation_id,
p1_0.product_id,
p1_0.check_in_time,
p1_0.check_out_time,
p1_0.count,
p1_0.maximum_number,
p1_0.name,
p1_0.product_facility_id,
p1_0.standard_number
from
accommodation a1_0
join
product p1_0
on a1_0.accommodation_id=p1_0.accommodation_id
where
a1_0.accommodation_id=?
hibernate 가 생성한 sql 쿼리에서 다른 연관관계에 놓인 테이블에 대한 추가 쿼리문은 발생하지 않았다.
모두 지연 로딩으로 설정했기 때문에 해당 정보를 조회하는 시점까지 데이터베이스에 로딩되지 않는다. 이후 해당 필드에 접근하는 시점에서 jpa 가 추가적인 쿼리를 실행하여 데이터를 가져올 것으로 판단된다.
리뷰 조회 시 필요한 정보가 아니기 때문에 지연 로딩으로 설정하여 불필요한 데이터베이스 조회를 방지할 수 있고 성능 향상을 이끌어 낼 수 있다.
many-to-one 연관관계의 경우 기본 타입이 즉시 로딩이기 때문에 추가 쿼리문 발생
@Entity @Table (name = "accommodation") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class Accommodation { // ... @ManyToOne @JoinColumn(name = "category_id") private Category category; // ... }Hibernate: select a1_0.accommodation_id, a1_0.address, a1_0.area_code, a1_0.category_id, a1_0.accommodation_facility_id, a1_0.latitude, a1_0.longitude, a1_0.name, a1_0.phone, p1_0.accommodation_id, p1_0.product_id, p1_0.check_in_time, p1_0.check_out_time, p1_0.count, p1_0.maximum_number, p1_0.name, p1_0.product_facility_id, p1_0.standard_number from accommodation a1_0 join product p1_0 on a1_0.accommodation_id=p1_0.accommodation_id where a1_0.accommodation_id=? Hibernate: select c1_0.category_id, c1_0.category_code from category c1_0 where c1_0.category_id=?
실제 데이터베이스에서 해당 jpql 쿼리에 해당하는 sql을 실행하면 다음과 같은 형태로 데이터가 나타난다.
select a.*, p.*
from accommodation a
inner join product p on a.accommodation_id = p.accommodation_id
where a.accommodation_id = 25;

@Transactional
public List<ProductReviewResponse> getProductReviews(Long accommodationId) {
// ...
List<Product> products = accommodation.getProductList();
List<ProductReviewResponse> productReviewResponseList = new ArrayList<>();
// ...
}
- 특정 숙소에 대한 정보는 위의 과정을 통해서 즉시 로딩된 상태이다.
- 영속성 컨텍스트에서 이미 해당 데이터를 가지고 있어 추가로 데이터베이스에 접근하지 않는다.
- 그렇기 때문에 accommodation 엔티티 접근 시 추가 쿼리가 발생하지 않는다.
@Transactional
public List<ProductReviewResponse> getProductReviews(Long accommodationId) {
// ...
for (Product product : products) {
List<Review> reviews = product.getReviewList();
productReviewResponseList.addAll(reviews.stream()
.map(ProductReviewResponse::fromEntity)
.collect(Collectors.toList()));
}
}
@Entity
@Table(name = "product")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
// ...
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE)
@BatchSize(size = 100)
private List<Review> reviewList = new ArrayList<>();
// ...
}
@Entity
@Table (name = "review")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Review {
// ...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private User user;
// ...
}
- product 엔티티 접근 시에도 마찬가지로 추가 쿼리가 발생하지 않는다.
- product 엔티티와 one-to-many 연관관계인 review 엔티티에 대한 @BatchSize 어노테이션을 작성했고, size = 100 으로 설정한 상태이다.
- review 엔티티와 many-to-one 연관관계인 user 엔티티에 대한 fetch type = eager 로 설정한 상태이다.
( review 조회 시 user 정보는 필수적이라 default 값인 eager 로 설정했다 )- 이후 review 엔티티에 접근 시 지정한 size 만큼 in 절을 사용하여 조회하며, user 에 관한 정보도 즉시 로딩한다.
review 엔티티 접근 시 hibernate 가 생성한 sql 쿼리를 콘솔에서 확인해보면 다음과 같다.
Hibernate:
select
r1_0.product_id,
r1_0.review_id,
r1_0.content,
r1_0.order_item_id,
r1_0.review_date,
r1_0.score,
u1_0.user_id,
u1_0.authority,
u1_0.cart_id,
u1_0.email,
u1_0.name,
u1_0.password
from
review r1_0
left join
user u1_0
on u1_0.user_id=r1_0.user_id
where
r1_0.product_id in(?,?,?,?) // 95, 96, 97, 98
실제 데이터베이스에서 해당 sql을 실행하면 다음과 같은 형태로 데이터가 나타난다.
select r.*
from review r left join user u r.user_id = u.user_id
where r.product_id in (95, 96, 97, 98);

이외에 reveiw 엔티티와 연관된 모든 엔티티에 대해서 지연 로딩으로 설정했다. 그렇기 때문에 review 엔티티에 접근하는 시점에서 추가적인 쿼리는 발생하지 않는다.
one-to-one 연관관계의 경우 기본 타입이 즉시 로딩이기 때문에 추가적인 쿼리문 발생
@Entity @Table (name = "review") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class Review { // ... @OneToOne @JoinColumn(name = "order_item_id") private OrderItem orderItem; // ... }Hibernate: select r1_0.product_id, r1_0.review_id, r1_0.content, o1_0.order_item_id, o1_0.end_date, o1_0.order_id, o1_0.person_number, o1_0.price, o1_0.product_id, o1_0.review_written, o1_0.start_date, r1_0.review_date, r1_0.score, u1_0.user_id, u1_0.authority, u1_0.cart_id, u1_0.email, u1_0.name, u1_0.password from review r1_0 left join order_item o1_0 on o1_0.order_item_id=r1_0.order_item_id left join user u1_0 on u1_0.user_id=r1_0.user_id where r1_0.product_id in(?,?,?,?)
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProductReviewResponse {
// ...
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class ProductDetailsResponse {
private Long productId;
private String productImage;
private String productName;
public static ProductDetailsResponse fromEntity(Review review) {
return ProductDetailsResponse.builder()
.productId(review.getProduct().getId())
.productImage(review.getProduct().getProductImageList().get(0).getImageUrl())
.productName(review.getProduct().getName())
.build();
}
}
@Entity
@Table(name = "product")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
// ...
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE)
@BatchSize(size = 100)
private List<ProductImage> productImageList = new ArrayList<>();
// ...
}
- product 엔티티와 one-to-many 연관관계인 product_image_list 엔티티에 대한 @BatchSize 어노테이션을 작성했고, size = 100 으로 설정한 상태이다.
- 이후 product_image_list 엔티티에 접근 시 지정한 size 만큼 in 절을 사용하여 조회한다.
product_image_list 엔티티 접근 시 hibernate 가 생성한 sql 쿼리를 콘솔에서 확인해보면 다음과 같다.
Hibernate:
select
p1_0.product_id,
p1_0.product_image_id,
p1_0.image_url
from
product_image p1_0
where
p1_0.product_id in(?,?,?,?) // 95, 96, 97, 98
실제 데이터베이스에서 해당 jpql 쿼리에 해당하는 sql을 실행하면 다음과 같은 형태로 데이터가 나타난다.
select pi.*
from product_image pi
where pi.product_id in (95, 96, 97, 98);

숙소 전체 리뷰 조회시 발생하는 전체 쿼리문은 다음과 같다.
Hibernate:
select
a1_0.accommodation_id,
a1_0.address,
a1_0.area_code,
a1_0.category_id,
a1_0.accommodation_facility_id,
a1_0.latitude,
a1_0.longitude,
a1_0.name,
a1_0.phone,
p1_0.accommodation_id,
p1_0.product_id,
p1_0.check_in_time,
p1_0.check_out_time,
p1_0.count,
p1_0.maximum_number,
p1_0.name,
p1_0.product_facility_id,
p1_0.standard_number
from
accommodation a1_0
join
product p1_0
on a1_0.accommodation_id=p1_0.accommodation_id
where
a1_0.accommodation_id=?
Hibernate:
select
r1_0.product_id,
r1_0.review_id,
r1_0.content,
r1_0.review_date,
r1_0.score,
u1_0.user_id,
u1_0.authority,
u1_0.cart_id,
u1_0.email,
u1_0.name,
u1_0.password
from
review r1_0
left join
user u1_0
on u1_0.user_id=r1_0.user_id
where
r1_0.product_id in(?,?,?,?)
Hibernate:
select
p1_0.product_id,
p1_0.product_image_id,
p1_0.image_url
from
product_image p1_0
where
p1_0.product_id in(?,?,?,?)
@Entity
@Table(name = "product")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
//...
@BatchSize(size = 100)
private List<ProductImage> productImageList = new ArrayList<>();
//...
}
@Transactional
public List<UserReviewResponse> getUserReviews() {
User user = getUser();
List<Review> reviews = reviewRepository.getReviewsByUserWithDetails(user);
List<UserReviewResponse> userReviewResponseList = reviews.stream()
.map(UserReviewResponse::fromEntity)
.collect(Collectors.toList());
return userReviewResponseList;
}
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
@Query("select r " +
"from Review r " +
"left join fetch r.product p " +
"left join fetch p.accommodation a " +
"left join fetch r.orderItem oi " +
"where r.user = :user")
List<Review> getReviewsByUserWithDetails(@Param("user") User user);
}
- 특정 사용자가 작성한 리뷰를 조회하며, 연관된 엔티티들에 대한 패치 조인을 통해 N+1 문제를 최적화.
- 패치 조인을 사용하여 관련 엔티티를 즉시 로딩하도록 설정하여, 한 번의 쿼리로 관련된 엔티티를 로딩한다.
- 모든 관련 엔티티에 대해 주문 상품, 상품, 숙소 등에 대해 패치 조인을 사용한다.
4.리뷰 엔티티 검색할 때 관련된 엔티티가 함께 로딩되므로 추가적인 쿼리가 발생하지 않는다.
- 특정 상품의 이미지 리스트에 대한 batch size = 100 으로 설정한 상태이다.
- 이후 상품 이미지 엔티티 접근 시 지정한 개수만큼 상품 아이디에 해당하는 상품 이미지 즉시 로딩
사용자 리뷰 전체 조회시 발생하는 전체 쿼리문은 다음과 같다.
Hibernate:
select
u1_0.user_id,
u1_0.authority,
u1_0.cart_id,
u1_0.email,
u1_0.name,
u1_0.password
from
user u1_0
where
u1_0.user_id=?
Hibernate:
select
r1_0.review_id,
r1_0.content,
o1_0.order_item_id,
o1_0.end_date,
o1_0.order_id,
o1_0.person_number,
o1_0.price,
o1_0.product_id,
o1_0.review_written,
o1_0.start_date,
p1_0.product_id,
a1_0.accommodation_id,
a1_0.address,
a1_0.area_code,
a1_0.category_id,
a1_0.accommodation_facility_id,
a1_0.latitude,
a1_0.longitude,
a1_0.name,
a1_0.phone,
p1_0.check_in_time,
p1_0.check_out_time,
p1_0.count,
p1_0.maximum_number,
p1_0.name,
p1_0.product_facility_id,
p1_0.standard_number,
r1_0.review_date,
r1_0.score,
r1_0.user_id
from
review r1_0
join
product p1_0
on p1_0.product_id=r1_0.product_id
join
accommodation a1_0
on a1_0.accommodation_id=p1_0.accommodation_id
join
order_item o1_0
on o1_0.order_item_id=r1_0.order_item_id
where
r1_0.user_id=?
Hibernate:
select
p1_0.product_id,
p1_0.product_image_id,
p1_0.image_url
from
product_image p1_0
where
p1_0.product_id in(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)