본문 바로가기

Spring/Querydsl

Querydsl- 프로젝션

프로젝션

 프로젝션이란 select절로 조회할 대상을 지정하는 것을 의미한다.

예컨데 querydsl에서는 프로젝션 대상이 하나인 경우 단일 타입으로 반환을 하게된다.

 

프로젝션 대상이 하나인 경우

List<String> result = queryFactory
 .select(member.username)
 .from(member)
 .fetch();

 

반면에 프로젝션 대상이 여러개인 경우 튜플이나 DTO를 통해서 조회하게 된다.

 

프로젝션 대상이 여러개인 경우

List<Tuple> result = queryFactory
 	.select(member.username, member.age)
 	.from(member)
 	.fetch();
 
for (Tuple tuple : result) {
    String username = tuple.get(member.username);
    Integer age = tuple.get(member.age);
    System.out.println("username=" + username);
	System.out.println("age=" + age);
}

 튜플로 조회된 경우 튜플의 get( ) 메서드를 이용해서 데이터를 꺼낼 수 있다.

설계시 주의 할 점

 여기서 주의할 점은 Tuple이라는 타입은 querydsl에 종속적인 타입이라는 것이다. 때문에 설계를 할 때 쿼리 dsl에서 다른 기술로 개발하기로 방향을 바꾸는 경우 시스템 전체적으로 영향을 받을 수 있다. 때문에 Tuple은 가급적이면 repository 계층에서만 사용을 하고, 외부 계층에다 데이터를 전달하는 경우에는 Tuple에서 데이터를 꺼내어 Dto에 담은 후 외부 계층에 반환하는 식으로 개발을 하자!

 

MemberDto

package study.querydsl.dto;

import lombok.Data;

@Data
public class MemberDto {

    private String username;
    private int age;

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

Member에서 username과 age를 전달하는데 사용할 MemberDto를 정의하였다.

 

jpa에서 Dto로 조회를 하는 경우는 다음과 같다.

@Test
public void findDtoByJpql() throws Exception{
    List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) " +
                    "from Member m", MemberDto.class)
            .getResultList();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

출력>>>
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)

 jpa에서 멤버를 조회후 Dto에 데이터를 넣는 과정은 Dto를 조회하는데에만 "study.querydsl.dto.MemberDto(...)" 같이 복잡한 쿼리를 요구한다.

Jpa에서 DTO 조회의 문제점

  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야 함
  • DTO의 package 이름을 다 적어줘야함
  • 생성자 방식만 지원

DTO 조회

1) Projections.bean( ) 메서드를 통한 Dto 조회 ( 수정자 주입 )

 Querydsl에서는 다음과 같다.

@Test
public void findDtoBySetter() throws Exception{

    // Projections.bean(DTO.class, 주입 필드...) => DTO 객체를 생성한 후 setter 주입으로 DTO 반환 
    List<MemberDto> result = queryFactory
            .select(Projections.bean(MemberDto.class,  
                    member.username,
                    member.age))
            .from(member)
            .fetch();
	// MemberDto 객체 생성 후 setter 주입(Dto의 기본생성자 필요!!)
    
    
    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

 

조회 자체는 member에서 할 것이기 때문에 from(member)로 진행한다. 중요한 것은 select 절인데 

select(Projections.bean(MemberDto.class, member.username, member.age))

select 절 내부에 Projections.bean( ) 메서드를 이용해 DTO(MemberDto)에 username과 age를 주입해주고 있다.

Projections.bean(DTO.class, 주입 필드들 ...)Dto 객체를 생성한 뒤 수정자(setter) 주입으로 Dto를 반환해준다.

2) Projections.fields( ) 메서드를 통한 Dto 조회 ( 필드 주입 )

@Test
public void findDtoByField() throws Exception{

    // Projections.bean(DTO.class, 주입 필드...) 
    // => DTO 객체를 생성한 후 필드 주입으로 DTO 반환. 굳이 setter 필요 없다
    List<MemberDto> result = queryFactory
            .select(Projections.fields(MemberDto.class,  // MemberDto 객체 생성 후 필드 주입(Dto의 기본생성자 필요!!)
                    member.username,
                    member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}
select(Projections.fields(MemberDto.class,member.username, member.age)

기존의 setter 주입의 Projections.bean( )에서 메서드만 살짝 바뀌었다. 필드 주입을 하므로 MemberDto의 필드 username, age에 직접 값을 때려 박아준다. 그래서 setter 주입과는 달리 setter가 없어도 동작한다.

 

주의점) setter, 필드 주입에서 Dto의 필드명과 파라미터명이 불일치 하는 경우

 필드 주입과 setter 주입의 경우에는 Dto의 멤버와 파라미터 명이 일치하는 경우에만 주입이 되고, 이름이 다른 경우에는 주입이 되지 않는다. 만약에 위처럼 주입을 하는 경우에 MemberDto의 username이 name으로 이름이 변경되었다고 하자.

그 경우에는 이름이 매칭되지 않으므로 MemberDto의 name 필드의 경우 null이 된다. 

 

그래서 이런 경우에는

select(Projections.fields(MemmberDto.class, member.username.as("name"), member.age)

와 같이 as구문을 이용해서 username의 이름을 name으로 변경하여 주입하는 방식으로 대체할 수 있다.

3) Projections.constructor( ) 메서드를 통한 Dto 조회( 생성자 주입 )

@Test
public void findDtoByConstructor() throws Exception{

    // Projections.bean(DTO.class, 주입 필드...) => DTO 객체를 생성한 후 생성자 주입으로 DTO 반환
    List<MemberDto> result = queryFactory
            .select(Projections.constructor(MemberDto.class,  // MemberDto 객체 생성 후 생성자 주입(Dto의 기본생성자 필요!!)
                    member.username,
                    member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

 이번에도 다 같고 Projections.constructor( )로 메서드만 살짝 바뀌었다. 다만 생성자 주입인만큼 여기서 주의해야 할 점은 생성자의 파라미터의 순서에 맞게 인자를 넣어 주어야한다.

 

생성자

public MemberDto(String username, int age) {
    this.username = username;
    this.age = age;
}

생성자가 이런식으로 되어 있는 경우

 Projections.constructor(MemberDto.class, member.age, member.username)

처럼 순서 바뀌어서 파라미터를 넣어주면 에러가 발생한다.(물론 타입이 같다면 에러가 발생하진 않겠지만 잘못된 값이 주입됨)

 

member.username, member.age 순서 바꾸자 발생한 에러

4) @QueryProjection - Q 파일을 사용한 Dto 조회

package study.querydsl.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

    @QueryProjection  //Dto를 바탕으로 QDto를 생성
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

 기존 Dto 클래스의 생성자위에 정의된 @QueryProjection을 보자.

해당 어노테이션을 사용하면 Dto 클래스들도 Q 파일로 생성할 수 있다.

 

생성 방법

  1. 생성할 Dto에 @QueryProjection 엔티티를 붙힘
  2. gradle.querydsl.other.compileQuerydsl 클릭

 Q 파일로 Dto를 생성한 경우, 패키지 명을 명시할 필요 없이 간단하게 자바 코드로 생성자 주입을 할 수 있다.

@Test
public void findDtoByQueryProjection() throws Exception{
    // QMemberDto와 MemberDto를 생성자 주입으로 사용하는 것의 차이점:
    // Projections.constructor(MemberDto.class, member.age, member.username)과 같이
    // 주입 순서가 잘못되었거나(타입이 잘못됨), 또는 생성자의 파라미터 개수보다 더 많은 인자를 주입하는 경우와
    // 같이 잘못된 주입이 되더라도, 이를 컴파일 타임에 잡지 못하고 런타임에 에러가 발생한다.

    // 반면에 QMemberDto(생성자 주입)를 사용한다면, 주입이 잘못된 경우 이를 컴파일 타임에 에러체킹을 할 수 있다.
    // 또한 문법적으로 사용도 더 편하다.
    List<MemberDto> result = queryFactory
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}

new QMemberDto()를 통해서 Dto 인스턴스를 생성한 후 생성자 주입으로 인자를 넣어준다.

'Spring > Querydsl' 카테고리의 다른 글

Querydsl- 벌크 연산  (0) 2023.08.06
Querydsl- 동적쿼리  (0) 2023.08.05
Querydsl- 조인  (0) 2023.07.27
Querydsl- 집합  (0) 2023.07.26
Querydsl- 정렬과 페이징  (0) 2023.07.25