Programming/Spring Boot

Querydsl DTO 조회하는 방법(Projection, @QueryProjection)

Jan92 2021. 12. 10. 00:04

Querydsl DTO

 

Projection 연산이란,

 

- 한 Relation의 Attribute들의 부분 집합을 구성하는 연산자입니다.

- 결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가집니다.

- 결과 Relation은 기본 키가 아닌 Attribute에 대해서만 중복된 tuple들이 존재할 수 있습니다.

 

=> 쉽게 Projection이란, '테이블에서 원하는 컬럼만 뽑아서 조회하는 것'이라고 할 수 있습니다.

 

* Relation 데이터를 원자 값으로 갖는 이차원 테이블

* Column = Attribute

 

(프로젝션 대상이 하나일 때는 그 대상의 타입으로 반환되지만, 프로젝션 대상이 둘 이상일 때는 Tuple 또는 DTO로 변환할 수 있습니다.)

 

 


 

 

Querydsl DTO로 조회하는 방법(Projection, @QueryProjection)

 

- Projection.bean

- Projection.fields

- Projection.constructor

- @QueryProjection

 

해당 포스팅에서는 위 4가지 방법에 대해서 살펴보겠습니다. 개인적으로 프로젝트를 하며 많이 사용한 방법은 Projection.constructor과 @QueryProejction 두 가지입니다.

 

* Tuple을 이용한 방법도 있으나 결과에서 각각의 필드 값을 하나하나 직접 꺼내 줘야 하는 번거로움이 있고, Tuple 자체가 Querydsl의 Tuple 객체이기 때문에 Repository가 아닌 Service, Controller 계층으로 이동할 경우에는 적합하지 않기 때문에 많이 사용되지 않는 방법입니다.

(Tuple은 com.querydsl.core package에 있습니다.)

 

 

@Entity
public class Member {
    @Id
    private Long id;
    private String name;
    private int age;
}


@Data
public class MemberDto {
    private String name;
    private int age;
    
    pulbic MemberDto() {}
    
    public MemberDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

 

먼저 예시에 사용할 Entity와 DTO입니다.

(@Data 어노테이션은 @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 모두 포함된 어노테이션입니다.)

 

 


 

 

1. Projections.bean (Setter 메서드를 이용한 조회 방법)

 

public void findDtoBySetter() {
    List<MemberDto> members = queryFactory
            .select(Projections.bean(MemberDto.class,
                member.name,
                member.age
            ))
            .from(member)
            .fetch();
}

 

Projection.bean 방법은 setter 메서드를 기반으로 동작합니다. 때문에 DTO 객체의 각 필드에 setter 메서드가 있어야 합니다.

단순하게 조회용으로만 사용되는 DTO라면 setter 메서드가 오픈되어 값이 변경되어도 상관이 없을 수 있겠지만, 만약 영속화된 데이터를 변경하는 책임을 가진 객체라면 문제가 될 수 있기 때문에 그러한 이유에서 권장되는 패턴은 아닙니다.

 

 


 

 

2. Projections.fields (Reflection을 이용한 조회 방법)

 

public void findDtoByReflection() {
    List<MemberDto> members = queryFactory
            .select(Projections.bean(MemberDto.class,
                member.name,
                member.age
            ))
            .from(member)
            .fetch();
}

 

Projections.fields를 이용한 방법은 getter, setter 메서드 필요 없이 field에 값을 직접 주입해주는 방식입니다. 위에서 본 setter를 이용한 Projections.bean 방식과 마찬가지로 Type이 다를 경우 매칭이 되지 않으며, 컴파일 시점에서는 에러를 잡지 못하고 런타임 시점에서 에러가 잡힙니다.

 

 

***

여기서 만약 Member Entity의 필드가 name이 아닌 username이고, MemberDto 클래스에서 해당 데이터를 받을 필드의 이름이 name인 경우 필드의 명칭이 다르기 때문에 querydsl의 결과로 나온 MemberDto에 name 값이 없을 것입니다.

이런 경우에 아래 예시 코드와 같이 alias(별칭)을 사용하여 매핑 문제를 해결할 수 있습니다.

 

List<MemberDto> fetch = queryFactory
        .select(Projections.fields(MemberDto.class,
                member.username.as("name"),
                member.age
        ).from(member)
        .fetch();

 

 


 

 

3. Projections.constructor (생성자를 이용한 조회 방법)

 

public void findDtoByConstructor() {
    List<MemberDto> members = queryFactory
            .select(Projections.constructor(MemberDto.class,
                member.name,
                member.age
            ))
            .from(member)
            .fetch();
}

Projections.constructor는 생산자 기반 바인딩입니다. 생성자 기반 바인딩이기 때문에 객체의 불변성을 가져갈 수 있다는 장점이 있지만 바인딩 과정에서 문제가 생길 수 있습니다.

 

* 개인적으로 실제 프로젝트에서 많이 사용한 기능이며, 가지고 온 값을 생성자에서 한번 더 가공할 수 있다는 장점도 있습니다.

 

 

  public static <T> ConstructorExpression<T> constructor(Class<? extends T> type, Expression<?>... exprs) {
    return new ConstructorExpression(type, exprs);
  }

 

Projections의 constructor 메서드입니다. 해당 메서드를 보면 DTO 객체의 생성자에게 직접 바인딩하는 것이 아니라 Expression <?>... exprs 값을 넘기는 방식으로 작동하는데요. 따라서 값을 넘길 때 생성자와 필드의 순서를 일치시켜야 합니다.

필드의 개수가 적을 때는 문제가 되지 않지만 필드의 개수가 많아지는 경우 실수가 발생하여 오류로 이어질 수 있습니다.

하지만 생성자를 이용한 방식은 필드명이 name, username처럼 달라도 해당 순서에 위치한 필드의 타입만 서로 일치한다면 정상적으로 동작한다는 특징이 있습니다.

 

생성자를 이용한 방법 역시 파라미터 작성을 잘못했을 경우, 컴파일 시점에서는 오류를 발견하지 못하고, 런타임 시점에서 해당 메서드를 실행했을 때 오류가 발생합니다.

 

 


 

4. @QueryProjection

 

public class MemberDto {
    private String name;
    private int age;
    
    @QueryProjection // 어노테이션 추가
    public MemberDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}


public void findDtoByQueryProjection() {
	queryFactory
		.select(new QMemberDto(member.name, member.age))
		.from(member)
		.fetch();
}

 

@QueryProjection을 이용하면 불변 객체 선언, 생성자를 그대로 사용할 수 있기 때문에 권장되는 패턴입니다.

(정확하게는 DTO의 생성자를 사용하는 것이 아니라 DTO 기반으로 생성된 QDTO 객체의 생성자를 사용하는 것입니다.)

 

작동 방법은 먼저 DTO의 생성자에 @QueryProjection 어노테이션을 추가하여 QType의 클래스를 생성하여 위의 예시 코드와 같이 사용합니다. 이 방식은 new QDTO로 사용하기 때문에 런타임 에러뿐만 아니라 컴파일 시점에서도 에러를 잡아주고, 파라미터로도 확인할 수 있다는 장점이 있습니다.

 

반면 해당 기능을 사용하려는 DTO마다 QType의 Class를 추가로 생성해줘야 하는 것도 있지만, DTO는 Repository 계층의 조회 용도뿐만 아니라 Service, Controller 모두 사용되기 때문에 @QueryProjection 어노테이션을 DTO에 적용시키는 순간, 모든 계층에서 쓰이는 DTO가 Querydsl에 의존성을 가지기 때문에 아키텍처적으로 적용을 생각해 볼 필요는 있습니다.

 

 

 

< 참고 자료 >

 

프로젝션 대상이 둘 이상일 경우, DTO로 조회하는 방법

안녕하세요, 방문해 주셔서 감사합니다. 🙇🏻‍♂️ 🥰

hue-dev.site

 

 

Querydsl Projection 방법 소개 및 선호하는 패턴 정리 - Yun Blog | 기술 블로그

Querydsl Projection 방법 소개 및 선호하는 패턴 정리 - Yun Blog | 기술 블로그

cheese10yun.github.io