본문 바로가기

Spring boot

QueryDSL 데이터 조회 시, Entity, Tuple, DTO 사용법 + 각 장단점

QueryDSL은 select 절에서 원하는 컬럼만 조회할 수 있다. (DB와 서버 사이에 사용하는 네트워크 사용량도 줄고 물론 속도도 줄어듦)

데이터를 조회할 때, Entity나 Tuple을 직접 조회하거나 DTO를 사용해 데이터를 조회해오는 방법이 있다. 

모두 장단점이 있으니, 아래에서 설명해보겠다. 

 

Entity 사용하기 

Entity 조회시 Hibernate 캐시, 불필요한 컬럼 조회, OneToOne N+1 쿼리 등 단순 조회 기능에서는 성능 이슈 요소가 많다.

(OneToOne 관계는 null인 경우 때문에 Lazy Loading을 허용하지 않음. 따라서 연관된 entity전체가 아닌 entity의 id만 필요해도 entity 전체를 조회함 => store.image.id.as("imageId")로 사용하기)- as는 eneity 필드명과 DTO 필드명이 다르면 사용. 맨아래 참고

따라서 Entity를 직접 조회하는 것은 성능상 좋지 않다. 

웬만해서는 사용을 하지 않도록 하자

 

 

Tuple 사용하기

Tuple은 com.querydsl.core.Tuple 을 사용한다.

public List<Tuple> getStoreWithDistrict() {
        return queryFactory
                .select(store.district, store.id)
                .from(store)
                .groupBy(store.district)
                .fetch();
}

장점

가져오고 싶은 데이터를 제일 간단하게 가져올 수 있음 (dto를 만들지 않아도 됨 -> 파일 수가 줄어든다.)

 

단점 

- service나 controller 계층으로 데이터를 이동시, Tuple 패키지 자체가 querydsl 에 종속적이라 repository 계층의 종속성을 끌고 다른 계층으로 이동하는 것과 같다.  

- 다른 계층으로 이동을 시킨다해도 Tuple에서 데이터를 꺼내기 어렵다. (tuple에서 get을 하여 데이터를 꺼낼 때, Qclass를 기준으로 꺼낸다.)

 

 

※ 여기서 조회한 response를 그대로 controller에서 반환한다해도 DTO를 사용하기를 추천..! ※

(모든 계층을 돌아다니는 데이터는 모든 계층에 의존성이 엮여 위험하기 때문..!)


DTO 사용하기

DTO를 사용하기 위해서는 아래 세가지 종류가 있다. 

- 프로퍼티 접근 (bean)

- 필드 직접 접근 (fields)

- 생성자 사용 (constructor)

- QueryProjection (@QueryProjection)

 


프로퍼티 접근  사용하기

public List<CountDTO> getStoreWithDistrict() {
        return queryFactory
                .select(Projections.bean(CountDTO.class, 
               	    store.district,
                    store.id
                )
                .from(member)
                .fetch();
 }

단점 

- DTO 의 필드 네임과 조회하는 컬럼의 이름이 같아야함. ( DTO에 필드가 district, id가 있다면, select bean 에도 store.district, store.id로 필드명이 동일해야함.) 만약 다르다면 맨 아래 참고 

- DTO에 setter로 값을 주입하기 때문에 DTO에 꼭 setter가 있어야 함.

 

 

필드 직접 접근 사용하기

public List<CountDTO> getStoreWithDistrict() {
        return queryFactory
                .select(Projections.fields(CountDTO.class, 
               	    store.district,
                    store.id
                )
                .from(member)
                .fetch();
 }

단점

- DTO 의 필드 네임과 조회하는 컬럼의 이름이 같아야함. ( DTO에 필드가 district, id가 있다면, select bean 에도 store.district, store.id로 필드명이 동일해야함.) 만약 다르다면 맨 아래 참고

 

 

생성자 사용 사용하기

public List<CountDTO> getStoreWithDistrict() {
        return queryFactory
                .select(Projections.constructor(CountDTO.class, 
               	    store.district,
                    store.id
                )
                .from(member)
                .fetch();
 }

장점

- 값을 조회해 다른 값으로 변경한다던지, 다른 연관된 값을 부르던지 등등 생성자를 통해 데이터를 변경할 수 있음.

 

단점 

- DTO에 생성자로 값을 주입하기 때문에 DTO에 꼭 생성자가 있어야 함.

 

 

 

QueryProjection 사용하기

생성자에 @QueryProjection 어노테이션을 붙여야 한다.

import com.querydsl.core.annotations.QueryProjection;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class CountDTO {

    private String district;

    private Long id ;

    @QueryProjection
    public CountDTO(String district, Long id) {
        this.district = district;
        this.id = id;
    }
}
public List<CountDTO> getStoreWithDistrict() {
        return queryFactory
                .select(new CountDTO(store.district,store.id))
                .from(member)
                .fetch();
}

장점

- 컴파일 시점에 타입 체크, 파라미터 갯수체크 등등 많은 오류를 잡을 수 있다. 

- 값을 조회해 다른 값으로 변경한다던지, 다른 연관된 값을 부르던지 등등 생성자를 통해 데이터를 변경할 수 있음.

 

단점

- Qfile이 생성됨.

- DTO이 QueryDSL 의존성을 가짐.

 

 

 

 

참고 ) DTO 필드와 Qclass 필드 이름을 맞춰야 할 땐?

프로퍼티 접근필드 직접 접근 방법은 DTO 필드 이름과 Qclass 필드 이름이 동일해야 한다. 

하지만 같지 않다면 아래와 같이 .as("") 방법으로 내가 Qclass의 필드이름을 임의로 DTO 이름과 동일하게 변경시킬 수 있다. 

 

예시)

CountDTO에는 String district와 Long count 필드가 있다.

store.id의 필드명인 id를 count와 동일하도록 바꾸기 위해 .as("count")로 설정해주었다. 

public List<CountDTO> getDistrictCount() {
        return queryFactory
                .select(Projections.fields(CountDTO.class,
                                           store.district,
                                           store.id.count().as("count")
                ))
                .from(store)
                .groupBy(store.district)
                .fetch();
}

 

 

참고 ) 이미 알고 있는 값을 또 조회할 필요있나?

성능개선을 위해 이미 알고 있는 값은 조회해오지 않도록 하자

이미 아는 값은 asXX 표현식으로 대체할 수 있기 때문에 select에서 제외시킬 수 있다.

public List<CountDTO> getDistrictCount (Long storeId){
    return queryFactory
                .select(Projections.fields(CountDTO.class,
                                           store.district,
                                           Expressions.asNumber(id).as("storeId")
                ))
                .from(store)
                .groupBy(store.district)
                .fetch();
}

 


총 정리 

DTO의 필드 이름과 조회하는 엔티티의 필드 이름이 동일하다면 필드 바로 접근 방법

조회한 값을 변경할 필요가 있다면 생성자 사용 방법

오류는 치명적이니 컴파일에 오류를 잡고 싶다면 QueryProjection 방법

 

 

 

참고한 사이트 

https://backtony.github.io/jpa/2021-10-04-jpa-querydsl-6/