문홍의 공부장

[Java] Querydsl @QueryProjection, transform() 을 활용하여 효율적으로 Result Handling 하기 본문

개발/Java

[Java] Querydsl @QueryProjection, transform() 을 활용하여 효율적으로 Result Handling 하기

moonong 2024. 1. 28. 22:11
반응형

 

애플리케이션을 개발하다 보면, 하나의 테이블에서 조회하는 단순 조회 쿼리 뿐 아니라, 다수의 테이블을 조인하여 값을 조회하는 경우가 매우 빈번하게 발생한다.

 

Querydsl은 결과를 사용자 정의하는 두 가지 방법, 즉 행 기반 변환을 위한 FactoryExpressions와 집계를 위한 ResultTransformer를 제공합니다.

 

Querydsl의 FactoryExpression 구현 기능은 com.mysema.query.types.Projections클래스를 통해 , ResultTransformercom.mysema.query.group.GroupBy를 통해 구현되어 있다.

 

1. Projection

Projection 이란, Querydsl을 이용해 entity전체를 가져오는 것이 아니라 조회 대상을 지정해 원하는 값만 조회하는 것을 말한다.

 

1-1. 기본값 - Tuple

기본적으로 Querydsl 에서 다양한 컬럼 값을 가져올 때에, Tuple 을 사용한다.
튜플은 Querydsl 의 객체이기 때문에, Controller, Service layer 로 이동하기에는 적절하지 않을 뿐더러, tuple.get() 을 통해 하나씩 직접 값을 꺼내와야 하는 번거로움이 있다.

 

List<Tuple> result = query.from(employee).list(employee.firstName, employee.lastName);
for (Tuple row : result) {
     System.out.println("firstName " + row.get(employee.firstName));
     System.out.println("lastName " + row.get(employee.lastName)); 
}}

 

이를 개선하기 위해 DTO(Model) 을 사용하는 방법을 채택한다.

 

1-2. Bean population

 

Projections.bean() 을 이용하면, DTO 객체의 각 필드에 setter 메서드를 통해 Projection 객체를 생성한다.
단순 조회용 DTO 클래스라면 괜찮을 수도 있으나, setter 메서드를 열어두는 것은 데이터의 변경이 어디에서 일어나는지 파악하기 어려워 사용을 지양하고 있다.

 

List<UserDTO> dtos = query.list(
    Projections.bean(UserDTO.class, user.firstName, user.lastName));

 

 

이에 두 번째 방법은 Projections.fields() 를 통해 getter/setter 메서드를 사용하지 않고 필드에 직접 값을 주입해주는 방법이다.

 

List<UserDTO> dtos = query.list(
    Projections.fields(UserDTO.class, user.firstName, user.lastName));

 

1-3. Constructor usage

 

Projections.constructor는 생산자 기반 바인딩이다. 생성자 기반 바인딩이기 때문에 객체의 불변성을 가져갈 수 있다는 장점이 있지, 필드의 개수가 많아지는 경우 실수가 발생하여 오류가 발생할 수 있다.

 

List<UserDTO> dtos = query.list(
    Projections.bean(UserDTO.class, user.firstName, user.lastName));

 

앞선 세 가지 방식 모두, 컴파일 시점에서는 오류를 발견하지 못하고, 런타임 시점에서 해당 메서드를 실행했을 때 오류가 발생하게 된다.

보다 직관적으로, 간편하게 객체를 바인딩하고 코드를 관리할 수는 없을까?

 

1-4. @QueryProjection

 

DTO의 생성자에 @QueryProjection 어노테이션을 추가하여 Q클래스를 생성하도록 한다. 이 방식은 생성자new QDTO()로 사용하기 때문에 불변 객체임을 보장하고, 런타임 에러뿐만 아니라 컴파일 시점에서도 에러를 잡아주는 장점이 있다. 또한, DTO는 Controller, Service layer 에서 모두 사용되기 때문에 3-layer 아키텍쳐 관점에서도 보다 적절하다.

 

class CustomerDTO {
  private Long id; 
  private String name; 

  @QueryProjection
  public CustomerDTO(Long id, String name){
     this.id = id; 
     this.name = name; 
  }

}
QCustomer customer = QCustomer.customer;
List<CustomerDTO> dtos = query
                        .from(customer)
                        .list(new QCustomerDTO(customer.id, customer.name));

 

2. GroupBy

2-1. Result aggregation - transform

 

클래스 com.mysema.query.group.GroupBy 는 쿼리 결과를 메모리에 집계하는 데 사용할 수 있는 집계 기능을 제공한다.

SQL 에서의 GROUP BY 절과는 달리, groupBy()aggregation에 필요한 데이터를 select절에 추가하는 역할과 select한 결과 값을 aggregation 하는 역할을 한다.

 

예를 들어, 다음과 같이 member 테이블과 team 테이블이 존재하고, team 테이블에 country 라는 속성이 있다고 가정한다. 그리고 나라별로 member 데이터를 모으고 싶을때, transform() 을 사용하지 않는다면 아래와 같이 쿼리한 후, Java Stream API 의 groupingBy() 를 이용하여 그룹핑해주어야 한다.

 

@Override public Collection<MemberByCountryProjection> findAllWithCountry() { 
    QMemberByCountryProjection selectExpression = new QMemberByCountryProjection(member, team); 
    return query.from(member)
        .select(selectExpression)
        .leftJoin(team)
        .on(member.teamId.eq(team.id))
        .fetch(); 
    }
---
public Map<String, List<MemberEntity>> groupingByCountry (){
    Map<String, List<MemberEntity>> actual = memberRepository
    .findAllWithCountry()
    .stream()
    .collect(Collectors.groupingBy(MemberByCountryProjection::getCountry, 
                                HashMap::new, 
                        Collectors.mapping(MemberByCountryProjection::getMember, Collectors.toList())));
}

 

 

transform() 을 활용하면 번거로움을 줄이고, 그룹핑하여 한번에 쿼리 결과를 뽑아낼 수 있다.

 

@Override 
public Map<String, List<MemberEntity>> groupByCountry() { 
    return query.from(member)
        .leftJoin(team)
        .on(member.teamId.eq(team.id))
        .transform(GroupBy.groupBy(team.country)
                    .as(list(member))); 
}

 

References.

반응형