본문 바로가기
데이터 접근 기술

Querydsl Projections

by oncerun 2023. 2. 9.
반응형

Querydsl에서 Projections을 많이 사용한다.

 

왜 많이 사용할까?

 

그 대답은 성능과 직결되기 때문이다. 

 

서버는 사용자의 요청에 응답을 해주어야 한다. 이 과정이 만약에 데이터베이스와 연결되어 있다면 데이터의 크기를 줄이는 작업 매우 큰 서비스에서는 성능과 직결될 수 있다. 

 

또한 클라우드를 사용하는 추세에 응답에 대한 트래픽에 대한 요금이 부과되기 때문에 무작정 모든 데이터를 끌어올려서

 

메모리에 넣어 매우 큰 데이터를 가공하여 응답하면 금전적으로도 손실이 발생하기 때문이다. 

 

프로그래밍적으로도 깔끔함을 유지할 수 있는 좋은 수단이 된다. 

 

요즘은 아키텍처의 각 Layer 간 결합도를 낮추기 위해 여러 가지 방법들이 동원된다. 

이 중 Projections을 사용하면 ui, service, repository 간 데이터 전송을 위한 DTO를 줄일 수 있다. 

 

다만 Projections을 사용하는 것은 특정 부분에 한정되어 보인다. 

 

데이터를 별다른 가공 없이, 커다란 비즈니스 로직 없이 다른 클라이언트로 전달해야 할 경우 그 스펙에 맞추어 전달하기 때문이다. 

 

서비스의 대부분의 요청이 조회라는 것을 감안하면 Projections 사용법을 익혀두면 상당한 도움이 될 것이다. 

그리고 Querydsl 공식문서가 그리 친절하지 않다는 것.. 

 

 

1. 단일 데이터

List<String> usernames = queryFactory
        .select(member.username)
        .from(member)
        .fetch();

단일 데이터를 언제 조회하게 될까? 

 

예를 들면 다음과 같은 경우가 있다.

 웹의 경우 select box에 고정된 데이터를 dropdown 방식으로 제공하는 경우가 많다.

이를 위해 모든 데이터를 클라이언트에게 던지면 프론트 영역에서는 필요 없는 데이터까지 알게 되기에 좋지 않은 방식이라고 할 수 있다. 

 

그래서 나는 적절한 쿼리로 단일 데이터만 올려 전달한다. 

 

2. 대상이 둘 이상인 경우

 

JPQL을 사용했을 때 모든 패키지명을 나열하는 과정을 사용한 적이 있다. 그리 좋은 경험은 아니었다.

 

가장 많이 사용되는 방법으로 DTO, Tuple을 사용한다.

List<Tuple> tuple = queryFactory
        .select(member.username, member.age)
        .from(member)
        .fetch();

 

Tuple은 querydsl이 제공하는 DTO정도로 생각하면 된다. 하지만 Tuple은 querydsl 기술이다. 이를 다른 계층까지는 넘겨서 infra 기술에 의존하도록 하지 말자. 

 

DTO를 사용한다면 다음과 같이 사용해 보자. 방법은 여러 가지가 존재한다.

  • setter
  • field
  • constructor

 

setter

 

기본 생성자가 필요하다. 이때 기본 생성자의 접근지시자는 public이어야만 가능한 것 같다.  

 

이는 getter/setter를 통해 들어간다.  

 

좋은 방법인지는 모르겠으나, setter를 통해 데이터를 애플리케이션에서 약간의 가공하는 데 사용하고 있다.

 

데이터베이스에서 가져온 데이터를 setter로 주입하기 때문에 가능한 방법이다.

List<MemberDto> result = queryFactory
        .select(Projections.bean(
                MemberDto.class,
                member.username,
                member.age
        ))
        .from(member)
        .fetch();

 

field

 

동일하게 기본 생성자가 필요하다.

이는 getter/setter를 사용하지 않고 필드에 주입시켜 준다.

List<MemberDto> result = queryFactory
        .select(Projections.fields(
                MemberDto.class,
                member.username,
                member.age
        ))
        .from(member)
        .fetch();

 

 

constructor

 

생성자 방식은 생성자의 타입을 맞춰줘야 한다. 이를 헷갈리면 의도치 않은 데이터가 바인딩될 수 있다.

 

생성자를 통해 생성되기 때문에 생성자에서 어떠한 가미도 할 수 있다. 다만 이는 유지보수가 어려워지는 냄새라고 생각도 든다.

 

생성자는 타입을 보고 들어간다.

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

 

필드명이 일치하지 않는 경우 as()를 통해 일치시켜 주어야 한다.

 

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

ExpressionUtils 함수가 제공하는 기능을 알아볼 필요가 있다.

 

 

@QueryProjection

 

여러 장점, 단점을 가지고 있지만 깔끔한 방식이다.

 

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

 

재밌는 건 DTO에 대한 QType 클래스가 생성된다.

@Generated("com.querydsl.codegen.DefaultProjectionSerializer")
public class QMemberDto extends ConstructorExpression<MemberDto> {

    private static final long serialVersionUID = -2112986775L;

    public QMemberDto(com.querydsl.core.types.Expression<String> username, com.querydsl.core.types.Expression<Integer> age) {
        super(MemberDto.class, new Class<?>[]{String.class, int.class}, username, age);
    }

}

 

그 결과 다음과 같이 사용할 수 있다.


List<MemberDto> result = queryFactory
        .select(new QMemberDto(member.username, member.age))
        .from(member)
        .fetch();

이는 컴파일 시점에 오류를 잡아낼 수 있고 Ctrl + P를 사용할 수 있다.

 

근데 단점은 뭘까?

 

1. QType 파일 생성한다는 점

2. DTO가 Querydsl에 대한 강력한 의존성이 생긴다는 점.

3. DTO가 여러 레이어를 거치는 아키텍처에서는 적절한 방법이 아닐 수 있다.

 

 

 

 

반응형

'데이터 접근 기술' 카테고리의 다른 글

Kotlin, Querydsl 설정  (0) 2023.07.08
OneToOne 연관관계에 대한 고민.  (0) 2023.02.09
Querydsl 기본(3)  (0) 2023.02.07
Querydsl 기본(2)  (0) 2023.02.06
QueryDSL 적용 방법 알아보기.  (0) 2023.02.06

댓글