BackEnd/JPA

[JPA] QueryDSL 중급문법 - 1

샤아이인 2022. 5. 14.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

[JPA] QueryDSL 중급문법 - 1

 

1. 프로젝션과 결과 반환 - 기본

프로젝션이란 열 단위로 조회하는 것을 의미한다. (select는 행 단위 조회)

  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회할 수 있다.

 

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

@Test
public void simple_projection_test() {
    List<String> result = queryFactory
            .select(member.username)
            .from(member)
            .fetch();

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

프로젝션 대상이 하나면 명확하게 타입을 지정해줄 수 있다. List<String>

 

1-2) 프로젝션 대상이 둘 이상인 경우

@Test
public void tuple_projection_test() {
    List<Tuple> tuples = queryFactory
            .select(member.username, member.age) // 프로젝션 대상이 둘 이상
            .from(member)
            .fetch();

    for (Tuple tuple : tuples) {
        String username = tuple.get(member.username);
        Integer age = tuple.get(member.age);
        System.out.println("username = " + username + ", age = " + age);
    }
}

Tuple은 Repository 계층 안에서 사용하는 것은 좋지만, 그 밖의 계층에서는 DTO로 변환시켜 사용하는 것 이 좋다.

Tuple을 열어보면 package가 com.querydsl.core에 있다. 즉, Tuple도 querydsl 에 종속적인 기술이다.

 

repository 안에서만 사용하고, 외부로 노출될때는 DTO로 변환해서 나가기를 권장한다.

하부 구현에 queryDsl을 사용한다는 것을 controller, service 계층에서 알게되면 향후 repository의 구현체를 변경할때 문제가 생긴다.

 

2. 프로젝션과 결과 반환 - DTO 조회

이번 시간에는 Member의 모든 필드값이 아닌, name 과 age 두건만 조회해고 싶은 상황이다.

사용할 MemberDto는 다음과 같다.

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

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

 

2-1) JPQL로 DTO 조회

@Test
public void findByDtoJPQL() {
    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);
    }
}

실행 결과는 다음과 같다.

[JPA] QueryDSL 중급문법 - 1 - 				
    
    	2. 프로젝션과 결과 반환 - DTO 조회
    
 - undefined - 2-1) JPQL로 DTO 조회

실행되는 쿼리는 다음과 같다.

select
    member0_.username as col_0_0_,
    member0_.age as col_1_0_ 
from
    member member0_

하지만 이 방식은 사용하기 너무 번잡하다. 일단 페키지 명을 전부 다 적어줘야하는 점부터가 고난길이다...

new 명령어도 사용해야하고, 생성자 방식만 지원한다.

 

2-2) QueryDSL 에서의 DTO 반환하기

QueryDsl 에서는 총 3가지 방식의 DTO반환 방식을 지원한다.

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용

 

▶ 프로퍼티 접근 - setter

@Test
public void findByDtoSetter() {
    List<MemberDto> memberDtoList = queryFactory
            .select(Projections.bean(MemberDto.class, member.username, member.age))
            .from(member)
            .fetch();

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

이 방식은 setter가 DTO 상에 있어야 사용 가능한 방식이다.

롬복을 통해 @Setter를 지정해 주거나, @Data 를 사용해야 한다.

 

▶ 필드 직접 접근

필드 방식은 getter, setter가 DTO에 없어도 적용할 수 있는 방식이다. 

필드가 private 여도 알아서 다 처리된다.

@Test
public void findByDtoField() {
    List<MemberDto> memberDtoList = queryFactory
            .select(Projections.fields(MemberDto.class, member.username, member.age))
            .from(member)
            .fetch();

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

Projections.fields(주입대상class, 필드1, 필드2, ...) 처럼 사용된다.

 

▶  프로퍼티, 필드 직접 접근 - 별칭이 다른 경우

다음과 같이 UsetDto를 따로 만들어 사용해보자.

@Data
@NoArgsConstructor
public class UserDto {

    private String name;
    private int age;

    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

따라서 실행해볼 test 코드는 다음과 같다. 반환 타입이 UserDto로 바뀌게 되었다.

@Test
public void findByUserDtoField() {
    List<UserDto> memberDtoList = queryFactory
            .select(Projections.fields(UserDto.class, member.username, member.age))
            .from(member)
            .fetch();

    for (UserDto userDto : memberDtoList) {
        System.out.println("userDto = " + userDto);
    }
}

실행시 정상적으로 실행된다. 다만 username에 대한 데이터를 가져오지 못하게 된다.

우리가 fields()에 인자로 넘기는 이름은 member.username인데, DTO 필드 이름은 name이다.

[JPA] QueryDSL 중급문법 - 1 - 				
    
    	2. 프로젝션과 결과 반환 - DTO 조회
    
 - undefined - 2-2) QueryDSL 에서의 DTO 반환하기

이럴때는 member.username.as("name") 으로 사용하자.

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

이렇게 별칭을 맞추어 주면 정상 작동한다. 이는 필드에 별칭을 주는 방식이다.

 

서브 쿼리에 별칭을 적용하는 방법을 알아보자.

@Test
public void findByUserDtoField() {
    QMember memberSub = new QMember("memberSub");

    List<UserDto> memberDtoList = queryFactory
            .select(Projections.fields
                    (
                            UserDto.class,
                            member.username.as("name"),
                            ExpressionUtils.as(JPAExpressions.select(memberSub.age.max()).from(memberSub), "age")
                    )
            )
            .from(member)
            .fetch();

    for (UserDto userDto : memberDtoList) {
        System.out.println("userDto = " + userDto);
    }
}

형식이 fields(UserDto.class, 이름관련, 나이관련) 와 같은 형식이다.

여기서 "이름관련" 부분은 'name'이라는 별칭을 지정해 주었다.

"나이관련" 부분에는 JPAExpression을 통해 subQuery를 지정해 주었는데, subQuery에는 별칭이 없다.

따라서 ExpressionUtils.as()로 한번 wrapping하여 "age" 라는 별칭을 추가해준 것 이다.

 

"이름관련" 부분에도 as("name")이 아니라, 다음과 같이 사용해도 된다.

ExpressionUtils.as(member.username, "name")

 

▶ 생성자 사용

@Test
public void findByDtoConstructor() {
    List<MemberDto> memberDtoList = queryFactory
            .select(Projections.constructor(MemberDto.class, member.username, member.age))
            .from(member)
            .fetch();

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

생성자 같은 경우 이름이 아니라 type을 보고 만들어지기 때문에 직전의 field 방식의 문제가 생기지 않는다.

 

3. 프로젝션과 결과 반환 - @QueryProjection

이번 방법은 궁극의 방법이자, 단점도 많은 방식이다.

 

MemberDto의 생성자에 바로 @QueryProjection 을 추가해주자.

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

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

이후 gradle에 가서 complieQuerydsl 을 실행해준다.

[JPA] QueryDSL 중급문법 - 1 - 				
    
    	3. 프로젝션과 결과 반환 - @QueryProjection

위와같이 QMemberDto가 생성된것을 확인할 수 있다.

 

이후 그냥 생성자를 통해 객체를 생성하듯이 넘겨주면 된다.

@Test
public 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);
    }
}

그럼 위 방식의 생성자를 사용하는 방식과, 이전시간에 배웠던 Projections.constructor는 뭐가 다를까?

 

Projections.constructor 방식은 다음과 같이 생성자에 추가적으로 값을 더 넘기게 되었을때 (id를 추가로 전달)

List<MemberDto> memberDtoList = queryFactory
        .select(Projections.constructor(MemberDto.class, member.id, member.username, member.age))
        .from(member)
        .fetch();

컴파일 오류가 아니라, 실행이 된 후 runtime 오류가 발생한다.

유저가 실제로 해당 기능을 사용하는 순간에 오류가 발생하는 것 이다.

 

똑같은 문제를 @QueryProjection 방식에서는 컴파일 오류로 알려준다.

[JPA] QueryDSL 중급문법 - 1 - 				
    
    	3. 프로젝션과 결과 반환 - @QueryProjection

하지만 이 방식에도 단점은 존제한다.

  1. Q 클래스 파일을 만들어야 한다는 점
  2. 아키텍처 문제(의존관계 문제), DTO 가 QueryDSL에 대한 의존성이 생겨버린다.

실용적인 관점에서 이정도는 사용해도 된다 생각되면 사용해도 된다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 실무 활용 - 순수 JPA와 Querydsl  (0) 2022.05.15
[JPA] QueryDSL 중급문법 - 2  (0) 2022.05.14
[JPA] QueryDSL 기본문법 - 4  (0) 2022.05.14
[JPA] QueryDSL 기본문법 - 3  (0) 2022.05.14
[JPA] QueryDSL 기본문법 - 2  (0) 2022.05.13

댓글