QueryDsl을 활용해서 조회를 구현하던 도중 잘 해결되지 않았던 문제가 있어서 공유를 한다.
개요
해당 문제가 발생했던 지점은 주문, 결제 서비스에서 전체 주문 또는 전체 결제 조회를 구현하려던 도중 발생하였다.
- 권한
- Customer(고객)
- Owner(점주)
- Admin(관리자)
이때 내가 고민한 부분은 주문 전체 조회를 할 때 삭제된 주문에 대한 처리이다.
- 고객은 본인 또는 관리자가 삭제한 주문 내역은 조회할 수 없지만 점주가 삭제한 주문 내역은 고객에게는 동일하게 보여져야 한다.
- 점주는 본인 또는 관리자가 삭제한 주문 내역은 조회할 수 없지만 고객이 삭제한 주문 내역은 점주에게는 동일하게 보여져야 한다.
- 관리자가 삭제한 주문 내역은 아무도 볼 수 없게 된다.
위 방식대로 구현하기 위해 내가 만든 QueryDsl 코드는 다음과 같다.
로직
@Transactional(readOnly = true)
public Page<GetOrderResponseDto> getOrders(Long userId, UUID storeId, OrderType type, OrderStatus status, OrderPaymentStatus orderPaymentStatus, String show, Pageable pageable) {
User loginUser = securityUtil.getCurrentUser();
Long loginUserId = loginUser.getId();
log.info("주문 조회 요청 loginUserId: {}, userId: {}, storeId", loginUserId, userId, storeId);
if (isOwner(loginUser) || isCustomer(loginUser)) {
if (show != null) {
throw new AccessDeniedException();
}
}
List<Order> orders = jpaQueryFactory
.selectFrom(order)
.where(
userEq(userId, loginUser),
storeEq(storeId, loginUser),
typeEq(type),
statusEq(status),
orderPaymentStatusEq(orderPaymentStatus),
showEq(show, loginUser)
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(getSortOrder(pageable))
.fetch();
user 필터링
private BooleanExpression userEq(Long userId, User loginUser) {
if (isCustomer(loginUser)) {
if (userId != null && userId != loginUser.getId()) {
throw new AccessDeniedException();
}
return order.user.eq(loginUser);
}
return (userId != null) ? order.user.eq(findUserById(userId)) : Expressions.TRUE;
}
store 필터링
private BooleanExpression storeEq(UUID storeId, User loginUser) {
List<UUID> loginUserStoreList = loginUser.getStores().stream().map(store -> store.getId()).collect(Collectors.toList());
if (isOwner(loginUser)) {
if (storeId != null && !loginUserStoreList.contains(storeId)) {
throw new AccessDeniedException();
}
return order.store.id.in(loginUserStoreList);
}
return (storeId != null) ? order.store.eq(findStoreById(storeId)) : Expressions.TRUE;
}
삭제된 주문 필터링
private BooleanExpression showEq(String show, User loginUser) {
// Customer or Owner
if (isOwner(loginUser) || isCustomer(loginUser)) {
return order.deletedAt.isNull()
.or(order.deletedBy.isNotNull()
.and(order.deletedBy.ne(loginUser))
.and(order.deletedBy.role.isNotNull().and(order.deletedBy.role.ne(UserRole.MASTER))));
}
// 관리자만
if (show == "all") {
return Expressions.TRUE;
}
// 관리자만
if (show == "deleted") {
return order.deletedAt.isNotNull();
}
//
return order.deletedAt.isNull();
}
이때 문제는 삭제된 주문을 필터링 하는 부분에서 발생했다.
order.deletedAt.isNull()까지는 정상적으로 데이터가 조회가 되는데 order.deletedBy.role.ne(UserRole.MASTER) 라는 코드가 들어가는 순간 값이 아무것도 조회되지 않았다.
또한 order.deleteBy.role.isNotNull() 까지만 하면 정상적으로 또 조회가 되었다.
이에 여러가지 시도를 하다, queryDsl 로 만들어진 쿼리가 어떻게 작성되어 전송되는지를 확인했다.
문제 파악
order.deletedBy.role.isNotNull() 까지는 query가 order 자체적으로 가지고 있는 값을 통해 해결했지만 order.deletedBy.role.ne()를 수행하는 순간 user테이블로 join 쿼리가 생성되는 것을 확인했다. 문제 이때 queryDsl의 경우 기본적으로 InnerJoin을 통해 작동하기 때문에 발생했던 문제이다.
Hibernate:
select
o1_0.id,
o1_0.accepted_at,
o1_0.address_id,
o1_0.canceled_at,
o1_0.canceled_by,
o1_0.completed_at,
o1_0.created_at,
o1_0.deleted_at,
o1_0.deleted_by,
o1_0.order_payment_status,
o1_0.rejected_at,
o1_0.status,
o1_0.store_id,
o1_0.total_price,
o1_0.type,
o1_0.updated_at,
o1_0.user_id
from
p_orders o1_0
join
p_users db2_0
on db2_0.id=o1_0.deleted_by
where
o1_0.user_id=?
and true
and true
and true
and true
and (
o1_0.deleted_at is null
or o1_0.deleted_by is not null
and o1_0.deleted_by<>?
and (
db2_0.role is not null
and db2_0.role<>?
)
)
order by
o1_0.created_at desc
offset
? rows
fetch
first ? rows only
이때 작동하는 쿼리를 살펴보면 조인 부분이 InnerJoin(default)로 작동해서 발생하는 문제였다. 즉 deletedBy에는 null값일 때 user 테이블과 서로 겹치는 부분이 존재하지 않기에 해당 주문 정보를 불러오지 못했던 것이다.
해결
따라서 join를 left조인으로 바꿔서 전체 주문에 제약 조건을 걸어주면 해결할 수 있다.
@Transactional(readOnly = true)
public Page<GetOrderResponseDto> getOrders(Long userId, UUID storeId, OrderType type, OrderStatus status, OrderPaymentStatus orderPaymentStatus, String show, Pageable pageable) {
User loginUser = securityUtil.getCurrentUser();
Long loginUserId = loginUser.getId();
log.info("주문 조회 요청 loginUserId: {}, userId: {}, storeId", loginUserId, userId, storeId);
if (isOwner(loginUser) || isCustomer(loginUser)) {
if (show != null) {
throw new AccessDeniedException();
}
}
List<Order> orders = jpaQueryFactory
.selectFrom(order)
.leftJoin(order.deletedBy, QUser.user)
.where(
userEq(userId, loginUser),
storeEq(storeId, loginUser),
typeEq(type),
statusEq(status),
orderPaymentStatusEq(orderPaymentStatus),
showEq(show, loginUser)
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(getSortOrder(pageable))
.fetch();
JPAQuery<Long> countQuery = jpaQueryFactory
.select(order.count())
.leftJoin(order.deletedBy, QUser.user)
.where(
userEq(userId, loginUser),
storeEq(storeId, loginUser),
typeEq(type),
statusEq(status),
orderPaymentStatusEq(orderPaymentStatus),
showEq(show, loginUser)
);
Page<Order> page = PageableExecutionUtils.getPage(orders, pageable, countQuery::fetchOne);
log.info("주문 정보 조회 성공 loginUserId: {}, userId: {}, storeId: {}", loginUserId, userId, storeId);
return page.map(GetOrderResponseDto::new);
}
위와 같이 leftJoin을 추가해주면 성공적으로 데이터를 조회할 숫 있다.
결과
{
"success": true,
"status": "OK",
"path": "/api/orders?userId=1&storeId=null&type=null&status=null&paymentStatus=null&show=null",
"data": {
"content": [
{
"orderId": "7b5ca5fa-974d-491d-b720-4dff427f9a2a",
"username": "sjhty123",
"storeName": "Jane's Bakery",
"orderType": "ONLINE",
"orderMenuList": [
{
"orderMenuId": "074e439c-cb22-48b0-9459-2c41d489caa6",
"menuName": "Red Velvet Cake",
"quantity": 4,
"price": 18000
}
],
"totalPrice": 72000,
"orderStatus": "PENDING",
"orderPaymentStatus": "PENDING",
"canceledBy": null,
"deletedBy": null,
"createdAt": "2024-09-05T08:44:34.28778",
"updatedAt": "2024-09-05T08:44:34.287789",
"acceptedAt": null,
"canceledAt": null,
"rejectedAt": null,
"completedAt": null,
"deletedAt": null
}
],
"page": {
"size": 10,
"number": 0,
"totalElements": 1,
"totalPages": 1
}
}
}
최종
queryDsl을 사용할 때 상황에 따라 다르지만 일단 조회에서는 leftJoin을 사용하는 상황이 많다는 것을 알게되었다.
또한 실제 날아가는 query를 직접 보고 어디에서 문제가 생겼는지 파악하는 것도 굉장히 중요하다.
'트러블 슈팅' 카테고리의 다른 글
| Jpa 테스트 시 TransactionRequiredException 발생과 해결 (0) | 2025.03.16 |
|---|---|
| SpringBoot 버전 업에 따른 RestClient 에러 발생 해결 (0) | 2025.02.16 |
| AWS CodeDeploy 에러 해결, 권한 문제 (0) | 2025.02.13 |
| AWS Fargate에서 Eureka 사용 시 Eureka Client의 IP 추적 문제 (0) | 2025.02.13 |
| 비동기 요청 시 AuditorAware 문제 (0) | 2025.02.13 |