[ 김영한 Querydsl #4 ] - 중급 문법 (1) : 프로젝션

정동욱·2023년 7월 16일
0
post-thumbnail

이번 글에서는 Querydsl프로젝션에 대해 알아보겠습니다.

프로젝션이란 SELECT의 대상, 조회의 결과물입니다. 그러니까 특정 컬럼만 조회하거나, 흔히 *라고 하면 테이블 전체를 조회하는 것이죠. Querydsl에서는 프로젝션 대상이 하나면 해당 컬럼의 타입을 반환하고, 2개 이상일 경우 기본적으로 Tuple이라는 타입을 반환하게 되어 있습니다. 코드로 비교해보겠습니다.

// username이라는 하나의 문자열 컬럼만 반환
@Test
void StringProjection() {
	List<String> result = queryFactory
			.select(member.username)
			.from(member)
			.fetch();
}

// username과 age라는 두 컬럼 Tuple로 반환
@Test
void tupleProjection() {
	List<Tuple> result = queryFactory
			.select(member.username, member.age)
			.from(member)
			.fetch();
}

보면 바로 알겠지만 Tuple이라는 타입은 이전까지 전혀 본 적도 없을 뿐더러, Tuple에 담긴 데이터를 꺼내는 것도 복잡합니다. 그렇기 때문에 반환 Dto를 별도로 생성해 사용하는 것이 권장됩니다.

Querydsl에서 Dto를 반환하는 방법은 프로퍼티 접근(setter), 필드 접근, 생성자까지 총 3가지가 있습니다. Projections라는 객체를 이용하면 조회한 컬럼들을 Dto에 매핑에 변환해줍니다.

// 세터 방식 dto 반환
@Test
void dtoBySetter() {
	List<MemberDto> result = queryFactory
			.select(Projections.bean(MemberDto.class,
					member.username,
					member.age))
			.from(member)
			.fetch();
}

// 필드 주입 방식 dto 반환
@Test
void dtoByField() {
	List<MemberDto> result = queryFactory
			.select(Projections.fields(MemberDto.class,
					member.username,
					member.age))
			.from(member)
			.fetch();
}

// 생성자 방식 dto 반환
@Test
void dtoByConstructor() {
	List<MemberDto> result = queryFactory
			.select(Projections.constructor(MemberDto.class,
					member.username,
					member.age))
			.from(member)
			.fetch();
}

언뜻 보기에는 셋 다 같은 코드처럼 보이나, Projections 뒤에 따라오는 변환 방식을 보면 다른 것을 확인할 수 있습니다. 주의해야 할 점은 조회한 컬럼과 Dto의 타입과 필드명이 동일해야한다는 점입니다. 만약 서브쿼리를 사용하거나 컬럼명과 필드명이 다를 경우 .as() 매서드를 통해 alias를 설정해줘야 합니다.

이 정도로도 기능적으로는 충분히 완성된 것처럼 보입니다만 Projections 객체를 매번 써주어야 한다는 점에서 가독성에 문제가 생깁니다. 또한 Dto의 필드는 2개인데 3개 이상의 컬럼을 조회하는 경우 컴파일 시점에서 이 에러를 잡아내지 못하기 때문에 런타임 에러를 야기할 수도 있습니다. 이에 대한 대안으로 @QueryProjection이라는 어노테이션이 있는데요, 이 어노테이션을 Dto의 생성자에 달아주면 이 Dto도 Q클래스로 생성해주어 만들어진 QDto를 사용할 수 있게 해줍니다.

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

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

먼저 Dto를 생성하고 생성자를 안에 만들어 준 뒤 @QueryProjection 어노테이션을 붙여줍니다. 그런 다음 프로젝트를 컴파일해 Q클래스가 생기도록 해주고 이를 사용하면 됩니다.

// @QueryProjection 어노테이션 Dto 반환
@Test
void dtoByQueryProjection() {
	List<MemberDto> result = queryFactory
			.select(new QMemberDto(member.username, member.age))
			.from(member)
			.fetch();
}

코드가 굉장히 깔끔해진 걸 볼 수 있습니다. 하지만 이 방식에도 문제점이 있는데요, Dto도 QueryType 클래스로 만들어야 한다는 점입니다. 사실 이 부분은 크게 문제될 것은 없다고 생각합니다. 더 큰 문제는 Dto는 원래 데이터를 전달하기 위해 존재하는 객체기 때문에 순수한 형태로 있어야 하지만, @QueryProjection 어노테이션을 쓰는 순간 이 Dto 객체도 Querydsl에 의존하게 되어버린다는 것입니다. 이는 아키텍처적인 문제로, 이 Dto 객체는 여러 계층에서 사용되기 때문에 사용되는 곳에서도 Querydsl을 알아야 한다는 것이죠.

물론 실제 프로젝트에서의 상황에 맞게끔 조정해서 사용하는 게 맞지만, 개인이나 사이드 프로젝트에서는 웬만하면 @QueryProjection 어노테이션 방식을 사용하는 것이 가장 효율적이라는 생각이 듭니다. 다음 글에서는 Querydsl으로 동적 쿼리를 작성하는 법에 대해 알아보겠습니다.

profile
거인의 어깨 위에서 탭댄스를

0개의 댓글