| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- Kotlin
- 백엔드개발
- prometheus
- NIO
- RDBMS
- 데이터베이스
- 성능 최적화
- 트랜잭션
- 백엔드
- kafka
- CloudNative
- Kubernetes
- JPA
- jvm
- Java
- GitOps
- mysql
- helm
- selector
- webflux
- 성능최적화
- monitoring
- DevOps
- netty
- redis
- grafana
- docker
- spring boot
- 동시성제어
- SpringBoot
- Today
- Total
유성
[JPA] fetchJoin 과 Paging 처리의 한계 및 해결 방안 본문
EntityGraph를 사용하여 fetch join을 할 경우, 쿼리에서 직접적으로 페이징 처리가 어려워집니다.
이유는 다음과 같습니다:
1. 복잡한 조인 구조
예를 들어, A, B, C 세 테이블을 조인하는 경우를 생각해 봅시다.
A와 B는 1:N 관계이고, B와 C도 1:N 관계라면, 예를 들어 N이 3이라 가정할 때
페이지 크기가 10이면, 실제로 10개의 A 엔티티를 가져오더라도 조인 결과로는 최대 10 × 3 × 3 = 90개의 행(row)이 반환될 수 있습니다.
2. RDBMS와 객체지향의 차이
관계형 데이터베이스에서는 위와 같이 다중 조인된 결과에 대해 LIMIT을 적용할 때, 원래의 A 엔티티 기준으로 제한하지 않고 전체 행에 대해 제한을 걸게 됩니다.
그래서 결과적으로 full scan을 하고, 반환된 결과들을 객체로 매핑한 후 Java 코드 내에서 subList() 같은 메서드로 페이징 처리를 하게 됩니다.
해결 방법은 두 가지로 나뉩니다:
1. paging이 필요한 하나의 테이블만 조회 후 나머지 테이블 fetchJoin 또는 entityGraph 조회
A 테이블만 조회하면 SQL 단계에서 LIMIT을 적용해 원하는 페이징 처리가 가능하며, 나머지 B, C 데이터는 별도의 fetch join이나 EntityGraph를 사용하여 조회할 수 있습니다.
2. paging이 필요한 하나의 테이블만 조회 후 Batch Size 옵션을 사용한 LazyLoading
성능상 fetch join이나 EntityGraph를 사용해야 하는 경우가 아니라면, @BatchSize 애너테이션이나 spring.jpa.properties.hibernate.default_batch_fetch_size 설정을 통해 연관 엔티티들을 한 번에 조회하도록 할 수 있습니다.
예시 코드
아래 코드는 2번째 방법인 Order 테이블에서 페이징 처리를 먼저 완료하고, 이후에 OrderItem과 그에 연관된 Product를 Lazy Loading으로 초기화하는 방법입니다:
@Transactional(readOnly = true)
public Page<Order> findOrdersWithOrdersItemsAndProductsByUserId(long userId, Pageable pageable) {
// 1. 페이징된 Order 엔티티들을 조회
Page<Order> findPagedOrders = orderRepository.findAllByBuyerId(userId, pageable);
// 2. 각 Order에 포함된 OrderItem과 Product를 강제로 초기화 (Lazy Loading)
List<Long> orderIds = findPagedOrders.map(Order::getId).toList();
if (!orderIds.isEmpty()) {
// 각 Order의 OrderItems 컬렉션을 초기화
findPagedOrders.getContent().forEach(order ->
order.getOrderItems().forEach(orderItem ->
orderItem.getProduct().getId() // Product의 식별자 접근으로 초기화
)
);
}
return findPagedOrders;
}
이 방식은 Order 엔티티들을 먼저 페이징 처리한 후, 각 Order의 Lazy Loaded 컬렉션에 접근하여 배치로 초기화합니다.
만약 배치 사이즈 설정이 없다면 N+1 문제가 발생할 수 있으므로, 위에서 언급한 @BatchSize나 hibernate.default_batch_fetch_size 옵션을 통해 쿼리 수를 줄일 수 있습니다.
또한 메인 로직이 아님에도 .forEach 같은 코드들이 있으면 가독성이 떨어지는 문제도 있으므로 아래와 같이 forceLoad를 사용하여
가독성을 개선할 수 있습니다.
public static <T> void forceLoad(Collection<T> list, Function<T, ?> loader) {
list.forEach(item -> {
Object lazyProperty = loader.apply(item);
if (lazyProperty instanceof Collection) {
int noUseValue = ((Collection<?>) lazyProperty).size();
} else if (lazyProperty != null) {
String noUseValue = lazyProperty.toString();
}
});
}
// 사용법 LazyLoadingUtils.forceLoad(orders, Order::getOrderItems);
public static <T> void forceLoad(T item, Function<T, ?> loader) {
Object lazyProperty = loader.apply(item);
if (lazyProperty instanceof Collection) {
int noUseValue = ((Collection<?>) lazyProperty).size();
} else if (lazyProperty != null) {
String noUseValue = lazyProperty.toString();
}
}
// 사용법 LazyLoadingUtils.forceLoad(orderItems, OrderItem::getProduct);
'Spring Data' 카테고리의 다른 글
| Spring Boot에서 MySQL Master-Slave (Replication) 효율적인 연결 (0) | 2026.01.22 |
|---|---|
| JPA의 진짜 설계 의도: 왜 N+1 문제를 방치하였나? (4) | 2025.07.26 |
| RDBMS 격리 수준 (0) | 2024.11.25 |
| 예외 발생 시 롤백 방지하기: Spring에서 독립적인 트랜잭션 설정 방법 (3) | 2024.09.07 |
| JPA와 Hibernate의 차이점은 무엇인가요? (0) | 2023.09.02 |