우리가 JPA만 썼을 때는 레포지토리에서 DB테이블을 조회하면 해당 엔티티(or Optional<엔티티>)로 리턴받았다.
그리고 프론트에 해당 엔티티를 넘기기에는 사용자가 볼 필요없는 정보와 같은 것들은 거르고 필요한 데이터만 담은 별도의 객체,
즉 DTO형태로 리턴해줘야 했고 이를 서비스계층에서 조립하였다.
그런데 QueryDSL의 장점 중 하나는 바로 DB테이블을 조회하여 엔티티가 아닌 DTO로 바로 리턴 받을 수 있다.
Projection 방식을 통해서다.
Projection 이란?
QueryDSL은 SQL처럼 결과값이 행(row) 형태로 나타나는데, 이걸 자바 객체에 자동으로 매핑하는 것이 Projection이다.
즉, entity 전체를 가져오는 것이 아니라 조회 대상을 지정해 원하는 값만 조회하는 것으로 쉽게 말해 DTO에 바로 매핑하여 받을 수 있는 것이다.
이 Projction은 4가지 방식을 통해 구현할 수 있다.
(마치 객체 의존성 주입할 때, 생성자 방식, 세터방식, 필드 방식이 있는 것 처럼)
먼저 나는 아래와 같은 DTO로 쿼리 결과를 매핑해서 받으려고 한다.
(내 테이블 설계에서는
id부터 itemPrice까지는 item 테이블에서 가져와야하고, url은 companyAttachment라는 테이블에서 가져와야하는 구조다.)
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ItemDetailResDto {
private Long Id; //아이디
private String itemExplanation; //아이템설명
private String itemName; //아이템명
private String itemPrice; //아이템가격
private String urls; //아이템이미지URL
}
-생성자 방식
select 절안에 Projections.constructor(클래스타입,조회해올 칼럼) 메서드를 쓰는 방식으로,
내부적으로 리플렉션을 사용하고 생성자를 호출하여 조회 결과를 DTO 매칭시킬 수 있다.
다만, 생성자의 파라미터의 순서,타입에서 하나라도 어긋나면 '런타임'시점에서 오류가 발생한다.
그리고 필드명에 매칭하는게 아니라 생성자 매개변수 순서에 대한 매칭이라 향후 리팩토링 시 불편하다는 단점이 있다.
public class ItemRepositoryImpl implements ItemRepositoryCustom{
private final JPAQueryFactory jpaQueryFactory;
public ItemRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
this.jpaQueryFactory = jpaQueryFactory;
}
QItem item = QItem.item;
QCompanyAttachment companyAttachment = QCompanyAttachment.companyAttachment;
@Override
public ItemDetailResDto getItemDetailPractice(Long companyId, Long itemId) {
return jpaQueryFactory
.select(Projections.constructor(ItemDetailResDto.class,
item.id,
item.itemExplanation,
item.itemName,
item.itemPrice,
companyAttachment.imageUrl
))
.from(item)
.join(companyAttachment).on(item.id.eq(companyAttachment.parentId)
.and(companyAttachment.companyParentType.eq(CompanyParentType.ITEM)).and(companyAttachment.delYn.eq(DelYN.N)))
.where(item.id.eq(itemId),(item.companyPage.id.eq(companyId)),(item.delYN.eq(DelYN.N)))
.fetchOne();
}
보면 리턴타입으로 dto를 바로 선언해놓았고, select 절안에 Projection.constructor 함수를 호출한다음
매개변수로 DTO.class와 DTO에 매핑받을 칼럼들을 둔다.
이때 매핑받을 칼럼의 순서가 중요하다. 위에서 언급했듯 생성자 매개변수 순서에 대한 매칭이기 때문이다.
우리가 기존 DTO에 AllArgsContructor 어노테이션을 달았다. 이 어노테이션은 선언한 필드의 순서대로 모든 필드를 매개변수로
갖는 생성자를 초기화하므로 여기서도 필드가 선언된 순서대로 명시한 것이다.
또 보면 JOIN 의 ON절에는 연속된 조건에 한하여 .and()로 메서드체이닝 방식으로 호출하고 있으나,
WHERE절에서는 ,(컴마)로 연속된 조건들을 나열하고 있다.
QueryDSL에서는 JOIN의 ON절에서는 연속된 조건을 나열할 때는 무조건 메서드 체이닝방식으로 가능하고
WHERE조건에서는 똑같이 .and()메서드체이닝방식도 가능하고 ,(컴마)로 나열하는 것도 가능하니 참고 바란다.
그 외에도 DTO로 받는 다른 방식에는 @QueryProjection 방식, 필드 접근방식, 프로퍼티 접근방식이 있으나 이 다른방식에 대해서는 향후에 다시 다뤄보도록 하겠다. 보통은 생성자 방식이 가장 많이 사용되는 듯하다.
DTO 내부에 리스트가 있을 때
그런데 만약 한 아이템에 여러 이미지가 있을 경우 아까 url을 단일 String으로 받는 DTO였지만,
List<Sting>으로 바꾸어야한다.
그럼 다시 아래 DTO에 매핑하여 결과를 받는 쿼리를 다시 짜보자.
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ItemDetailResDto {
private Long Id;
private String itemExplanation;
private String itemName;
private String itemPrice;
private List<String> urls;
}
DTO에 컬렉션(List)를 매핑할 때 쓰이는 핵심문법은 .transfrom(grouBy(...)) 이다.
그리고 아래 groupBy를 쓰고 또 리스트에 매핑시키기 위해서는 아래와 같은 import가 필수다
import static com.querydsl.core.group.GroupBy.groupBy;
import static com.querydsl.core.group.GroupBy.list;
public class ItemRepositoryImpl implements ItemRepositoryCustom{
private final JPAQueryFactory jpaQueryFactory;
public ItemRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
this.jpaQueryFactory = jpaQueryFactory;
}
QItem item = QItem.item;
QCompanyAttachment companyAttachment = QCompanyAttachment.companyAttachment;
@Override
public ItemDetailResDto getItemDetail(Long companyId, Long itemId) {
return jpaQueryFactory
.from(item)
.join(companyAttachment)
.on(item.id.eq(companyAttachment.parentId)
.and(companyAttachment.companyParentType.eq(CompanyParentType.ITEM)))
.where(item.id.eq(itemId),(item.companyPage.id.eq(companyId)),(item.delYN.eq(DelYN.N)))
.transform(
groupBy(item.id).as(
Projections.constructor(ItemDetailResDto.class,
item.id,
item.itemExplanation,
item.itemName,
item.itemPrice,
list(companyAttachment.imageUrl)
)
)
)
.get(itemId);
}
}
-.transform(...)은 QueryDSL에서 결과 변환기의 역할을 한다.
단순 결과를 가져오는 fetch()와 달리 결과를 특정 형태(Map,DTO,컬렉션)으로 변환시키는 것이다.
그리고 transfrom은 항상 groupBy와 짝궁같이 사용된다.
-groupBy(item.id)는 "아이템 아이디 컬럼(키) 기준으로 결과를 그룹핑해서 하나의 객체에 묶어라'라는 의미다.
그러면 다시 위의 코드를 보면 item.id 기준으로 묶고 as(...)로 그룹핑된 결과를 DTO로 만든다.
-list(companyAttachment.imageUrl)로 item.id에 연결된 모든 이미지들을 리스트로 모은다.
.get(itemId) - > itemId 키 값에 해당하는 DTO하나를 꺼낸다.
즉, 동일한 item.id에 연결된 여러 companyAttachment.imageUrl 값을 하나의 리스트로 모을 준비를 하는 것이다.
*참고

'스프링부트 > JPA' 카테고리의 다른 글
| [Spring] QueryDSL로 서브쿼리 사용-JPAExpressions (0) | 2025.11.25 |
|---|---|
| [Spring] Query DSL 문법 정리-2 (0) | 2025.09.04 |
| [Spring] Query DSL 문법 정리 (1) | 2025.08.26 |
| [Spring] JPA에서의 프록시 객체-불필요한 SELECT 쿼리 최소화 방법 (2) | 2025.08.10 |
| [Spring] MyBatis 사용법 - JPA와 차이점을 중심으로 (4) | 2025.07.30 |