Querydsl를 이용하는 경우 엔티티와 다른 반환 타입인 경우 Projections를 사용합니다. Projections을 하는 방법과 선호하는 패턴을 정리해보았습니다.
Projections을 이용해서 projection 하는 방법은 크게 3가지가 있습니다.
Projections.bean을 이용하는 방법
Projections.constructor를 이용하는 방법
@QueryProjection를 사용하는 방법
결론부터 말씀드리면 @QueryProjection을 사용하는 방법이 가장 좋다고 생각합니다. 각 패턴의 장단점을 설명드리겠습니다.
Projections.bean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class MemberDtoBean { var username: String? = null var age: Int ? = null } class ProjectionTest ( private val em: EntityManager ) : SpringBootTestSupport() { val query = JPAQueryFactory(em) ... @Test internal fun `projection bean`() { val members = query .select(Projections.bean( MemberDtoBean::class .java, qMember.username, qMember.age )) .from(qMember) .fetch() for (member in members) { println(member) } } }
Projections.bean 방식은 setter 기반으로 동작하게 됩니다. 그러기 때문에 MemberDtoBean객체의 setter 메서드를 열어야 합니다. 일반적으로 Response, Request 객체는 불변 객체를 지향하는 것이 바람직하다고 생각하기 때문에 권장하는 패턴은 아닙니다.
Projections.constructor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 data class MemberDtoConstructor ( val username: String, val age: Int ) class ProjectionTest ( private val em: EntityManager ) : SpringBootTestSupport() { val query = JPAQueryFactory(em) ... @Test internal fun `projection constructor `() { val members = query .select(Projections.constructor ( MemberDtoConstructor::class .java, qMember.username, qMember.age )) .from(qMember) .fetch() for (member in members) { println(member) } } }
Projections.constructor를 사용하면 생성자 기반으로 바인딩 하기 때문에 MemberDtoConstructor객체를 불변으로 가져갈 수 있습니다. 하지만 바인딩 시키는 작업에 문제가 있습니다.
1 2 3 4 5 .select(Projections.constructor ( MemberDtoConstructor::class .java, qMember.username, qMember.age ))
위 코드를 보면 MemberDtoConstructor객체 생성자에 바인딩 하는 것이 아니라 Expression<?>... exprs 값을 넘기는 방식으로 진행합니다.
즉 값을 넘길 때 생성자와 순서를 일치시켜야 합니다. 위처럼 개수가 몇 개 안될 때는 문제가 되지 않으나 값이 많아지는 경우 실수할 수 있는 문제가 발생할 수 있는 확률이 높습니다. 이러한 문제가 있어 권장하지 않은 패턴입니다.
@QueryProjection 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 data class MemberDtoQueryProjection @QueryProjection constructor ( val username: String, val age: Int ) class ProjectionTest ( private val em: EntityManager ) : SpringBootTestSupport() { val query = JPAQueryFactory(em) ... @Test internal fun `projection annotation `() { val members = query .select(QMemberDto( qMember.username, qMember.age )) .from(qMember) .fetch() for (member in members) { println(member) } } }
@QueryProjection를 이용하면 위에서 발생한 불변 객체 선언, 생성자 그대로 사용을 할 수 있어 권장하는 패턴입니다.
1 2 3 4 .select(QMemberDto( qMember.username, qMember.age ))
정확히는 MemberDtoQueryProjection의 생성자를 사용하는 것이 아니라. MemberDtoQueryProjection 기반으로 생성된 QMemberDtoQueryProjection 객체의 생성자를 사용하는 것입니다.
1 2 3 4 5 6 7 8 9 @Generated("com.querydsl.codegen.ProjectionSerializer") public class QMemberDtoQueryProjection extends ConstructorExpression <MemberDtoQueryProjection> { private static final long serialVersionUID = -277743863L ; public QMemberDtoQueryProjection (com.querydsl.core.types.Expression<String> username, com.querydsl.core.types.Expression<Integer> age) { super (MemberDtoQueryProjection.class, new Class <?>[]{String.class, int .class}, username, age); } }
QMemberDtoQueryProjection생성자는 MemberDtoQueryProjection 생성자의 변수명과 순서와 정확하게 일치합니다.
그래서 IDE의 자동완성 기능을 이용해서 보다 안전하고 편리하게 생성자에 필요한 값 바인딩을 진행할 수 있습니다. 그래서 가장 권장하는 패턴입니다.
물론 단점도 있습니다. Dto라는 특성상 해당 객체는 많은 계층에서 사용하게 됩니다. 그렇게 되면 Querydsl의 의존성이 필요 없는 레이어에서도 해당 의존성이 필요하게 됩니다.
저는 개인적으로 이 정도의 의존관계 때문에 발생하는 의존성 문제 보다 Projections를 안전하게 사용할 수 있는 방법이 더 효율적이라고 생각합니다.