[QueryDSL] 실무 활용 - 스프링 데이터 JPA와 Querydsl

윤경·2021년 12월 12일
1

QueryDSL

목록 보기
10/11
post-thumbnail

[1] 스프링 데이터 JPA 리포지토리로 변경

이번 시간에는 간단하게 순수 JPA로 만든 코드를 스프링 데이터 JPA로 바꿔보겠다.

✔️ MemberRepository.java인터페이스

public interface MemberRepository extends JpaRepository<Member, Long> {

    // select m from Member m where m.username = ?
    List<Member> findByUsername(String username);
}

테스트 파일 생성
✔️ MemberRepositoryTest.java
이전에 작성했던 코드를 가져와서 단축키를 이용해 한 번에 바꿔줌

@SpringBootTest
@Transactional
public class MemberRepositoryTest {

    @Autowired
    EntityManager em;

    @Autowired MemberRepository memberRepository;   // shift + fn + F6으로 변수명을 한꺼번에 바꿀 수 있음

    @Test
    public void basicTest() {
        Member member = new Member("member1", 10);
        memberRepository.save(member);

        Member findMember = memberRepository.findById(member.getId()).get();
        assertThat(findMember).isEqualTo(member);

        List<Member> result1 = memberRepository.findAll();
        assertThat(result1).containsExactly(member);

        List<Member> result2 = memberRepository.findByUsername("member1");
        assertThat(result2).containsExactly(member);
    }
}
  • Querydsl 전용 기능인 회원 search를 작성할 수 없다. ➡️ 사용자 정의 리포지토리 필요함

단축키

shift + fn + F6: 변수명 한꺼번에 바꾸기


[2] 사용자 정의 리포지토리

(querydsl을 쓰려면 결국 구현 코드를 만들어야 함.
스프링 데이터는 인터페이스로 동작함.)

📌 사용자 정의 리포지토리 사용법

  1. 사용자 정의 인터페이스 작성
  2. 사용자 정의 인터페이스 구현
  3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

사용자 정의 리포지토리 구성

1. 사용자 정의 인터페이스 작성

public interface MemberRepositoryCustom {

    List<MemberTeamDto> search(MemberSearchCondition condition);
}

2. 사용자 정의 인터페이스 구현

// MemberRepository + Impl 이라는 이름으로 만들어줘야 함
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    @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")
                ))
//                .selectFrom(member)   // (장점) 이렇게 select projection이 달라져도 where절 안에 있는 것들을 재사용할 수 있음
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetch();
    }

    // usernameEq, teamNameEq, ageGoe, ageLoe는 재사용 가능
    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName) : null;
    }

    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null ? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.loe(ageLoe) : null;
    }
}

3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {

    // select m from Member m where m.username = ?
    List<Member> findByUsername(String username);
}

다 됐으면 예전에 사용했던 searchTest의 내용을 가져와 memberRepository로만 수정해 테스트해보면 된다.


[3] 스프링 데이터 페이징 활용1 - Querydsl 페이징 연동

  • 스프링 데이터의 Page, Pageable을 활용할 것
    1. 전체 카운트를 한 번에 조회하는 단순한 방법
    1. 데이터 내용과 전체 카운트를 별도로 조회하는 방법

✔️ MemberRepositoryCustom.java에 아래의 코드 추가

    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);

1. 전체 카운트를 한 번에 조회하는 단순한 방법

✔️ MemberRepositoryImpl.java

    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> results = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();    // fetchReesults: querydsl이 count 쿼리랑 content 쿼리 이렇게 두 번 날림

        List<MemberTeamDto> content = results.getResults();
        long total = results.getTotal();

        return new PageImpl<>(content, pageable, total);
    }

➡️ searchPageSimple(), fetchResults() 사용

  • Querydsl이 제공하는 fetchResults()내용과 전체 카운트한 번에 조회할 수 있다. (실제 쿼리 2번 호출됨)
  • fetchResults()는 카운트 쿼리 실행시 필요없는 order by제거한다.

2. 데이터 내용과 전체 카운트를 별도로 조회하는 방법

    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // 내가 직접 total count 쿼리를 날리는 것
        long total = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetchCount();

        return new PageImpl<>(content, pageable, total);
    }

➡️ searchPageComplex() 사용

  • 전체 카운트를 조회하는 방법을 최적화 할 수 있다면 위와 같이 분리하면 된다.
    (예를 들어 전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있는 방법이다.)
  • 코드를 리펙토링하여 내용 쿼리전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.

[4] 스프링 데이터 페이징 활용2 - CountQuery 최적화

  • 스프링 데이터가 라이브러리를 제공한다.
  • count 쿼리를 생략할 수 있는 경우는
    - 페이지의 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때,
    - 마지막 페이지일 때 (마지막 페이지일 때는 offset이랑 컨텐츠 사이즈를 더해 전체 사이즈가 구해진다.)
        /**
         * 바로 위의 코드에서 CountQuery 최적화하기
         * PageableExecutionUtils.getPage()로 최적화
         */
        JPAQuery<Member> countQuery = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);

➡️ PageableExecutionUtils.getPage()로 최적화 할 수 있다.


[5] 스프링 데이터 페이징 활용3 - 컨트롤러 개발

✔️ MemberController.java

    @GetMapping("/v2/members")
    public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageSimple(condition, pageable);
    }

    @GetMapping("/v3/members")
    public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageComplex(condition, pageable);

v2 페이징 ➡️ page=1로 바꾸면 다음 페이지로 (제로 인덱스라 0부터 시작함)

스프링 데이터 정렬(Sort)

스프링 데이터 JPA자신의 정렬(Sort)을 Querydsl의 정렬 (OrderSpecifier)로 편리하게 변경하는 기능을 제공한다.

(이 부분은 뒤에서 다시 말씀하신다고 하심)

스프링 데이터 sort → Querydsl의 OrderSpecifier로 변환하기

⚠️ 정렬(sort)은 조건이 조금만 복잡해져도 Pageable의 Sort 기능을 사용하기 어렵다.
루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 sort를 사용하기 보다는 파라미터를 받아 직접 처리하는 것을 권장한다.


profile
개발 바보 이사 중

0개의 댓글