| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- R
- Graph
- list 컬렉션
- buffer
- C 언어 코딩 도장
- 알기쉬운 알고리즘
- coding test
- insertion sort
- 윤성우의 열혈 자료구조
- 윤성우 열혈자료구조
- Stack
- JSON
- 혼자 공부하는 C언어
- ㅅ
- stream
- s
- datastructure
- Serialization
- C programming
- Algorithm
- 이스케이프 문자
- 메모리구조
- 이것이 자바다
- Selection Sorting
- Today
- Total
Engineering Note
[JPA] 컬렉션 Fetch Join 페이지네이션 문제와 해결책 본문
컬렉션을 Fetch Join하면 생기는 문제
일대다 연관관계를 맺는 컬렉션 엔티티를 Fetch Join하면 페이지네이션이 적용되지 않는 문제가 있다. 정확하게는 JPA는 페이지네이션을 적용하기 위해 DB의 데이터를 메모리로 가지고와서 페이지네이션을 수행하는데, 이렇게 되면 데이터가 중복돼서 페이지네이션이 정상적으로 적용되지 못할 수도 있고, 데이터가 메모리의 용량을 초과할 경우 Out Of Memory 에러가 발생할 수 있다.
상황
Order Entity
- Order Entity는 Member, Deliver와는 일대일관계지만 OrderItem과는 일대다
- Item은 OrderItem과 일대다 관계
주문 내역을 조회를 위해 쿼리로 offset, limit으로 0(첫 번째 페이지)에서 100개의 데이터를 가져오는 코드
public List<Order> findAllWithItem() {
return em.createQuery(
"select distinct o from Order o"
+ " join fetch o.member m"
+ " join fetch o.delivery d"
+ " join fetch o.orderItems oi"
+ " join fetch oi.item i",
Order.class)
.setFirstResult(0)
.setMaxResults(100)
.getResultList();
}
실제 수행 쿼리와 경고 로그 메세지
2025-12-29T19:11:36.532+09:00 WARN 69105 --- [nio-8080-exec-1] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
2025-12-29T19:11:36.549+09:00 DEBUG 69105 --- [nio-8080-exec-1] org.hibernate.SQL :
select
distinct o1_0.order_id,
d1_0.delivery_id,
d1_0.city,
d1_0.street,
d1_0.zipcode,
d1_0.status,
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name,
o1_0.order_date,
oi1_0.order_id,
oi1_0.order_item_id,
oi1_0.count,
i1_0.item_id,
i1_0.dtype,
i1_0.name,
i1_0.price,
i1_0.stock_quantity,
i1_0.artis,
i1_0.title,
i1_0.author,
i1_0.isbn,
i1_0.director,
i1_0.etc,
oi1_0.order_price,
o1_0.status
from
orders o1_0
join
member m1_0
on m1_0.member_id=o1_0.member_id
join
delivery d1_0
on d1_0.delivery_id=o1_0.delivery_id
join
order_item oi1_0
on o1_0.order_id=oi1_0.order_id
join
item i1_0
on i1_0.item_id=oi1_0.item_id
=> 실제 수행된 쿼리를 보면 offset과 limit가 쿼리에 적용되지 않았다. 그리고 윗줄의 로그를 보면 출력된 로그를 보면 경고 메세지와 함께 쿼리가 적용되지 않은 이유를 알 수 있다.
컬렉션 페치조인에서는 offset/limit에 해당하는 'firstResult/maxResults'이 적용되지 않고 메모리에서 적용된다는 내용이다.
해결 방법1
컬렉션 페치조인 페이지네이션 문제를 해결하는 방법은 단계적으로는 일대일 관계만 먼저 fetch join으로 조회하고 일대다 관계의 엔티티는 지연로딩으로 두면서 batch_size를 이용하는 방법이다.
public List<Order> findAllWithMemberDelivery(int offset, int limit) {
return em.createQuery(
"select o from Order o" +
" join fetch o.member m" +
" join fetch o.delivery d", Order.class
).setFirstResult(offset)
.setMaxResults(limit).getResultList();
}
위에서 조회한 Order Entity에 대해 루프를 돌면서 지연로딩으로 OrderItems 등 데이터를 세팅하는 코드. (N+1발생 코드)
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
OrderDto 생성자
public OrderDto(Order order) {
this.orderId = order.getId();
this.name = order.getMember().getName();
this.orderDate = order.getOrderDate();
this.orderStatus = order.getStatus();
this.address = order.getDelivery().getAddress();
this.orderItems = order.getOrderItems().stream().map(orderItem -> new OrderItemDto(orderItem))
.collect(Collectors.toList());
}
application.yml
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
default_batch_fetch_size: 100
해결방법2
기준이 되는 엔티티만 먼저 페이지네이션을 하고 조회된 PK를 기준으로 fetch join 하면서 where 조건절에 id값을 in 쿼리로 수행하는 방법이 있다.
@Override
public List<Order> findOrdersByEmail(String email, Pageable pageable) {
// 1단계: ID만 페이징 (DB 페이징)
List<Long> orderIds = queryFactory
.select(order.id)
.from(order)
.where(order.member.email.eq(email))
.orderBy(order.regTime.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
if (orderIds.isEmpty()) {
return List.of();
}
// 2단계: Fetch Join (IN 쿼리)
return queryFactory
.selectFrom(order)
.distinct()
.join(order.orderItems, orderItem).fetchJoin()
.join(orderItem.item, item).fetchJoin()
.where(order.id.in(orderIds))
.orderBy(order.regTime.desc())
.fetch();
}
두 방법은 사실 같은 방법이기 때문에 취향에 맞게 사용하면 된다.
@BatchSize
- 'N+1'로 실행되는 쿼리는 데이터베이스를 엄청나게 많이 사용하는 어노테이션.
- size 속성을 지정해 'N'번 수행되는 쿼리를 모아서 한번에 실행할 수 있게 해준다.
최종 주문 내역 조회 코드
@GetMapping("/api/v3.1/orders")
public List<OrderDto> ordersV3_page(
@RequestParam(value = "offset", defaultValue = "0") int offset,
@RequestParam(value = "limit", defaultValue = "100") int limit) {
List<Order> orders = orderRepository.findAllWithMemberDelivery(offset, limit);
List<OrderDto> result = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return result;
}
참고자료 : 인프런 JPA 활용2편(김영한)
'Server > JPA ORM' 카테고리의 다른 글
| [JPA] 영속성 전이 옵션과 연관관계 편의 메서드가 필요한 이유, 고아객체 설정 (1) | 2026.01.10 |
|---|---|
| [JPA] JPA 활용 주의점과 사용팁 (0) | 2026.01.06 |
| [JPA] Repository로 동시성 테스트하면 안 되는 이유 (0) | 2025.12.24 |
| [JPA] Spring Data JPA Query Method 네이밍 규칙 (0) | 2025.12.23 |
| [JPA] 지연로딩과 N+1문제해결, 실행 쿼리 비교 (0) | 2025.11.09 |