JPA

QueryDSL Projection 결과 반환하는 방법

빈코 2022. 11. 14. 16:28

이전 포스팅에서는 Query DSL을 사용해서 조인하는 방법에 대해 알아보았습니다. 이번 포스팅은 Projection 결과를 반환하는 방법에 대해 알아보려고 합니다. 프로젝션(Projection)select 절에서 어떤 칼럼들을 조회할지 대상을 지정하는 것을 말합니다😄

 

프로젝션
Projection

 

개요

예시 관계도

이전 포스팅에서 사용했던 엔티티들을 재사용할 예정입니다. 혹여 엔티티 클래스의 코드가 궁금하신 분들은 엔티티 설계 포스팅을 참고해주세요😄

 

📌 저는 BeforeEach 어노테이션을 활용해 가데이터가 들어간 상황에서 테스트를 진행한 것입니다.

 

@BeforeEach
public void before() {
    queryFactory = new JPAQueryFactory(em);
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    em.persist(teamA);
    em.persist(teamB);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 20, teamA);

    Member member3 = new Member("member3", 30, teamB);
    Member member4 = new Member("member4", 40, teamB);
    em.persist(member1);
    em.persist(member2);
    em.persist(member3);
    em.persist(member4);
}
MEMBER_ID NAME AGE TEAM_NAME
1 member1 10 teamA
2 member2 20 teamA
3 member3 30 teamB
4 member4 40 teamB

 

 

개수에 따른 프로젝션 타입📗

첫 번째로, 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있습니다. 아래 예시를 볼까요?

 

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

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

테스트 결과
테스트 결과

select 절로 가져오는 데이터가 회원의 이름뿐이기 때문에 result의 타입은 String으로 국한되어 있는 걸 볼 수 있습니다. 하지만 실무에서 이렇게 한 개의 데이터만 가져오는 경우는 흔치 않습니다.

 

그럼 반환 되는 데이터의 종류가 많다면 어떻게 될까요?

 

@Test
public void tupleProjection() {
    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 자체를 출력하게 되면 배열 형태로 출력이 되고, 위의 테스트 코드처럼 Tuple 내에 접근하기 위해서 get 메서드를 활용하였습니다.

 

하지만 실무에서 Tuple을 사용하는 것은 외부에 DB를 그대로 노출시키는 행위와 같기 때문에 대부분 Dto 형식으로 매핑하여 반환합니다.

 

📌상황에 따라 Entity로 받아서 Dto로 다시 매핑할수도, 처음부터 Dto로 받는 경우도 있습니다

 

Member Dto 생성📔

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

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

클라이언트의 요구 사항이 회원의 이름과 나이만 필요하다고 했을 때를 가정하고 DTO를 생성하였습니다. @Data 롬복 어노테이션을 사용해서 getter, setter, toString을 외부에서 활용할 수 있습니다.

 

JPQL을 활용한 DTO 반환 방법📘

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

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

Query DSL을 사용하기 이전에 JPQL을 사용하여 DTO로 반환 받으려면 위의 코드처럼 new 연산자를 사용해야 했습니다. 코드만 봐도 DTO의 package 이름을 다 적어줘야 해서 지저분할뿐더러 생성자 방식만 지원하는 단점이 있었습니다.

 

하지만 Query DSL에서는 DTO를 프로퍼티 접근 방법, 필드 직접 접근 방법, 생성자 사용 총 3가지의 방법을 제공합니다.

 

QueryDSL을 활용한 DTO 반환 방법📒

첫번째로 프로퍼티(Setter)를 활용해서 DTO를 반환받는 방법입니다. 다음 코드를 볼까요?

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

테스트 결과
테스트 결과

com.querydsl.core.types.Projections의 메서드인 bean()을 호출하여 매핑할 클래스와 매핑할 필드를 순서대로 전달합니다. 테스트 결과를 보면 원하는 값들이 DTO 형식으로 반환받은 것을 알 수 있습니다😊


필드 직접 접근 방법은 위에 bean() 코드에서 fields()로 방식만 수정해주면 됩니다.

 

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

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

bean()과 차이점이 있다면 fields()는 getter/setter가 필요 없다는 점이고 결과는 동일하게 나옵니다. 생성자 방식도 fields() 대신 constructor()로 변경하면 됩니다. 마찬가지로 정상적으로 조회해오는 것을 확인할 수 있었습니다.(코드가 너무 중복되어 생략하겠습니다)

 

필드명이 다른 DTO 사용📙

만약 필드명이 다른 DTO를 사용해서 매핑해야 할 경우에는 어떻게 해야 할까요? 예시로 UserDTO를 만들어 보겠습니다.

 

@Data
@NoArgsConstructor
public class UserDto {

    private String name;
    private int age;

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

MemberDTO와 다른점은 이름 필드인 name입니다. 코드는 위와 동일하게 작성하였고 테스트 결과에 집중해주세요😄

 

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

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

테스트 결과
테스트 결과

테스트 결과를 보시면 해당 DTO들의 name 필드가 모두 null로 처리된 것을 확인할 수 있습니다. 당연한 거지만 필드의 이름이 일치하지 않아 무시하고 넘어가는 현상인데, 이 부분은 as() 별칭 메서드로 아래와 같이 해결할 수 있습니다.

 

member.username.as("name")

테스트 결과
테스트 결과

 

📌서브 쿼리는 ExpressionUtils를 사용하면 되고, 서브쿼리에서도 as()를 사용할 수 있습니다😊

 

@QueryProjection 활용📒

@QueryProjection을 활용하는 방법은 해당 DTO 생성자에 어노테이션을 삽입하여 사용합니다. 이 방법은 컴파일러로 타입을 체크할 수 있으므로 런타임 시에 오류가 나는 것이 아닌, 컴파일 단계에서 오류를 찾을 수 있기 때문에 가장 안전한 방법입니다.

 

하지만, DTO에 QueryDSL 어노테이션을 유지해야 하는 점DTO까지 Q파일을 생성해야 하는 단점이 있습니다.

 

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

이전에 만든 MemberDto 클래스 생성자에 어노테이션을 삽입하고 compile하여 QMemberDto를 생성해 줍니다.

 

Q 타입 생성
Q 타입 생성

@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);

    }
}

테스트 결과
테스트 결과

테스트 결과는 이전 테스트들과 동일하게 성공하는 것을 확인할 수 있었습니다😄

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

 

마치며

지금까지 QueryDSL의 Projection에 대해 다양한 방법들을 알아보았습니다. 사실 DTO는 getter/setter를 대부분 사용하기 때문에 가장 기본적인 bean() 방식을 사용하는 것이 제일 좋아 보입니다. 하지만 회사마다 각 팀마다 코드를 구성하는 스타일이 각기 다르기 때문에 여러 방법이 있다고 숙지만 해두셔도 좋을 것 같습니다😊

 

QueryDSL Series

1. QueryDSL 적용 및 예제 시리즈 시작

2. QueryDSL 조회 예제

3. QueryDSL 정렬 & 페이징 & 집합

4. QueryDSL 조인 예제

5. QueryDSL Projection(현재)

6. QueryDSL 동적 쿼리 & Bulk 연산

7. QueryDSL 실무 활용

 

Reference

실전! Querydsl

 

반응형

'JPA' 카테고리의 다른 글

QueryDSL 실무 활용  (0) 2022.11.18
QueryDSL 동적 쿼리 및 Bulk 연산  (1) 2022.11.15
QueryDSL 조인 사용 방법  (0) 2022.11.01
QueryDSL 정렬과 페이징 처리 및 집합 예시  (2) 2022.10.31
QueryDSL 사용하는 조회 예제  (0) 2022.10.28