개요

인프런을 보다가 정리하는게 좋다는 생각이 들었다.

 

-

projection 이란 테이블의 특정한 컬럼들만 따로 조회함을 의미하는데, 여기서 결과값을 반환할 때, Dto 로 반환이 가능하다. 이 때 Dto 클래스로 반환하기 위해서 projection 하는 방법이 몇가지 있는데 거기에 대한 설명을 하려고 한다.

 

설명

1. Tuple 을 통한 query projection

Tuple (=Record) 를 통한 반환은 하나의 데이터 로우를 반환한다. 

@Test
@DisplayName("튜플 대상이 여러 개일때")
public void multiTupleSelectTest() {
    final List<Tuple> tuples = queryFactory
            .select(member.username, member.age)
            .from(member)
            .fetch();

    assertThat(tuples).hasSize(4);

    for(Tuple tuple : tuples) {
        System.out.println(
        	tuple.get(member.username) + " : " + tuple.get(member.age)
        );
    }
}
  • 해당 Tuple 을 통한 조회는 권장되지 않는다. 그래도 이용해야 한다면, Tuple 에 대한 사용은 Dao 또는 Repository 에서만 사용하고 다른 레이어 계층으로 이동될때는 Dto 클래스로 변환해주어서 이동시키도록 한다.
  • 왜냐하면 Tuple 은 com.querydsl.core import 를 물고 있기 때문에 이후에 querydsl 을 쓰지 않도록 서비스가 바뀌게되면 유연한 대처를 하지 못하기 때문이다.

 

2. Setter 를 통한 query projection

Dto 클래스의 Setter 메소드를 통한 조회를 할 수 있다.

@Test
@DisplayName("querydsl 이용, Dto 반환 [Setter] 이용")
public void findDtoByQuerydslSetter() {
    final List<MemberDto> memberDtos = queryFactory
            .select(Projections.bean(MemberDto.class, member.username, member.age))
            .from(member)
            .fetch();

    memberDtos.forEach(System.out::println);
}

// Dto 클래스
@Setter
@ToString
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public class MemberDto {

    private String username;
    private int age;
}
  • Projections.bean() 을 이용한다.
  • 기본생성자로 MemberDto 를 만들고, 거기에 setter 메소드를 이용해서 값을 세팅한다.

 

3. Field 를 통한 query projection

@Test
@DisplayName("querydsl 이용, Dto 반환 [Field] 이용")
public void findDtoByQuerydslField() {
    final List<MemberDto> memberDtos = queryFactory
            .select(Projections.fields(MemberDto.class, member.username, member.age))
            .from(member)
            .fetch();

    memberDtos.forEach(System.out::println);
}

// Dto 클래스
@ToString
@NoArgsConstructor(access = AccessLevel.PUBLIC)
public class MemberDto {

    private String username;
    private int age;
}
  • Projections.field() 를 이용한다.
  • 기본생성자로 MemberDto 를 만들고, 필드값을 삽입한다.
  • 접근지정자가 private 라고 하더라도 값의 삽입이 가능한데, projection 내부에 리플렉션 API 를 사용하고 있기 때문에 접근이 가능하도록 되어있다.

 

4. Constructor 를 통한 query projection

@Test
@DisplayName("querydsl 이용, Dto 반환 [Constructor] 이용")
public void findDtoByQuerydslConstructor() {
    final List<MemberDto> memberDtos = queryFactory
            .select(Projections.constructor(MemberDto.class, member.username, member.age))
            .from(member)
            .fetch();

    memberDtos.forEach(System.out::println);
}

// Dto 클래스
@ToString
public class MemberDto {

    private final String username;
    private final int age;
    
    public MemberDto(final String username, final int age) {
        this.username = username;
        this.age = age;
    }
}
  • Dto 에 생성자를 미리 만들어놓고, Projections.constructor() 를 이용하여 조회할 수 있다.
  • 이 때 생성자에서는 조회하려고 하는 컬럼의 값이 있어야 한다.

 

5. @QueryProjection 애노테이션을 활용한 query projection

@Test
@DisplayName("querydsl 이용, @QueryProjection annotation 이용을 한다.")
public void findDtoByQueryProjectionAnnotation() {
    final List<AccountDto> accountDtos = queryFactory
            .select(new QAccountDto(member.username, member.age))
            .from(member)
            .fetch();

    accountDtos.forEach(System.out::println);
}

// Dto 클래스
@Getter
@ToString
public class AccountDto {

    private String username;
    private int age;

    @QueryProjection
    public AccountDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  • Dto 클래스에 미리 @QueryProjeciton 이란 애노테이션을 달아놓고 조회하는 방법이 있다.
  • 이렇게 하려고 하면 AccountDto 에 대한 QType 의 클래스 QAccountDto 가 있어야 하기 때문에 gradle 의 compileQuerydsl Task 를 수행시켜주어서 generated 해주어야 한다. (이렇게 된다면 Dto 에는 Querydsl 의 의존성이 들어갈 수 밖에 없다.)

 

어떤걸 쓰는게 좋을까?

쓰고 싶은걸 쓰면 된다. 다만 한가지 유의점이 있다.

 

Constructor 를 통한 query projection 시에 새로운 컬럼 값을 조회하기위해 .select() 절에 새로운 컬럼을 추가해도 컴파일 에러가 발생하지 않는다. (Dto class 에서는 해당 새로운 칼럼을 생성자에 추가하지 않았는데 말이다.) 해당 문제는 런타임때 발생한다. 개발자가 사전에 인지하지 못한다.

 

반면에 @QueryProjection 은 새로운 컬럼 값 추가 시, 컴파일 에러가 발생해서 개발자가 사전에 알아차릴 수 있다.

 

그 밖에 Setter 나 Field 의 경우에 새로운 컬럼값을 추가하더라도 projection 유형이 setter, field 이기 때문에 그래도 조회하여도 무방하다.

 

참고링크

Posted by doubler
,