5 분 소요

인프런 실전! Querydsl 강의 내용 정리

스프링 데이터 JPA가 지원하는 Querydsl 기능 1

여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다.

리포지토리 지원 - QuerydslRepositorySupport

장점

  • getQuerydsl().applyPagination()스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능. (단! Sort는 오류 발생)
  • from()으로 시작 가능(최근에는 QueryFactory를 사용해서 select()로 시작하는 것이 더 명시적)
  • EntityManager 재공

한계

  • Querydsl 3.x버전을 대상으로 만듦
  • Querydsl 4.x에 나온 JPAQueryFactory 로 시작할 수 없음
    • select로 시작할 수 없음 (from으로 시작해야 함)
  • QueryFactory를 제공하지 않음
  • 스프링 데이터 Sort 기능이 정상 동작 하지 않음.

MemberRepositoryImpl

1
2
3
4
5
6
7
8
9
10
11
12
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom { 

	private final JPAQueryFactory queryFactory;  
	  
	/*public MemberRepositoryImpl(EntityManager em) {  
	    this.queryFactory = new JPAQueryFactory(em);}*/  
	  
	public MemberRepositoryImpl(EntityManager em) {  
	    super(Member.class);  
	    this.queryFactory = new JPAQueryFactory(em);  
	}
}

extends QuerydslRepositorySupport 이걸 받아주고 생성자에

1
2
3
4
public MemberRepositoryImpl(EntityManager em) {  
	super(Member.class);  
	this.queryFactory = new JPAQueryFactory(em);  
}

다음과 같이 받아주면 사용이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override  
public List<MemberTeamDto> search(MemberSearchCondition condition) {  
    return queryFactory  
            .select(new QMemberTeamDto(  
                    member.id.as("memberId"),  
                    member.username,  
                    member.age,  
                    team.id.as("teamId"),  
                    team.name.as("teamName")  
            ))  
            .from(member)  
            .where(  
                    usernameEq(condition.getUsername()),  
                    teamNameEq(condition.getTeamName()),  
                    ageGoe(condition.getAgeGoe()),  
                    ageLoe(condition.getAgeLoe())  
            )  
            .leftJoin(member.team, team)  
            .fetch();  
}

자 이걸 한번 바꿔보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<MemberTeamDto> fetch = from(member)  
	.leftJoin(member.team, team)  
	.where(  
			usernameEq(condition.getUsername()),  
			teamNameEq(condition.getTeamName()),  
			ageGoe(condition.getAgeGoe()),  
			ageLoe(condition.getAgeLoe())  
	)  
	.select(new QMemberTeamDto(  
			member.id.as("memberId"),  
			member.username,  
			member.age,  
			team.id.as("teamId"),  
			team.name.as("teamName")  
	))  
	.fetch();

이런 식으로 from()부터 바로 시작할 수 있다. queryFactory 사용 없이.

흠..

그럼 유일한 장점을 함 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JPQLQuery<MemberTeamDto> jpaQuery = from(member)  
        .leftJoin(member.team, team)  
        .where(  
                usernameEq(condition.getUsername()),  
                teamNameEq(condition.getTeamName()),  
                ageGoe(condition.getAgeGoe()),  
                ageLoe(condition.getAgeLoe())  
        )  
        .select(new QMemberTeamDto(  
                member.id.as("memberId"),  
                member.username,  
                member.age,  
                team.id.as("teamId"),  
                team.name.as("teamName")  
        ));  
  
JPQLQuery<MemberTeamDto> query = getQuerydsl().applyPagination(pageable, jpaQuery);  
  
List<MemberTeamDto> results = query.fetch();

페이징이 있는 querydsl 구문인데 다음과 같이
JPQLQuery<MemberTeamDto> query = getQuerydsl().applyPagination(pageable, jpaQuery);

을 사용해서

1
2
.offset(pageable.getOffset())  
.limit(pageable.getPageSize())

이걸 쿼리에서 안 넣어 줘도 된다….. 이게 장점이라고 한다..

아무튼 몇 줄 축약할 수 있다..

난 안쓸꺼다.

Querydsl 지원 클래스 직접 만들기

스프링 데이터가 제공하는 QuerydslRepositiorySupport가 지닌 한계를 극복하기 위해 직접 Querydsl 지원 클래스를 만들어 보자.

장점

  • 스프링 데이터가 제공하는 페이징을 편리하게 변환
  • 페이징과 카운트 쿼리 분리 가능
  • 스프링 데이터 Sort 지원
  • select(), selectFrom()으로 시작 가능
  • EntityManager, QueryFactory 제공

영한님이 만드신 Querydsl4RepositorySupport.java 이다.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package study.querydsl.repository.support;  
  
import com.querydsl.core.types.EntityPath;  
import com.querydsl.core.types.Expression;  
import com.querydsl.core.types.dsl.PathBuilder;  
import com.querydsl.jpa.impl.JPAQuery;  
import com.querydsl.jpa.impl.JPAQueryFactory;  
import jakarta.annotation.PostConstruct;  
import jakarta.persistence.EntityManager;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.data.domain.Page;  
import org.springframework.data.domain.Pageable;  
import org.springframework.data.jpa.repository.support.JpaEntityInformation;  
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;  
import org.springframework.data.jpa.repository.support.Querydsl;  
import org.springframework.data.querydsl.SimpleEntityPathResolver;  
import org.springframework.data.support.PageableExecutionUtils;  
import org.springframework.stereotype.Repository;  
import org.springframework.util.Assert;  
  
import java.util.List;  
import java.util.function.Function;  
  
@Repository  
public abstract class Querydsl4RepositorySupport {  
    private final Class domainClass;  
    private Querydsl querydsl;  
    private EntityManager entityManager;  
    private JPAQueryFactory queryFactory;  
  
    public Querydsl4RepositorySupport(Class<?> domainClass) {  
        Assert.notNull(domainClass, "Domain class must not be null!");  
        this.domainClass = domainClass;  
    }  
  
    @Autowired  
    public void setEntityManager(EntityManager entityManager) {  
        Assert.notNull(entityManager, "EntityManager must not be null!");  
        JpaEntityInformation entityInformation =  
                JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);  
        SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;  
        EntityPath path = resolver.createPath(entityInformation.getJavaType());  
        this.entityManager = entityManager;  
        this.querydsl = new Querydsl(entityManager, new  
                PathBuilder<>(path.getType(), path.getMetadata()));  
        this.queryFactory = new JPAQueryFactory(entityManager);  
    }  
    @PostConstruct  
    public void validate() {  
        Assert.notNull(entityManager, "EntityManager must not be null!");  
        Assert.notNull(querydsl, "Querydsl must not be null!");  
        Assert.notNull(queryFactory, "QueryFactory must not be null!");  
    }  
    protected JPAQueryFactory getQueryFactory() {  
        return queryFactory;  
    }  
    protected Querydsl getQuerydsl() {  
        return querydsl;  
    }  
    protected EntityManager getEntityManager() {  
        return entityManager;  
    }  
    protected <T> JPAQuery<T> select(Expression<T> expr) {  
        return getQueryFactory().select(expr);  
    }  
    protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {  
        return getQueryFactory().selectFrom(from);  
    }  
    protected <T> Page<T> applyPagination(Pageable pageable,  
                                          Function<JPAQueryFactory, JPAQuery> contentQuery) {  
        JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());  
        List<T> content = getQuerydsl().applyPagination(pageable,  
                jpaQuery).fetch();  
        return PageableExecutionUtils.getPage(content, pageable,  
                jpaQuery::fetchCount);  
    }  
    protected <T> Page<T> applyPagination(Pageable pageable,  
                                          Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory,  
            JPAQuery> countQuery) {  
        JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());  
        List<T> content = getQuerydsl().applyPagination(pageable,  
                jpaContentQuery).fetch();  
        JPAQuery countResult = countQuery.apply(getQueryFactory());  
        return PageableExecutionUtils.getPage(content, pageable,  
                countResult::fetchCount);  
    }  
}

이걸로 뭘 할 수 있는지 보자. MemberTestRepository.java를 만들어 보자.

1
2
3
4
5
public class MemberTestRepository extends Querydsl4RepositorySupport { 
	public MemberTestRepository() {  
	    super(Member.class);  
	}
}

이렇게 세팅해 놓으면 사용 준비가 끝났다.

먼저 간단하게 리스트 가져오는 메서드를 만들어 보자.

1
2
3
4
5
6
7
8
9
10
public List<Member> basicSelect() {  
    return select(member)  
            .from(member)  
            .fetch();  
}  
  
public List<Member> basicSelectFrom() {  
    return selectFrom(member)  
            .fetch();  
}

많이 줄어들었다.. ㅋㅋ

그 다음 앞에서 QuerydslRepositiorySupport를 이용해 만든 페이징 쿼리와 영한님이 만든 support 를 이용한 쿼리를 비교해 보자.

QuerydslRepositiorySupport 페이징

1
2
3
4
5
6
7
8
9
10
11
12
13
public Page<Member> searchPageByApplyPage(MemberSearchCondition condition, Pageable pageable) {  
    JPAQuery<Member> query = selectFrom(member)  
            .leftJoin(member.team, team)  
            .where(usernameEq(condition.getUsername()),  
                    teamNameEq(condition.getTeamName()),  
                    ageGoe(condition.getAgeGoe()),  
                    ageLoe(condition.getAgeLoe())  
            );  
  
    List<Member> content = getQuerydsl().applyPagination(pageable, query).fetch();  
  
    return PageableExecutionUtils.getPage(content, pageable, query::fetchCount);  
}

자 전에 배운 걸로 만든 것이고
List<Member> content = getQuerydsl().applyPagination(pageable, query).fetch(); 체인이 한번 끊어져야 하지만 짧아지긴 했다.

Querydsl4RepositorySupport (영한) - 페이징

1
2
3
4
5
6
7
8
9
10
11
12
public Page<Member> applyPagination(MemberSearchCondition condition, Pageable pageable) {  
    return applyPagination(pageable, query ->  
            query.selectFrom(member)  
                    .leftJoin(member.team, team)  
                    .where(  
                            usernameEq(condition.getUsername()),  
                            teamNameEq(condition.getTeamName()),  
                            ageGoe(condition.getAgeGoe()),  
                            ageLoe(condition.getAgeLoe())  
                    )  
    );  
}

안 끊기고 한번에 쭉 가는 걸 볼 수 있다. 그 대신

1
2
3
4
5
6
7
8
protected <T> Page<T> applyPagination(Pageable pageable,  
                                      Function<JPAQueryFactory, JPAQuery> contentQuery) {  
    JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());  
    List<T> content = getQuerydsl().applyPagination(pageable,  
            jpaQuery).fetch();  
    return PageableExecutionUtils.getPage(content, pageable,  
            jpaQuery::fetchCount);  
}

실제 applyPagination() 메서드를 까보면 두 번째 파라미터가 함수다.

그래서 query -> query.select... 이런 식으로 람다식을 넣어줬다.

그리고 applyPagination() 메서드를 잘 보면 QuerydslRepositiorySupport를 그대로 옮겨서 구현해 놓은 걸 볼 수 있다. 이로서 실제 구현 시에는 짧은 코드를 만들 수 있게 된다.

나도 이렇게 라이브러리 들을 내 방식대로 개선해 나갈 수 있는 개발자가 되고 싶다.

마지막으로 페이징 , 카운터 쿼리를 나눠서 하는 메서드를 만들어 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Page<Member> applyPagination2(MemberSearchCondition condition, Pageable pageable) {  
    return applyPagination(pageable,  
            contentQuery -> contentQuery  
                    .selectFrom(member)  
                    .leftJoin(member.team, team)  
                    .where(  
                            usernameEq(condition.getUsername()),  
                            teamNameEq(condition.getTeamName()),  
                            ageGoe(condition.getAgeGoe()),  
                            ageLoe(condition.getAgeLoe())  
                    ),  
  
            countQuery -> countQuery  
                    .select(member.id)  
                    .from(member)  
                    .leftJoin(member.team, team)  
                    .where(  
                            usernameEq(condition.getUsername()),  
                            teamNameEq(condition.getTeamName()),  
                            ageGoe(condition.getAgeGoe()),  
                            ageLoe(condition.getAgeLoe())  
                    )  
            );  
}

이건 다를 거 없고 파라미터가 3개이고, 3번째 파라미터는 카운터 쿼리 함수를 받는다.

이로써 Querydsl 강의가 끝났다… JPA를 배우면서 정말 재밌었고, 이런 기능이 있다는 거에 신기했고, 벌써 끝나서 아쉽다..
이제 내가 직접 사용해 보면서 실력을 더 쌓아가야 한다.

태그: ,

카테고리:

업데이트:

댓글남기기