Querydsl Alias 사용 시 주의사항 및 최적화 방법
Querydsl은 자바 기반의 타입 안전한 SQL 빌더로, 복잡한 쿼리를 쉽게 작성할 수 있도록 도와줍니다.
그러나, Querydsl을 사용할 때, 특히 여러 엔티티를 조인(join)하는 상황에서는 주의해야 할 사항들이 있습니다.
이 글에서는 그중에서도 조인을 명시했음에도 불구하고 불필요한 조인이 한 번 더 나가는 경우와 관련된 문제와 해결 방법에 대해 다루겠습니다.
1. 잘못된 조인으로 인한 성능 저하 문제
예시에 활용될 엔티티 설명
우선, 이 예시에서 사용할 엔티티들을 소개하겠습니다. 이 엔티티들은 OrderItem, Goods, Importer, OrderHistory로 구성되어 있으며, 각 엔티티의 역할과 관계를 간단히 설명하겠습니다.
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "goods_id")
private Goods goods; // 상품과의 일대일 관계
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_history_id")
private OrderHistory orderHistory; // 주문 기록과의 다대일 관계
// 기타 필드 및 메서드 생략
}
public class Goods {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "importer_id")
private Importer importer; // 제조사와의 다대일 관계
// 기타 필드 및 메서드 생략
}
public class Importer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 기타 필드 및 메서드 생략
}
public class OrderHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 기타 필드 및 메서드 생략
}
엔티티 설명:
- OrderItem (주문 아이템)
- 고객이 주문한 각 상품을 나타내는 엔티티입니다.
- OrderItem은 하나의 Goods(상품)와 연결되어 있으며(@OneToOne 관계), 여러 개의 OrderItem이 하나의 OrderHistory(주문 기록)에 속할 수 있습니다(@ManyToOne 관계).
- Goods (상품)
- 고객에게 판매되는 개별 상품을 나타냅니다.
- Goods는 하나의 Importer(제조사)와 연결되어 있습니다(@ManyToOne 관계).
- Importer (제조사)
- 상품을 제조하는 회사나 브랜드를 나타내는 엔티티입니다.
- 하나의 Importer는 여러 개의 Goods와 연결될 수 있습니다.
- OrderHistory (주문 기록)
- 고객이 주문한 내역을 기록하는 엔티티입니다.
- 하나의 주문 내역에는 여러 개의 OrderItem이 포함될 수 있습니다.
이러한 관계들을 이해하면, Querydsl을 통해 작성되는 쿼리에서 엔티티들이 어떤 순서로 어떻게 조인되는지 쉽게 파악할 수 있습니다.
1-1. 선언한 엘리어스를 일관되게 사용하기
Querydsl을 사용할 때, 조인한 엔티티에 대해 별칭(alias)을 선언한 경우, 이후의 select, where, join 절에서 반드시 이 별칭을 일관되게 사용해야 합니다.
그렇지 않으면 불필요한 조인이나 크로스 조인(cross join)이 발생할 수 있습니다.
다음은 잘못된 예시입니다.
queryFactory
.select(orderItem.goods.importerBrandName) // 잘못된 접근 방식
.from(orderItem)
.innerJoin(orderItem.goods, goods) // goods 별칭 선언
위 코드에서는 innerJoin(orderItem.goods, goods)로 goods라는 별칭을 선언했지만, select 절에서 orderItem.goods.importerBrandName을 사용함으로써, 별칭을 무시하고 원래 엔티티를 다시 접근하고 있습니다.
이로 인해, goods 엔티티에 대한 불필요한 조인이 추가로 발생합니다.
성능 저하를 초래할 수 있으므로, 선언한 별칭을 반드시 사용해야 합니다.
다음은 올바른 사용 예시입니다.
queryFactory
.select(goods.importerBrandName) // 올바른 접근 방식
.from(orderItem)
.innerJoin(orderItem.goods, goods) // goods 별칭 선언
이처럼, 별칭을 일관되게 사용함으로써, 쓸데없는 조인 절이 추가되는 것을 방지할 수 있습니다.
2. 다중 조인 시 주의사항
다음으로, Goods 엔티티가 Importer라는 엔티티와 조인되어 있는 경우를 가정해보겠습니다.
이때, OrderItem을 통해 Goods를 조인하고, 이어서 Goods를 통해 Importer를 조인하는 상황을 생각해볼 수 있습니다.
잘못된 예시를 먼저 보겠습니다
queryFactory
.innerJoin(orderItem.goods) // 1번 조인
.innerJoin(orderItem.goods.importer) // 2번 조인
위 코드는 첫 번째 줄에서 goods를 조인하고, 두 번째 줄에서 다시 goods.importer를 조인합니다.
그러나, Querydsl은 두 번째 innerJoin(orderItem.goods.importer)에서 이미 조인된 goods를 재사용하지 않고, 새로운 goods 조인을 생성해버립니다.
결과적으로, goods가 두 번 조인되고, 성능에 불리한 영향을 미칠 수 있습니다.
다음은 올바른 조인 방법입니다.
queryFactory
.innerJoin(orderItem.goods, goods) // 1번 조인 및 별칭 선언
.innerJoin(goods.importer, importer) // 2번 조인
이 방법에서는 goods에 대한 별칭을 재사용하므로, 불필요한 조인이 발생하지 않습니다.
이처럼 별칭을 활용하면 on 절을 명시적으로 작성하지 않아도 되어 더욱 깔끔한 쿼리를 작성할 수 있습니다.
3. 전체 쿼리 예시
마지막으로, 모든 주의사항을 반영한 실제 쿼리 예시를 보겠습니다.
아래는 복수의 엔티티를 조인하여 복잡한 조회를 수행하는 쿼리입니다.
List<OrderExcelResponse> excelList = queryFactory
.select(
new QOrderExcelResponse(
orderItem.id,
orderHistory.orderNo,
orderItem.registedDatetime,
retailStore.companyName,
retailStore.businessNumber,
goods.importerBrandName,
goods.goodsNo,
orderItem.goodsName,
orderItem.capacity,
orderItem.containerType,
orderItem.alcoholType,
orderItem.boxBarcode,
orderItem.packageBarcode,
orderItem.singleBarcode,
orderItem.saleUnitCount,
orderItem.count,
orderItem.orderStatus
))
.from(orderItem)
.innerJoin(orderItem.orderHistory, orderHistory)
.innerJoin(orderItem.goods, goods)
.innerJoin(goods.importer, importer)
.innerJoin(orderHistory.saleUser, chainMarket)
.innerJoin(orderHistory.orderUser, retailStore)
.leftJoin(orderExcelDownloadHistory)
.on(orderExcelDownloadHistory.orderHistory.eq(orderHistory))
.leftJoin(orderExcelDownloadHistory.account, account)
.where(
account.id.eq(currentUser.getId()),
orderExcelDownloadHistory.orderHistory.id.isNull(),
orderItem.orderStatus.ne(OrderStatus.주문취소)
)
.fetch();
위 쿼리에서는 각 엔티티를 조인할 때 일관된 별칭을 사용하고 있으며, 불필요한 조인이 발생하지 않도록 주의하고 있습니다.
또한, where 절에서 필터링 조건을 적용하여 원하는 데이터를 효율적으로 조회할 수 있습니다.
마무리
Querydsl을 사용하여 복잡한 SQL 쿼리를 작성할 때는, 조인 별칭에 대한 주의가 필요합니다.
별칭(alias)을 일관되게 사용하지 않으면 불필요한 조인으로 인한 성능 저하가 발생합니다.