본문 바로가기
개념 공부/Spring & ORM

[Java / querydsl] 여러 Projection 방법

by clean01 2025. 3. 16.

프로젝션 기본

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

    // 프로젝션 대상이 하나인 경우
    @DisplayName("모든 회원의 이름 조회")
    @Test
    void simpleProjection() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();

        for (String s : result) {
            System.out.println("result = " + s);
        }
    }

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

    @DisplayName("모든 회원의 이름과 나이를 반환")
    @Test
    void tupleProjection() {
        // given
        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);
        }
    }

프로젝션 대상이 여러 개일때는 Tuple로 결과를 반환한다.

하지만 Tuple은 querydsl에 의존적인 자료구조이므로, repository 계층에서만 사용하고, 다른 계층에서는 DTO로 결과를 반환하는 편이 훨씬 좋다.

프로젝션 - DTO를 활용한 방식

[1] 프로퍼티 접근 - Setter

Setter로 접근하는 방식은 “프로퍼티 접근” 이라고도 한다.

이 방식은 기본 생성자와 세터가 필요하다.

    @DisplayName("프로퍼티 접근")
    @Test
    void findDtoBySetter() {
        // given
        List<MemberDto> result = queryFactory.select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age
                ))
                .from(member)
                .fetch();

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

[2] 필드 접근

위 프로퍼티 접근 방식과 달리, Setter가 없어도 프로젝션이 가능한 방법이다.

이 방식은 기본 생성자, Getter만 열려 있어도 가능하다.

위 프로퍼티 접근 방식에서 Projections.bean을 Projections.fields로 바꿔주기만 하면 된다.

    @DisplayName("필드 접근")
    @Test
    void findDtoByField() {
        // given // when
        List<MemberDto> result = queryFactory.select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

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

 

 

[+추가] Projection 필드 접근 방식에서, 엔티티의 필드명과 dto 필드명이 다를 때

현재 Member 엔티티는 회원의 이름을 뜻하는 필드가 “username”이다.

 

 

그리고 쿼리 프로젝션을 하기위한 UserDto를 만들었다고 하자.

여기서 회원의 이름을 나타내는 필드는 “name”이다.

 

이렇게 엔티티와 Dto의 필드 이름이 일치하지 않는 상황에서 아래와 같은 프로젝션 코드가 실행된다면 어떨까?

    @DisplayName("필드 접근 2 - dto의 필드 이름과 QClass의 필드이름이 다른 경우")
    @Test
    void findDtoByField2() {
        // given // when
        List<UserDto> result = queryFactory.select(Projections.fields(UserDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

        // then
        for (UserDto userDto : result) {
            System.out.println("memberDto = " + userDto);
        }
    }

출력결과는 아래와 같이 name이 null로 조회가 된다.

Querydsl의 Projection에서 필드 방식으로 접근을 할 때는 엔티티와 dto의 필드명이 일치해야 가져올 수 있다.

 

그렇다면, 엔티티와 dto의 필드명이 일치하지 않는 경우는 어떻게 해야할까?

바로, 일치하지 않는 필드 뒤에 ‘.as(”dto의 필드이름”)’라고 작성해주면 된다.

즉, 아래 예시로 치면 as 메서드를 통해 “username” 컬럼에 “name”이라는 alias를 주는 셈이다.

    @DisplayName("필드 접근 2 - dto의 필드 이름과 QClass의 필드이름이 다른 경우")
    @Test
    void findDtoByField2() {
        // given // when
        List<UserDto> result = queryFactory.select(Projections.fields(UserDto.class,
                        member.username.as("name"), // 이부분 as 메서드 추가
                        member.age))
                .from(member)
                .fetch();

        // then
        for (UserDto userDto : result) {
            System.out.println("memberDto = " + userDto);
        }
    }

 

 

 

@QueryProjection 어노테이션

우선 dto의 특정 생성자 위에 @QueryProjection 어노테이션을 달아준다.

 

 

그리고 한번 compileQuerydsl 해준다.

 

 

그러면 QMemberDto라는 Class가 생성되어 있을 것이다.

/**
 * study.querydsl.dto.QMemberDto is a Querydsl Projection type for MemberDto
 */
@Generated("com.querydsl.codegen.DefaultProjectionSerializer")
public class QMemberDto extends ConstructorExpression<MemberDto> {

    private static final long serialVersionUID = 1356709634L;

    public QMemberDto(com.querydsl.core.types.Expression<String> username, com.querydsl.core.types.Expression<Integer> age) {
        super(MemberDto.class, new Class<?>[]{String.class, int.class}, username, age);
    }

}

그리고 이렇게 쿼리 프로젝션으로 조회하는 코드를 작성한 후 실행해보면 잘 조회되는 것을 알 수 있다.

 

    @DisplayName("QueryProjection 어노테이션")
    @Test
    void findDtoByQueryProjection() {
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .fetch();

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

 

[+추가] Query Projection과 생성자 방식은 뭐가 다를까?

Query Projection 방식의 가장 큰 장점은 컴파일 시점에 오류를 잡을 수 있다는 것이다.

이런식으로 QMemberDto에 존재하진 않는 필드를 넣으면 컴파일 시점에 오류를 뱉어주기 때문에, 런타임 시점에 오류가 발생하는 constructor 방식보다 더 좋다.

 

 

 

 

 

하지만 QueryProjection은 장점만 있는 것은 아니다.

  • 단점 1: 추가로 QFile이 생성되어야 한다는 것
  • 단점 2: DTO가 querydsl 라이브러리에 의존하게 된다는 것

Query Projection 방식을 쓰기 전에는 MemberDto는 querydsl 라이브러리와 의존 관계가 없었다.

하지만 MemberDto 생성자에 @QueryProjection 어노테이션을 달아야 하는 상황이 오면서, MemberDto가 Querydsl 라이브러리에 의존하게 되었다.

 

그럼 Query Projection을 쓸 것인가, 아니면 field 접근과 같은 다른 방식을 사용할 것인가는 프로젝트의 상황에 따라 결정을 하면 된다.

만약 DTO를 querydsl에 의존하게 하지 않고 깔끔하게 가져가고 싶다면 필드 접근 방식을 사용하면 되고,

프로젝트 전반에서 이미 Querydsl을 많이 사용하고 있어서 의존 관계가 생겨도 상관 없다고 판단하였다면 장점이 많은 쿼리 프로젝션 방식을 사용하는 것을 선택하면 된다.

 

Reference

인프런 < 실전! Querydsl (김영한) >