[인강노트 - querydsl] 8. 스프링 데이터 JPA가 제공하는 Querydsl 기능

봄도둑·2023년 1월 5일
0

김영한님의 실전! querydsl 강의 내용을 정리한 노트입니다. 블로그에 있는 자료를 사용하실 때에는 꼭 김영한님 강의 링크를 남겨주세요!

💡 본격적인 내용에 들어가기 앞서 지금부터 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 적합한 기능은 아님. 기능을 풀어헤쳐서 직접 구현을 해야할 경우가 많음

1. 인터페이스 지원 - QuerydslPredicateExecutor

  • 먼저 repository에 QuerydslPredicateExecutor를 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom, QuerydslPredicateExecutor<Member> {
    //..
}
  • 테스트를 해보자 → 아래의 예제처럼 findAll에 조건을 넣을 수 있음
@Test
public void querydslPredicateExecutorTest() {
    //데이터 넣는 로직..

    Iterable<Member> result = memberRepository.findAll(member.age.between(10, 40).and(member.username.eq("member1")));

    for (Member findMember : result) {
        System.out.println("member1 : " + findMember);
    }
}
  • 이렇게 보면 QuerydslPredicateExecutor가 꽤 괜찮아보임 → 그러나 한계가 명확함
  • 우리는 기본적으로 RDB를 쓰기 때문에 join을 피할 수 없음 → 우리가 값을 가져올 때 기본적으로 하나의 테이블에서 원하는 값을 바로 가져오는 경우가 거의 없음. QuerydslPredicateExecutor는 join에 대한 지원이 없음
  • 클라이언트 코드가 Querydsl 코드에 의존해야 함 → repository는 QuerydslPredicateExecutor를 의존하고 있음 → 얘는 service, controller등 다른 계층에서 사용됨. repository라는 걸 만드는 이유는 다른 계층에서 repository를 만들고 그 하부에 구체화된 기술들(querydsl etc…)를 숨기기 위함. 다음에 구체화된 기술을 바꿀 때 하부 레벨만 바꿔주기만 하면 되는데 executor를 사용하게 되면 service, controller 레벨의 로직이 executor에 대해 의존관계가 생겨버림
  • 순수한 자바 클래스 객체를 넘겨주는 게 아니라 querydsl에 의존하는 객체가 넘어가게 됨 → 실무 수준에서 쓰는 것을 권장하지 않음.

2. Querydsl Web 지원

  • 단순한 조건만 가능
  • 조건을 커스텀하는 기능이 복잡하고 명시적이지 않음
  • 컨트롤러가 Querydsl에 의존
  • 복잡한 실무환경에서 사용하기에는 한계가 명확
  • 득보다 실이 많기 때문에 굳이 사용하는 걸 추천하지 않음

3. 리포지토리 지원 - QuerydslRepositorySupport

  • querydsl 라이브러리를 사용하기 위해 받을 구현체에 상속 받아서 사용하면 됨 → entityManager를 주입 받고 할 필요 없이 추상 클래스이기 때문에 super에 바로 도메인 클래스를 넘겨주기만 하면 됨
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
	
		public MemberRepositoryImpl() {
        super(Member.class);
    }

		//...
}
  • 부모 클래스인 QuerydslRepositorySupport에서 무엇을 제공해주는지 보자!

    • 기본적으로 EntityManager를 제공해주고 특이하게 Querydsl이란 것을 제공해줌
    • from, delete같은 것들을 쉽게 할 수 있는데 예제 코드를 살펴보자
  • from으로 시작하는 녀석을 만들 수 있음 → 그런데 왜 from 먼저 쓰는 게 좋음?

    • querydsl 3버전일 때 from이 만들어짐, queryFactory는 querydsl 4부터 사용이 가능해서 당시에는 이렇게 쓸 수 밖에 없었다라는 이야기
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
    return 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();
}
  • 엔티티매니저도 주입을 받고, 그 외에 레포지토리에서 해줘야 할 것들을 추상 클래스에서 가져와서 사용하면 되는 편리함이 있음
  • 그 외에도 pageable을 쉽게 처리할 수 있음 → getQuerydsl의 applyPagination을 부른 후 나온 결과값을 fetch 하면 offset, limit가 적용된 결과값을 받을 수 있음
@Override
    public Page<MemberTeamDto> searchPageSimple2(MemberSearchCondition condition, Pageable pageable) {
        JPQLQuery<MemberTeamDto> jpqlQuery = 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, jpqlQuery);
        List<MemberTeamDto> result = query.fetch();

	      //...
    }
  • 우리가 불렀던 applyPagination을 들어가보면 이 녀석이 offset, limit를 다 해줌
  • 장점
    • 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게(?) 변환 가능(막상 편한가라는 생각이 들기도…) → 그런데 Sort는 안됨
  • 그런데 나머지가 단점
    • select로 시작할 수 없음 → from으로 시작 ⇒ 우리는 기존에 select로 시작하는 게 익숙한데 그렇게 쓰지 못함
    • queryFactory를 쓸 수 없음
    • Spring data sort를 쓸 수 없음
    • 메소드 체인으로 코드를 쫙쫙 뽑아내야 하는데 중간에 메소드 체인이 끊기고 다른 로직을 수행해야 함 → 고작 코드 두 줄을 줄이기 위해 메소드 체인이 끊기는 코드 두 줄이 다시 추가됨

4. Querydsl 지원 클래스 직접 만들기

  • querydslRepositorySupport가 지닌 한계를 극복하기 위해 직접 querydsl 지원 클래스를 만들자
  • 참고로 이번 강의는 어드벤스 → 이번 강의를 잘 학습하면 라이브러리를 확장하거나 기능을 좀 더 좋게 쓰는 방법을 찾을 수 있음. 이런 식으로 나도 유틸리티 클래스를 만들 수 있구나, 코드를 더 줄일 수 있겠구나라는 답에 접근할 수 있음
  • 장점
    • 스프링 데이터가 제공하는 페이징을 편리하게 변환
    • 페이징과 카운트 쿼리 분리 가능
    • 스프링 데이터 osrt 지원
    • select, selectFrom으로 시작하도록 설정
    • entityManager, queryFactory 제공
  • 먼저 support 추상 클래스를 만들자 → 김영한님 제공
/**
 * Querydsl 4.x 버전에 맞춘 Querydsl 지원 라이브러리
 *
 * @author Younghan Kim
 * @see
org.springframework.data.jpa.repository.support.QuerydslRepositorySupport
 */
@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);
    }
}
  • 이녀석은 생성 시점에 setEntityManager를 통해 queryFactory 등 다 들고 시작함
  • 사용 코드를 간단히 만들어보자
public class MemberTestRepository extends Querydsl4RepositorySupport {

    public MemberTestRepository() {
        super(Member.class);
    }

    public List<Member> basicSelect() {
        return select(member)
                .from(member)
                .fetch();
    }

    public List<Member> basicSelectFrom() {
        return selectFrom(member)
                .fetch();
    }

}
  • select로 시작할 수 있는 건 support 클래스에 select를 살펴보자 → queryFactory에서 바로 select와 selectFrom을 던져주고 있음. 단순히 축약한 게 전부
@Repository
public abstract class Querydsl4RepositorySupport {
    //..
    protected <T> JPAQuery<T> select(Expression<T> expr) {
        return getQueryFactory().select(expr);
    }
		protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
        return getQueryFactory().selectFrom(from);
    }
		//..
}
  • 그런데 Page를 한 번 살펴보자 → 먼저 기존 코드 동작을 살펴보자(기존 querydslSupport)
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);
}
  • 그럼 커스텀한 querydslSupport로 써보자
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())
                    )
    );
}
  • searchPageByApplyPage와 applyPagination는 완전히 똑같은 코드임
  • 한 번 더 추상화한 querydsl4Support에 applyPagination을 추가함 → 이 녀석은 getQuerydsl().applyPagination(pageable, query).fetch()과 PageableExecutionUtils.getPage를 대신 해주고 있는 것 뿐. 그런데 실질적으로 쓰는 코드는 더 깔끔해짐
@Repository
public abstract class Querydsl4RepositorySupport {
    //..
    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);
    }
		//..
}
  • 자바8부터 람다를 쓰면서 굉장히 깔끔하게 쓸 수 있게 됨 → contentQuery.apply(getQueryFactory())는 파라미터로 넘어온 contentQuery를 실행해주는 것
  • 예제로 만들었던 카운트 쿼리를 따로 쏘는 방법을 구현해보자
public Page<Member> applyPagination2(MemberSearchCondition condition, Pageable pageable) {
    return applyPagination(pageable, contentQuery ->
            contentQuery.selectFrom(member)
                    .where(usernameEq(condition.getUsername()),
                            teamNameEq(condition.getTeamName()),
                            ageGoe(condition.getAgeGoe()),
                            ageLoe(condition.getAgeLoe())
                    ),
            countQuery ->
                    countQuery.select(member.id)
                            .from(member)
                            .where(usernameEq(condition.getUsername()),
                                    teamNameEq(condition.getTeamName()),
                                    ageGoe(condition.getAgeGoe()),
                                    ageLoe(condition.getAgeLoe())
    ));
}
  • 필요한 기능은 내가 추가해서 사용하면 됨 → 라이브러리의 불편한 점이 있다면 본인이 직접 구현해보는 것도 또하나의 방법이 될 수 있음
profile
Java Spring 백엔드 개발자입니다. java 외에도 다양하고 흥미로운 언어와 프레임워크를 학습하는 것을 좋아합니다.

0개의 댓글