Querydsl - 중급 문법 - 1 프로젝션과 결과 반환
인프런 실전! Querydsl 강의 내용 정리
프로젝션과 결과 반환 - 기본
프로젝션 : select 대상 지정
프로젝션 대상이 하나
1
2
3
4
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
- 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음
- 프로젝션 대상이 둘 이상이라면 튜플이나 DTO로 조회
튜플 조회
프로젝션 대상이 둘 이상일 때 사용
com.querydsl.core.Tuple
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void tupleProjection() throws Exception {
//when
List<Tuple> fetch = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
//then
for (Tuple tuple : fetch) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
System.out.println("username = " + username);
System.out.println("age = " + age);
}
}
다음과 같이 쓸 수 있다.
Tuple은 리파지토리단 안에서만 사용하자.
밖으로 넘어갈 땐 DTO로 변환해서 넘기자.
프로젝션과 결과 반환 - DTO 조회
순수 JPA 에서 DTO 조회 방법
일단 MemberDto 생성
1
2
3
4
5
6
7
8
9
10
11
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
그 다음 순수 JPQL로 작성해 보면
1
2
3
4
5
6
7
8
9
10
@Test
public void findDtoByJPQL() throws Exception {
List<MemberDto> resultList = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age)" +
" from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : resultList) {
System.out.println("memberDto = " + memberDto);
}
}
select new study.querydsl.dto.MemberDto(m.username, m.age)
다음과 같이 dto를 생성자로 세팅 하듯이 만들어야 함.

그럼 이렇게 select 쿼리에서도 딱 username, age만 나가게 됨.
그런데 이건 좀 .. ㅋㅋ
패키지명 다 적고 그런게 좀 그렇다.
- 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야 함.
- DTO의 패키지 이름을 다 적어줘야 해서 지저분함
- 생성자 방식만 지원함
Querydsl 빈 생성(Bean population)
결과를 DTO로 반환할 때 사용
다음 3가지 방법을 지원
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
프로퍼티 접근
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void findDtoBySetter() throws Exception {
List<MemberDto> fetch = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : fetch) {
System.out.println("memberDto = " + memberDto);
}
}
다음과 같이 select 절에 Projections.bean() 을 사용해서 MemberDto.class(타입)를 불러오고
그 다음 내가 원하는 값 username, age를 차례로 적어주면 된다.
하지만 이렇게만 해놓고 돌려보면 에러가 나온다.

원인은 MemberDto에 기본 생성자를 안 만들어 줬기 때문이다.
@NoArgsConstructor 나 기본 생성자를 만들어 주자.

그 후 다시 돌려보면 정상적으로 동작한다.
필드 직접 접근
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void findDtoByField() throws Exception {
List<MemberDto> fetch = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : fetch) {
System.out.println("memberDto = " + memberDto);
}
}
Projections.fields() 얘는 Setter 필요 없이 그냥 필드 변수를 바꿔버린다.
생성자 접근 방식
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void findDtoByConstructor() throws Exception {
List<MemberDto> fetch = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : fetch) {
System.out.println("memberDto = " + memberDto);
}
}
Projections.constructor() 이걸 사용하면 되고
여기서 주의점은 member.username, member.age 이런 걸 생성자와 동일하게 넣어줘야 한다.
기타 주의 사항
만약에 UserDto를 다음과 같이 만들었다 쳐보자.
1
2
3
4
5
@Data
public class UserDto {
private String name;
private int age;
}
이제 필드 주입을 해서 UserDto로 반환할 거다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void findUserDtoByField() throws Exception {
List<UserDto> fetch = queryFactory
.select(Projections.fields(UserDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (UserDto userDto : fetch) {
System.out.println("userDto = " + userDto);
}
}
다음과 같이 하고 돌려 보자.

보이는가? name = null 이 들어간다.
원인은 우리가 select 절에 member.username 이렇게 넣었기 때문이다.
따라서 방법은 다음과 같다.
member.username.as("name")
이렇게 수정하고 다시 돌려보자

이제 원하는 대로 동작하게 된다.
기타 주의 사항 - 2 서브 쿼리
만약에 age에 최대 값을 구하고 싶어서 서브 쿼리를 사용한다면?
서브 쿼리 as ‘age’ 뭐 이런 식으로 해야 할 것이다.
코드로 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void findUserDtoByFieldSubQuery() throws Exception {
QMember memberSub = new QMember("memberSub");
List<UserDto> fetch = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")))
.from(member)
.fetch();
for (UserDto userDto : fetch) {
System.out.println("userDto = " + userDto);
}
}
다음과 같이 작성해야 한다.
age 부분만 따로 보면
1
2
3
ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age"))
ExpressionUtils.as()라는 메서드 안에 이전에 배운 서브 쿼리 생성 방법 JPAExpressions 을 사용해서 서브 쿼리를 만들고 그 후 마지막에 alias 로 "age" 를 넣어 줬다.

결과가 최대 값 40으로 잘 나왔다.
- 프로퍼티나 필드 접근 생성 방식에서 이름이 다를 때 해결 방안
ExpressionUtils.as(source, alias): 필드 혹은 서브 쿼리에 별칭 사용username.as("memberName"): 그냥 필드에 별칭 사용
기타 주의 사항 - 3 생성자 주입
1
2
3
4
5
6
7
8
9
10
11
12
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
생성자를 만들어 놓고
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void findUserDtoByConstructor() throws Exception {
List<UserDto> fetch = queryFactory
.select(Projections.constructor(UserDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (UserDto userDto : fetch) {
System.out.println("userDto = " + userDto);
}
}
다음 코드를 돌려 보면

잘 된다. 이름은 굳이 상관 없고, 생성자의 타입 이랑 순서 가 중요하기 때문에
1
2
3
4
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
여기에 맞게 잘 들어 간다.
결론
- 프로퍼티 접근, 필드 접근은 이름을 항상 맞춰줘야 한다.
- 생성자 접근은 이름은 맞출 필요 없지만,
기본 생성자가 있어야 하고, 순서와 타입을 잘 맞추자.
댓글남기기