QueryDSL의 Projections는 쿼리 결과를 특정 DTO로 매핑하거나 새로운 객체로 반환할 때 사용됩니다. 이는 주로 JPQL 또는 Hibernate의 결과를 직접 다루는 복잡함을 줄이고, 타입 안전성을 제공하며, DTO 기반으로 데이터를 처리해야 하는 상황에서 유용합니다.
다음은 QueryDSL에서 Projections를 사용하는 방법과 동작 방식에 대한 설명입니다.
엔티티가 아닌 DTO(Data Transfer Object)에 쿼리 결과를 직접 매핑하기 위해 사용합니다.
엔티티 노출을 피하고 필요한 데이터만 선택적으로 반환하여 성능 최적화 및 보안 강화에 도움을 줍니다.
복잡한 JPQL 쿼리를 대체하면서 가독성과 유지보수성을 높입니다.
JavaBeans 표준을 기반으로 DTO 객체를 생성합니다.
DTO 클래스에 기본 생성자와 setter 메서드가 필요합니다.
QUser user = QUser.user;
List<UserDTO> result = queryFactory
.select(Projections.bean(UserDTO.class,
user.id,
user.name,
user.email
))
.from(user)
.fetch();
DTO 클래스의 필드에 직접 값을 매핑합니다.
기본 생성자가 필요하지 않고, 필드에 직접 접근하는 방식으로 동작합니다.
List<UserDTO> result = queryFactory
.select(Projections.fields(UserDTO.class,
user.id.as("userId"), // 별칭 매핑
user.name,
user.email
))
.from(user)
.fetch();
DTO 클래스의 생성자를 사용하여 매핑합니다.
DTO에 해당하는 생성자가 반드시 존재해야 하며, 생성자 매개변수의 타입과 순서가 일치해야 합니다.
List<UserDTO> result = queryFactory
.select(Projections.constructor(UserDTO.class,
user.id,
user.name,
user.email
))
.from(user)
.fetch();
별칭이 일치하지 않을 때 사용하는 유틸리티 메서드입니다.
필드 이름이 다를 경우 수동으로 매핑할 수 있습니다.
List<UserDTO> result = queryFactory
.select(Projections.fields(UserDTO.class,
user.id.as("userId"),
ExpressionUtils.as(
JPAExpressions.select(order.count())
.from(order)
.where(order.userId.eq(user.id)),
"orderCount" // DTO 필드 이름
)
))
.from(user)
.fetch();
생성된 쿼리 : SELECT
user.id AS userId,
(SELECT COUNT(order.id)
FROM order
WHERE order.user_id = user.id) AS orderCount
FROM
user
Expression<타입> 객체명 = ExpressionUtils.as(대상, "별칭");
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.ExpressionUtils;
QUser user = QUser.user;
// 사용자 이름에 별칭 부여
Expression<String> userNameAlias = ExpressionUtils.as(user.name, "userNameAlias");
// 사용 예
JPAQuery<?> query = new JPAQuery<>(entityManager);
query.select(userNameAlias)
.from(user)
.fetch();
생성된 쿼리 : SELECT user.name AS userNameAlias
FROM user
Expression<타입> 객체명 = ExpressionUtils.as(서브쿼리, "별칭");
QOrder order = QOrder.order;
QUser user = QUser.user;
// 서브쿼리 생성
JPASubQuery subQuery = new JPASubQuery();
Expression<Long> orderCount = subQuery.select(order.count())
.from(order)
.where(order.user.id.eq(user.id));
// 서브쿼리에 별칭 부여
Expression<Long> orderCountAlias = ExpressionUtils.as(orderCount, "orderCountAlias");
// 메인 쿼리
JPAQuery<?> query = new JPAQuery<>(entityManager);
query.select(user.name, orderCountAlias)
.from(user)
.fetch();
생성된 쿼리 : SELECT user.name,
(SELECT COUNT(order.id) FROM order WHERE order.user_id = user.id) AS orderCountAlias
FROM user
타입 안정성: 컴파일 시점에 오류를 감지할 수 있어 안전한 코드 작성이 가능합니다.
가독성: 복잡한 JPQL에 비해 간결하고 직관적인 쿼리 작성이 가능합니다.
유연성: 필요한 필드만 선택적으로 가져올 수 있습니다.
Projections.constructor를 사용할 때 생성자 매개변수가 정확히 맞지 않으면 런타임 오류가 발생할 수 있습니다.ExpressionUtils.as를 통해 수동 매핑을 처리해야 합니다.REST API에서 필요한 데이터만 DTO로 반환할 때.
엔티티의 필드를 모두 가져올 필요 없이 일부 데이터만 가져와 성능 최적화를 원할 때.
복잡한 쿼리 결과를 DTO에 직접 매핑하여 비즈니스 로직과 분리하고자 할 때.
Projections를 적절히 활용하면 QueryDSL로 효율적인 데이터 조회와 DTO 매핑이 가능합니다. 필요한 상황에 맞게 bean, fields, constructor 방식을 선택하면 됩니다.
