[JPA] QueryDSL DTO 반환 방법
0. 관련 포스팅
- QueryDSL 설정하기
- QueryDSL DTO 반환 방법 <현재 포스팅>
- QueryDSL 동적 쿼리 적용
- QueryDSL 사용자 정의 레포지토리 적용
- QueryDSL 페이징 적용
1. 개요
JPA나 JPA Repository, QueryDSL 등 어떤 조회를 하든 DTO 로 반환해야하는 경우가 발생할 수 있다.
QueryDSL 로 DTO를 반환할 때 사용하는 방법에 대한 글이다.
(해당 포스팅은 김영한 님의 QueryDSL 강의를 보고 검색한 내용과 함께 정리한 글입니다.)
2. 구현
구현 방법에는
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
- @QueryProjection 사용
방법이 있다.
DTO 반환 시,
기본 생성자로 먼저 객체 생성으로 객체를 만들기 때문에 만들어주어야한다.
(생성자 사용이나, @QueryProjection 시에는 매핑 방식이 다르기 때문에 기본 생성자가 없어도 되었다.)
-. DTO
테스트 구현에 앞서 우리가 사용할 DTO 클래스에 대한 내용이다.
MemberDto (프로퍼티, 필드, 생성자 테스트용)
@Getter
@Setter // Setter 방식을 이용할 경우 필요! ex Projections.bean()
@NoArgsConstructor // DTO 반환 시, 기본 생성자로 먼저 생성하기 때문에 필요!
@ToString
public class MemberDto {
private Long id;
private String name;
private Integer age;
public MemberDto(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
UserDto (Alias 테스트용)
@Getter
@NoArgsConstructor
@ToString
public class UserDto {
private Long id;
private String username;
private Integer age;
public UserDto(Long id, String username, Integer age) {
this.id = id;
this.username = username;
this.age = age;
}
}
PreMemberDto (@QueryProjection 테스트용)
@Getter
@NoArgsConstructor
@ToString
public class PreMemberDto {
private Long id;
private String name;
private Integer age;
@QueryProjection
public PreMemberDto(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
a. 프로퍼티 접근
이 방법은 클래스 파일 내 Setter 를 읽어들여서 매핑하는 방법이다.
앞서 MemberDto 클래스에 Lombok으로 @Setter를 세팅해둔 상태이다.
@DisplayName("Setter 매핑 테스트")
@Test
void dtoPropertyTest() {
// given
// when
List<MemberDto> results = query.select(Projections.bean(MemberDto.class,
member.name,
member.age))
.from(member)
.fetch();
// then
for (MemberDto result : results) {
System.out.println(result.toString());
}
}
이때 Projections.bean() 메소드를 사용하는데,
클래스의 Setter 가 매핑하려는 프로퍼티의 이름과 같게 존재하지 않는다면 매핑이 되지 않는다.
정상적으로 조회 후 매핑되는 형태는 아래와 같이 매핑한 name과 age의 값이 표기된다.
만약 Setter의 이름을 변경한다면?
매핑이 되지 않을지, 에러가 발생할지 확인해보았는데,
에러가 발생하지는 않았지만 로그를 보면 name 이 null 값으로 매핑이 되지 않은 것을 볼 수 있었다.
코드를 작성 시에는 확인이 안되고 Runtime 시에 확인이 되는 특징이 볼 수 있는데,
이 부분에 대해서는 좀 더 아래부분에서 한번 더 보도록 하겠다.
b. 필드 직접 접근
필드 접근 방법은 DTO 클래스의 필드 명과 일치하면 매핑되는 구조를 가지고 있다.
Projections.field() 메소드를 이용한다.
@DisplayName("필드 매핑 테스트")
@Test
void dtoFieldsTest() {
// given
// when
List<MemberDto> results = query.select(Projections.fields(MemberDto.class,
member.name,
member.age))
.from(member)
.fetch();
// then
for (MemberDto result : results) {
System.out.println(result.toString());
}
}
필드명을 name -> username 으로 변경하고
테스트하였을 때는 null 값으로 반환되는 모습을 볼수 있다.
c. 생성자 사용
이번에는 생성자를 사용하는 방법이다.
Projections.constructor() 메소드를 이용한다.
@DisplayName("Construct 매핑 테스트")
@Test
void dtoConstructTest() {
// given
// when
List<MemberDto> results = query.select(Projections.constructor(MemberDto.class,
member.id,
member.name,
member.age))
.from(member)
.fetch();
// then
for (MemberDto result : results) {
System.out.println(result.toString());
}
}
이 방법은 생성자를 사용하기 때문에,
생성자의 Args에 해당하는 값들을 넣어줘야한다.
만약 *위에서 만든 코드에서 id값을 빼고 넣는다면 에러를 발생시킨다.
d. Alias
이 방법은 조회하려는 필드값과 이름이 다른 경우이다.
앞서 테스트 한 MemberDto 클래스가 아닌 UserDto 클래스를 만들어
name 대신 username으로 필드 명을 주었고
SubQuery를 사용하여 age의 max 값을 age로 매핑하도록 하였다.
@DisplayName("Alias 매핑 테스트")
@Test
void dtoAliasTest() {
// given
QMember memberSub = member;
// when
List<UserDto> results = query.select(Projections.fields(UserDto.class,
member.name.as("username"), // 매핑하고자 하는 필드네임
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
)
)
.from(member)
.fetch();
// then
for (UserDto result : results) {
System.out.println(result.toString());
}
}
실행 결과
UserDto 클래스에 매핑이 되었고
name 필드는 username으로 age 값은 Max 값인 30으로 정상 매핑 되었다.
사용 방법은 아래와 같이 사용된다.
- 필드.as(매핑 이름) : 필드에 별칭 적용
- ExpressionUtils.as(source, alias) : 필드나 서브 쿼리에 별칭 적용
e. @QueryProjection 사용
이 방법은 DTO의 생성자에 @QueryProjection 어노테이션을 사용하는 방법이다.
이 어노테이션의 설명을 보면 APT(Annotation Processing Tool) 베이스로 한 쿼리 타입 생성인데,
기존 Entity를 QEntity로 생성하는것과 같이 DTO 클래스를 Q타입 클래스로 미리 만들어서 사용하는 방식이다.
위에서 만든 PreMemberDto 클래스의 Q파일을 열어보면 아래와 같은 클래스가 만들어진걸 볼 수 있는데,
Q파일의 생성자를 PreMemberDto로 매핑하게 끔 되어있는 것을 볼수 있다.
아래 테스트 코드와 같이 new 생성자로 QPreMemberDto 클래스의 생성자에 맞게 매핑하면
PreMemberDto 로 정상적으로 매핑되는 것을 볼수 있다.
@DisplayName("QueryProjection Anno 매핑 테스트")
@Test
void dtoQueryProjTest() {
// given
// when
List<PreMemberDto> results = query.select(new QPreMemberDto(member.id,member.name,member.age))
.from(member)
.fetch();
// then
for (PreMemberDto result : results) {
System.out.println(result.toString());
}
}
이 방법의 장점은
런타임 시 에러를 확인 할수 있는 다른 방법들과 달리
코드 레벨(또는 컴파일 시)에서 확인이 가능하다는 점이다.
물론 장점만 있는건 아니다.
DTO에 어노테이션만 작성하면 되지만,
여러 레이어에서 사용될 수 있는 DTO 클래스가 QueryDSL에 의존성이 생길 수 있기 때문이다.
(김영한 님의 QueryDSL 강의에서는 상황(회사)에 맞게 결정하는 방향으로 말해주신다.)
3. 깃허브
GitHub - mk1-p/jpa-sample-code: Test for Spring JPA
Test for Spring JPA. Contribute to mk1-p/jpa-sample-code development by creating an account on GitHub.
github.com
4. 참고 문서
3.2. Result handling
Querydsl provides two ways to customize results, FactoryExpressions for row based transformation and ResultTransformer for aggregation. The com.mysema.query.types.FactoryExpression interface is used for Bean creation, constructor invocation and for the cre
querydsl.com