select 절에 조회 대상을 지정하는 것을 프로젝션이라 한다.
QItem item = QItem.item;
List<String> result = query.from(item).list(item.name);
//query.select(item.name).from(item).fetch();
//list는 내부적으로 select + fetch역할을 한다
for(String name : result) {
System.out.println("name = " + name); }
| 용어 | 의미 |
|---|---|
| QItem.item | Item이라는 엔티티를 기준으로 한 쿼리용 클래스 |
| item.name | Item 엔티티의 name 필드 (Q클래스의 StringPath) |
| from(item) | FROM item절 구성 |
| list(item.name) | SELECT item.name → 그리고 List<String>으로 결과 받음 |
| 프로젝션 | 쿼리 결과에서 어떤 필드만 선택적으로 조회할 것인지 지정하는 것 |
| 쿼리 | 의미 |
|---|---|
selectFrom(item) | 전체 엔티티 Item을 반환 |
select(item.name) | name 필드 하나만 반환 (→ List<String>) |
select(Projections.constructor(...)) | 여러 필드를 묶어서 DTO로 반환 |
프로젝션 대상으로 여러 필드를 선택하면 querydsl은 기본으로 Tuple이라는 Map과 비슷한 내부 타입을 사용한다. 조회 결과는 tuple.get() 메소드에 조회한 쿼리 타입을 지정하면 된다.
QItem item = QItem.item;
List<Tuple> result = query.from(item).list(item.name, item.price);
//List<Tuple> result = query.from(item).list(new QTuple(item.name, item.price));
//같다
for (Tuple tuple : result) {
System.out.println("name = " + tuple.get(item.name));
System.out.println("price = " + tuple.get(item.price));
}
쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶으면 빈 생성 기능을 사용한다.
querydsl은 객체를 생성하는 다양한 방법을 제공한다.
프로퍼티 접근
필드 직접 접근
생성자 사용
원하는 방법을 지정하기 위해 com.mysema.query.types.Projection을 사용하면 된다.
public class ItemDto {
private String username;
private int price;
public ItemDto() {}
public ItemDTO(String username, int price){
this.username = username;
this.price = price;
}
//Getter, Setter
}
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.bean(ItemDTO.class, item.name.as("username"),item.price));
item.name.as("username") -> DB에서 name을 조회하되, 결과 컬럼 이름을 username으로 alias를 준다
Projections.bean()은:
ItemDto dto = new ItemDTO();// 기본생성자 호출
dto.setUsername(조회한 name 값)
dto.setPrice(조회한 price 값)
이처럼 setter를 통해 필드를 주입해준다
setter가 “직접 호출”되는 게 아니라, QueryDSL이 내부에서 리플렉션으로 자동 호출.
Projections.bean() 메서드는 setter를 사용해서 값을 채운다.
예제를 보면 쿼리 결과는 name인데 ItemDTO는 username 프로퍼티를 가지고 있다. 이처럼 쿼리 결과와 매핑할 프로퍼티 이름이 다르면 as를 사용해서 별칭을 주면 된다.
"프로퍼티 접근"의 의미는?
“Setter 메서드를 이용해서 값을 집어넣는다”는 뜻.
이걸 프로퍼티(property) 기반 접근 방식이라고 부름.
필드에 직접 접근하는 게 아니라 → setUsername(...), setPrice(...) 같은 setter 메서드를 호출
빈(bean)은 자바에서 getter/setter가 있는 순수 자바 객체(POJO)를 말한다
기본 생성자 + Setter를 가진 객체 = 자바 빈(bean)
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.fields(ItemDTO.class, item.name.as("username"),item.price));
필드를 private으로 설정해도 동작한다.
QItem item = QItem.item;
List<ItemDTO> result = query.from(item).list(
Projections.constructor(ItemDTO.class, item.name,item.price));
지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요하다.
@QueryProjection 을 통해서, DTO 를 Q-Type 으로 생성할 수 있다.
전제(DTO 클래스에 @QueryProjection 추가)
@Getter
public class ItemDto {
private String name;
private int price;
@QueryProjection
public ItemDto(String name, int price) {
this.name = name;
this.price = price;
}
}
Q타입 생성 (Gradle 설정 필요)
./gradlew compileQuerydsl
→ QItemDto 생성됨
QueryDSL 사용 예
QItem item = QItem.item;
List<ItemDto> result = queryFactory
.select(new QItemDto(item.name, item.price)) // 컴파일 타임 체크 O
.from(item)
.fetch();
장점
타입/순서 정확히 안 맞으면 컴파일 오류 발생
IDE 자동완성 가능
성능도 좋음 (리플렉션 X)
Projections.constructor(...)는 리플렉션(Constructor.newInstance())을 사용하여 런타임 비용이 발생
반면, @QueryProjection은 직접 new QDto(...) 생성자 호출이므로 리플렉션 없이 JVM이 최적화 가능한 코드
| 단점 | 설명 |
|---|---|
| QueryDSL 의존성 생김 | DTO가 @QueryProjection을 사용하면 QueryDSL 라이브러리에 종속됨 (→ DTO가 순수하지 않음) |
| Q타입을 매번 생성해야 함 | DTO를 바꿀 때마다 ./gradlew compileQuerydsl로 QDto 재생성 필요 |
| 빌드 속도 저하 | Q타입 생성 과정이 느리진 않지만, 전체 빌드 시간에 영향 |
| 레이어 침범 가능성 | DTO가 query layer 의 구현 디테일을 알게 됨 → 계층 분리 원칙에 위배될 수 있음 |
| 테스트/공유 어려움 | DTO를 다른 모듈/프로젝트에서 재사용할 때 QueryDSL 의존성도 따라가야 함 |
| 상황 | 추천 방식 |
|---|---|
| 내부 전용 DTO, 타입 안정성 중요 | @QueryProjection |
| 외부 API 응답 DTO, 계층 분리 중요 | Projections.constructor or Mapper 사용 |
| 팀 프로젝트에서 통일성 유지 | @QueryProjection, 단 QDto는 querydsl 전용으로만 쓰고 외부에는 노출 안 함 |
| 프로젝션 방식 | 동작 방식 | 값 주입 방식 | 리플렉션 사용 여부 | 타입 안정성 | 1차 캐시 등록 여부 | 특징 및 주의사항 |
|---|---|---|---|---|---|---|
| 인터페이스 기반 | Getter 기반으로 값 주입 (getName()) | 스프링이 프록시 객체 생성 | 없음 | 높음 | DTO라서 제외 | 가장 가볍고 성능 좋음, 단순 조회에 적합 |
DTO 생성자 기반 (Projections.constructor) | 생성자 리플렉션으로 호출 (newInstance) | 생성자 리플렉션 호출 | 사용 | 낮음 | 제외됨 | 인자 순서 틀리면 런타임 오류 발생 |
프로퍼티 접근 (Projections.bean) | setter 메서드 호출 | Method.invoke()로 setter 호출 | 사용 | 낮음 | 제외됨 | Setter 필수, 필드명 중요 |
필드 직접 접근 (Projections.fields) | 필드에 직접 값 주입 | Field.set() 호출 | 사용 | 낮음 | 제외됨 | Setter 없어도 되나 느림 |
@QueryProjection (new QDto(...)) | Q타입 직접 생성 | 명시적 생성자 호출 (new) | 없음 | 매우 높음 | 제외됨 | 타입 체크 완벽, 유지보수에 강함 |
| 튜플(Tuple) | select()로 여러 필드 묶음 | 내부적으로 Tuple 객체로 저장 | 없음 | 없음 | 제외됨 | 타입 안정성 없음, 단순 결과 |
단일 필드 조회 (list(item.name)) | 단일 값만 바로 반환 | 값 자체 (ex: String, Long) | 없음 | 높음 | 제외됨 | 가장 단순하고 가벼움 |
Projections.*() 는 대부분 리플렉션을 쓰고, QDto는 쓰지 않는다.
JPA는 엔티티를 조회할 때만 영속성 컨텍스트에 등록한다.
DTO나 Tuple 등은 엔티티가 아니기 때문에 1차 캐시에 등록되지 않음.
인터페이스 기반 프로젝션은 QueryDSL이 Map 기반 데이터에서 인터페이스를 구현한 프록시 객체를 만들어 반환하는 구조.
그래서 컴파일러가 인터페이스 메서드 유효성 체크를 해주고, 런타임엔 단순한 getter 호출로 작동하므로 안정성 + 성능 둘 다 잡을 수 있는 방식.
Q: 인터페이스 기반과 단일 필드 조회도 컴파일 시점에 타입 체크되는데, 왜 굳이 QDto를 또 만들어서 써야 하나요?
인터페이스 프로젝션은 "컴파일 타임 타입 안정성"이 아닌, "컴파일 시점의 인터페이스 구조의 제약" 때문에 어느 정도의 타입 안정성을 제공한다는 것일 뿐. 즉, 완전한 타입 안정성은 아니다.
| 방식 | 컴파일 시 체크 가능한가? | 얼마나 안전한가? | 근거 |
|---|---|---|---|
| 단일 필드 조회 | O | 매우 안전 | List<String> 등 구체 타입을 지정하므로 완벽히 타입 체크 |
| 인터페이스 기반 | O (getXxx 메서드 존재 여부) | 비교적 안전 | 메서드명과 반환 타입이 컴파일 시점에 확인됨 |
| @QueryProjection (QDto) | O | 최고 수준 | 생성자 파라미터의 순서/타입까지 컴파일러가 검사 |
| 방식 | 매핑 방식 | 에러 발생 시점 | 자동완성 지원 | 구조 변경 시 위험도 |
|---|---|---|---|---|
| 단일 필드 | 직접 값 반환 | 컴파일 시 | O | 없음 |
| 인터페이스 기반 | 프록시 객체 + 메서드 기반 | 컴파일 시 | O | 비교적 낮음 |
Projections.constructor | 생성자 리플렉션 | 런타임 | X | 높음 (순서 바뀌면 죽음) |
| @QueryProjection | Q타입 직접 생성 | 컴파일 시 | (IDE 지원) | 거의 없음 (변경 감지 강함) |
| 이유 | 설명 |
|---|---|
| 1. 생성자 타입/순서 강제 체크 | 잘못된 순서나 타입이면 컴파일 에러 발생 (실수 방지) |
| 2. 리플렉션 없음 → 성능 좋음 | new QDto(...)로 직접 생성 → 런타임 비용 없음 |
| 3. IDE 자동완성 + 리팩토링 안정성 | DTO 생성자 변경 시 사용 코드도 컴파일 에러 발생 → 유지보수 용이 |
| 4. 복잡한 매핑 구조 대응 | 서브쿼리, 연산 필드 등 다양한 필드를 안전하게 처리 가능 |
"런타임에 리플렉션이 알아서 작동해서, 기본 생성자를 호출하고, setter로 값을 채운다"
예:
List<ItemDto> result = query
.select(Projections.bean(ItemDto.class, item.name.as("username"), item.price))
.from(item)
.fetch();
QueryDSL이 SQL을 생성
SELECT item.name AS username, item.price FROM item
즉, DB에 name, price 컬럼만 조회
결과가 ResultSet 형태로 JDBC를 통해 넘어옴
QueryDSL 내부에서 리플렉션을 사용하여
ItemDto dto = new ItemDto(); ← 기본 생성자 호출 (newInstance())dto.setUsername("홍길동"); ← invoke()로 setter 호출dto.setPrice(3000);이렇게 만들어진 ItemDto 객체가 List로 수집됨
invoke() invoke()는 리플렉션으로 가져온 메서드를 실제 실행하는 메서드
Method m = clazz.getMethod("setUsername", String.class);
m.invoke(dto, "홍길동"); // dto.setUsername("홍길동") 과 같음
(정적 메서드 호출에도 쓰이고, 객체 메서드에도 쓰임)
| 장점 | 단점 |
|---|---|
| 런타임에 동적으로 객체 생성/조작 가능 | 느리고 복잡하며 실수하면 런타임 에러 |
| 프레임워크 유연성에 필수 (Spring, Hibernate 등) | 컴파일 타임 타입 체크가 안 됨 |