
1:N 관계(OneToMany)의 Entity 데이터를 조회할 때, FetchJoin을 활용 해 한방 쿼리로 조회를 하려고 시도
쿼리 확인 결과 limit,offset 절이 날아가지 않음 확인, 그러나 Pagination 정상 동작 확인
[WARN Log] HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory 발생
모든 데이터를 조회 하여 메모리에 저장 중 이라고 한다, 데이터가 많아진다면 성능 이슈가 발생할 수 있을거라 예상됨'
Code
// ..........
JPQLQuery<Order> query = from(order)
.innerJoin(order.user,QUser.user)
.fetchJoin()
.innerJoin(order.propOrders, QPropOrder.propOrder)
.fetchJoin()
.innerJoin(propOrder.prop, prop)
.fetchJoin()
.innerJoin(prop.propImage, QPropImage.propImage)
.fetchJoin()
.innerJoin(prop.propFile, QPropFile.propFile)
.fetchJoin()
.innerJoin(prop.detailCategory, QDetailCategory.detailCategory)
.fetchJoin()
.innerJoin(prop.middleCategory, QMiddleCategory.middleCategory)
.fetchJoin()
.innerJoin(prop.largeCategory, QLargeCategory.largeCategory)
.fetchJoin()
;
if (keyWord != null && !keyWord.isEmpty()) {
query.where(
prop.title.containsIgnoreCase(keyWord)
.or(prop.contents.containsIgnoreCase(keyWord))
);
}
//............. Pagination 적용 부
.orElseThrow(() -> new ServiceLogicException(ErrorCode.DATA_ACCESS_ERROR))
.applyPagination(pageable, query)
.fetch();
// ........
Order 테이블이 여러 테이블과 관계가 있어 우선 무지성 FetchJoin을 하였다.
경고 문구

Query문의 offset & limit 절 없음

요구하는 응답 데이터 형태(List Data 중 하나의 객체 추출)
{
"orderId": 4,
"orderStatus": "APPROVED",
"payType": "PAYPAL",
"userId": 2,
"nickName": "test2",
"email": "test2@test.com",
"orderPlatformId": "12HJK14GK23BDDDdK3",
"orderPlatformStatus": "APPROVED",
"orderProps": [
{
"propId": 3,
"baseFileName": "Base File Name2",
"extension": ".fbx",
"detailImageUrl": "https://grdd....",
"propFileId": 1,
"title": "Title2",
"contents": "Content2",
"propPrice": "9.99",
"downloadCount": 3,
"orderCount": 2,
"likesCount": 19,
"propStatus": "SALE",
"largeCategoryId": 1,
"largeCategoryName": "Large Category",
"middleCategoryId": 1,
"middleCategoryName": "Middle Category",
"detailCategoryId": 1,
"detailCategoryName": "Detail Category",
"createAt": "2023-08-31T13:01:31.068189",
"updateAt": "2023-08-31T13:01:31.068189"
},
{
"propId": 4,
"baseFileName": "Base File Name3",
"extension": ".fbx",
"detailImageUrl": "https://grdd....",
"propFileId": 1,
"title": "Title3",
"contents": "Content3",
"propPrice": "9.99",
"downloadCount": 3,
"orderCount": 2,
"likesCount": 19,
"propStatus": "SALE",
"largeCategoryId": 1,
"largeCategoryName": "Large Category",
"middleCategoryId": 1,
"middleCategoryName": "Middle Category",
"detailCategoryId": 1,
"detailCategoryName": "Detail Category",
"createAt": "2023-08-31T13:01:31.068189",
"updateAt": "2023-08-31T13:01:31.068189"
}
],
"totalPrice": "19.98",
"createAt": "2023-08-31T13:01:31.07019",
"updateAt": "2023-08-31T13:01:31.07019"

FetchJoin은 엔티티 그래프를 탐색 하여야 하는 경우, 즉 서비스 로직, 데이터의 수정과 같은 경우에 사용한다.
단순한 조회 로직에서는 사용할 필요가 없으며, 데이터의 조회만 할 경우 Dto를 반환하는 로직으로 구현 하여 처리하는 것이 좋다.
추가로, 나의 경우 N:M 관계를 1:N / N:1로 풀어 놓은 상태라서, 조금 더 유동적으로 조회 하기 위해 N 테이블의 엔티티 Repository를 구현하여 개선 하였다.
위 JSON 데이터에서 orderProps 객체는 리스트 타입으로, orderId에 의해 분류 된다 (하나의 Order에 여러개의 Prop 데이터가 있다고 생각하면 됨)
GroupBy로 order별 prop을 묶어서 데이터 조회가 필요 했고, 필요한 테이블에 Join 후 Transform으로 groupBy하여 Dto 데이터로 응답 하는 방향으로 수정 하였다.
첫번째 개선 시도 Code
List<PropOrderResponseDto> fetch = Optional.ofNullable(getQuerydsl())
.orElseThrow(() -> new ServiceLogicException(ErrorCode.DATA_ACCESS_ERROR))
.applyPagination(pageable, query)
.transform(
groupBy(propOrder.order.orderId)
.list(
Projections.constructor(
PropOrderResponseDto.class,
propOrder.order.orderId,
propOrder.order.orderStatus,
propOrder.order.payType,
propOrder.order.user.userId,
propOrder.order.user.nickName,
propOrder.order.user.email,
propOrder.order.orderPlatformId,
propOrder.order.orderPlatformStatus,
GroupBy.list(
Projections.constructor(
PropResponseDto.class,
propOrder.prop.propId,
propOrder.prop.baseFileName,
propOrder.prop.extension,
propOrder.prop.propImage.url,
propOrder.prop.propFile.propFileId,
propOrder.prop.title,
propOrder.prop.contents,
propOrder.prop.propPrice,
propOrder.prop.downloadCount,
propOrder.prop.orderCount,
propOrder.prop.likesCount,
propOrder.prop.propStatus,
propOrder.prop.largeCategory.largeCategoryId,
propOrder.prop.largeCategory.name,
propOrder.prop.middleCategory.middleCategoryId,
propOrder.prop.middleCategory.name,
propOrder.prop.detailCategory.detailCategoryId,
propOrder.prop.detailCategory.name,
propOrder.prop.createAt,
propOrder.prop.updateAt
)
)
,
propOrder.order.totalPrice,
propOrder.order.createAt,
propOrder.order.updateAt
)
)
);
Projections.constructor를 활용하여 DTO 생성자에 알맞은 필드를 바인딩 한다
GroupBy.list를 활용해 List 객체 필드도 바인딩 해준다
개선 후 쿼리

[WARN Log] HHH90003004 로그도 발생하지 않고, offset 절이 정상적으로 날아가는걸 확인
다른 문제가 발생한다

DB에 들어있는 데이터는 12개의 더미 데이터고, 실제 응답으로 나오는 데이터는 9개로 조회 되고 있다
groupBy가 적용되기 이전 데이터의 상태를 그림으로 보면
groupBy가 적용되고 난 이후 데이터 상태를 확인 해 보면 아래와 같을 것이다.
위와 같은 상태로 offset을 하게 되면, 10개의 데이터를 조회하더라도 offset의 기준이 groupBy가 적용되기 이전 데이터에서 잘리기 때문에 3개의 데이터가 최종 데이터로 조회 된다.
뤼튼 똑똑해
위 문제를 해결하기 위해 Order 테이블을 먼저 조회 후 orderId를 통해 Prop 데이터를 매칭 시켜 추가 조회 후 응답 Dto에 서비스 로직에서 매핑 시켜주기로 함
추기로 orderId 리스트로 prop을 조회 하게 되면, orderId Size 만큼 쿼리가 발생하기 때문에 default_batch_fetch_size: 1000 옵션을 주어 in 절로 묶어 한번에 최대 1000개의 쿼리를 하나로 날려 주도록 설정
개선 Code
// OrderRepositoryImpl.java
JPQLQuery<PropOrderResponseDto> query = from(order)
.join(prop)
.distinct()
.select(
Projections.constructor(
PropOrderResponseDto.class,
order.orderId,
order.orderStatus,
order.payType,
order.user.userId,
order.user.nickName,
order.user.email,
order.orderPlatformId,
order.orderPlatformStatus,
order.totalPrice,
order.createAt,
order.updateAt
)
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(
order.createAt.desc()
)
;
// ......
List<PropOrderResponseDto> fetch = query.fetch();
return new PageImpl<>(fetch, pageable, query.fetchCount());
//.....
//PropOrderRepositoryImpl.java
public List<OrderPropQueryDto> findAllPropOrderResponseDto(List<Long> orderIds) {
return from(propOrder)
.select(
Projections.constructor(
OrderPropQueryDto.class,
propOrder.order.orderId,
Projections.constructor(
PropResponseDto.class,
propOrder.prop.propId,
propOrder.prop.baseFileName,
propOrder.prop.extension,
propOrder.prop.propImage.url,
propOrder.prop.propFile.propFileId,
propOrder.prop.title,
propOrder.prop.contents,
propOrder.prop.propPrice,
propOrder.prop.downloadCount,
propOrder.prop.orderCount,
propOrder.prop.likesCount,
propOrder.prop.propStatus,
propOrder.prop.largeCategory.largeCategoryId,
propOrder.prop.largeCategory.name,
propOrder.prop.middleCategory.middleCategoryId,
propOrder.prop.middleCategory.name,
propOrder.prop.detailCategory.detailCategoryId,
propOrder.prop.detailCategory.name,
propOrder.prop.createAt,
propOrder.prop.updateAt
)
)
).where(propOrder.order.orderId.in(orderIds))
.fetch();
}
//OrderService.java
public Page<PropOrderResponseDto> findAllOrderSearch(
LocalDate startDate,
LocalDate endDate,
String keyWord,
Long userId,
Pageable pageable,
String orderStatus
) {
LocalDateTime start = LocalDateTime.of(startDate, LocalTime.MIN);
LocalDateTime end = LocalDateTime.of(endDate, LocalTime.MAX);
OrderStatus orderStatusEnum = null;
if (!orderStatus.isEmpty() && OrderStatus.getValueStringList().contains(orderStatus)) {
orderStatusEnum = OrderStatus.valueOf(orderStatus);
}
Page<PropOrderResponseDto> findOrderPage = orderRepository.findAllProp(
pageable,
userId,
start,
end,
keyWord,
orderStatusEnum
);
List<Long> idList = findOrderPage.map(PropOrderResponseDto::getOrderId).toList();
List<OrderPropQueryDto> findPropList = propOrderRepository.findAllPropOrderResponseDto(idList);
return findOrderPage.map(por ->
{
List<PropResponseDto> list = findPropList
.stream()
.filter(a -> a.getOrderId().equals(por.getOrderId()))
.map(OrderPropQueryDto::getProp)
.toList();
por.setOrderProps(list);
return por;
}
);
}

QueryDSL에 대한 학습이 더 필요하다, 쿼리를 나눠 조회하는 방식 외에 개선할 수 있는 방법이 있는지 더 찾아보고 학습을 해 나갈 예정이다.