Study/실전! QueryDSL

Day 5. Spring Data JPA와 QueryDSL

주지민 2021. 10. 6. 21:13
반응형

본 포스트는 인프런 - 실전! QueryDSL의 강의를 듣고 공부한 내용입니다

https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지 한번에 해결, 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의 입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. 로드

www.inflearn.com


1. 사용자 정의 레포지토리 ( 기존 JpaRepository와 통합 ) 

  • 기존 Spring Data JPA를 사용하기 위해서는 JpaRepository 인터페이스를 상속받아 사용하기만 하면 됐지만, 추가적인 QueryDSL을 적용하기 위해서는 Custom Repository를 추가로 생성하여 구현해줘야한다
  • 구현 클래스 다이어그램

  • 예제 코드
    // MemberRepository
    public interface MemberRepository extends JpaRepository<Member, Long>, CustomMemberRepository {
    
        List<Member> findByUserName(String userName);
    }​

    // CustomMemberRepository
    public interface CustomMemberRepository {
        List<MemberTeamDto> search(MemberSearchCondition condition);
    }​
  • public class MemberRepositoryImpl implements CustomMemberRepository {
    
        private final JPAQueryFactory jpaQueryFactory;
    
        public MemberRepositoryImpl(EntityManager entityManager) {
            this.jpaQueryFactory = new JPAQueryFactory(entityManager);
        }
    
        @Override
        public List<MemberTeamDto> search(final MemberSearchCondition condition) {
    
            return jpaQueryFactory
                .select(new QMemberTeamDto(
                    member.id.as("memberId"),
                    member.userName,
                    member.age,
                    team.id.as("teamId"),
                    team.name
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                    userNameEq(condition.getUserName()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
                )
                .fetch();
        }
    
        private BooleanExpression userNameEq(String userName) {
            return StringUtils.hasText(userName)? member.userName.eq(userName) : null;
        }
    
        private BooleanExpression teamNameEq(String teamName) {
            return StringUtils.hasText(teamName)? team.name.eq(teamName) : null;
        }
    
        private BooleanExpression ageGoe(Integer ageGoe) {
            return Objects.nonNull(ageGoe)? member.age.goe(ageGoe) : null;
        }
    
        private BooleanExpression ageLoe(Integer ageLoe) {
            return Objects.nonNull(ageLoe)? member.age.loe(ageLoe) : null;
        }
    }
  • 구현 설명
    • JpaRepository를 상속하여 MemberRepository에 사용할 Entity와 @Id값을 명시하고 메서드 이름으로 생성되는 JPQL인 findByUserName 메서드를 정의
    • QueryDSL을 쓰기위해서는 구현체가 있는 Custom한 Repository가 필요, CustomMemberRepository를 생성한 후에 사용할 메서드 템플릿 생성(search)
    • 그 후 커스텀 레포지토리를 상속받아 실제 사용할 Repository 이름 + Impl의 이름을 갖는 MemberRepositoryImpl 클래스(구현체)를 생성하여 QueryDSL 내용 추가 ( 구현 클래스 이름 규칙은 반드시 규칙에 따라야 한다 )
    • 구현체 주입등은 Spring Data JPA가 알아서 해준다
  • 사용 방식 분석
    • 핵심 비즈니스 로직으로 재사용 가능성이 있고, 엔티티 검색하는 것들은 위의 방법처럼 통합 Repository로 정의
    • 라이프 싸이클 자체가 특정 부분(UI 등등)이라면 그냥 Repository를 분리해서 Class(@Repository)를 정의하는 것도 좋을것 같다

 

2. QueryDSL 페이징

  • 간단한 버전
    • fetchResults() 메서드 활용
    • 다만 fetchResults()는 count 조회 쿼리와 contents 조회 쿼리 두개가 만들어져 나가기 때문에 불필요한 성능의 조회 쿼리가 나갈 수 있다. 유의해야한다
    • 예제 코드
      // Simple Page
      @Override
      public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
                                                  Pageable pageable) {
          QueryResults<MemberTeamDto> queryResults = jpaQueryFactory
              .select(new QMemberTeamDto(
                  member.id.as("memberId"),
                  member.userName,
                  member.age,
                  team.id.as("teamId"),
                  team.name
              ))
              .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();
      
          return new PageImpl<>(queryResults.getResults(),
                                pageable,
                                queryResults.getTotal());
      }​

      • Pageable 클래스를 파라미터로 받아 offset과 limit를 연결해준다
      • org.springframework.data.domain.Pageable
      • 참고로 Spring Page 번호는 0부터 시작이다 ( 0 index )
      • 반환형은 Page를 구현하고있는 PageImpl을 통해 Page 반환
    • 테스트 코드
          
      // Test
      @Test
      void searchPageSimpleTest() {
          // given
          MemberSearchCondition condition = new MemberSearchCondition();
          PageRequest pageRequest = PageRequest.of(0, 3);
      
          // when
          Page<MemberTeamDto> actual = memberRepository.searchPageSimple(condition, pageRequest);
      
          // then
          assertThat(actual.getSize()).isEqualTo(3);
          assertThat(actual.getContent())
              .extracting("userName")
              .containsExactly("memberA", "memberB", "memberC");
      }​
    • SQL Query
      # JPQL
      /* 
      select 
          count(member1)
      from 
          Member member1
      left join 
          member1.team as team 
      */ 
      
      # SQL Query
      select 
          count(member0_.member_id) as col_0_0_
      from 
          member member0_ 
      left outer join 
          team team1_ 
          on member0_.team_id=team1_.id;



      # JPQL
      /* 
      select 
          member1.id as memberId, 
          member1.userName, 
          member1.age, 
          team.id as teamId, 
          team.name
      from 
          Member member1
      left join 
          member1.team as team 
      */ 
      
      # SQL Query
      select 
          member0_.member_id as col_0_0_, 
          member0_.user_name as col_1_0_, 
          member0_.age as col_2_0_, 
          team1_.id as col_3_0_, 
          team1_.name as col_4_0_ 
      from 
          member member0_ 
      left outer join 
          team team1_ 
          on member0_.team_id=team1_.id 
      limit 3;​
  • totalCount와 contents를 따로 조회 ( fetchResults를 이용하지 않음 )
    • 예제 코드
      // totalCount + contents
      @Override
      public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
                                                   Pageable pageable) {
      
          // get TotalCount
          long totalCount = jpaQueryFactory
              .select(new QMemberTeamDto(
                  member.id.as("memberId"),
                  member.userName,
                  member.age,
                  team.id.as("teamId"),
                  team.name
              ))
              .from(member)
              .leftJoin(member.team, team)
              .where(
                  userNameEq(condition.getUserName()),
                  teamNameEq(condition.getTeamName()),
                  ageGoe(condition.getAgeGoe()),
                  ageLoe(condition.getAgeLoe())
              )
              .fetchCount();
      
          // get Contents
          List<MemberTeamDto> contents = jpaQueryFactory
              .select(new QMemberTeamDto(
                  member.id.as("memberId"),
                  member.userName,
                  member.age,
                  team.id.as("teamId"),
                  team.name
              ))
              .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();
      
          return new PageImpl<>(contents,
                                pageable,
                                totalCount);
      }​
    • 위에서도 설명했다시피 fetchResults()는 불필요한 성능을 가지는 totalCount 조회 쿼리를 날릴 수 도 있다
    • 그럴땐 totalCount와 contents 조회 로직을 분리하여 직접 호출하자
    • 다만 totalCount를 조회하는데 큰 복잡함이 없음에도 분리하는 작업에 시간을 쏟진 말자
  • 추가: totalCount 조회 성능 최적화
    • 때에 따라 totalCount는 생략 가능하다
      • 페이지의 시작이면서, contents 사이즈가 page 사이즈보다 작을때
      • 마지막 페이지 일때 ( offset + contents size로 전체 사이즈를 구할 수 있음 )
    • PageableExecutionUtils.getPage() 메서드를 이용하여 위 상황에서 정의된 totalCount 날릴지 말지 결정한다
    • org.springframework.data.support.PageableExecutionUtils
    • 수정한 코드
      // Using PageableExecutionUtils
      @Override
      public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
                                                   Pageable pageable) {
      
          // get TotalCount
          JPAQuery<MemberTeamDto> totalCountQuery = jpaQueryFactory
              .select(new QMemberTeamDto(
                  member.id.as("memberId"),
                  member.userName,
                  member.age,
                  team.id.as("teamId"),
                  team.name
              ))
              .from(member)
              .leftJoin(member.team, team)
              .where(
                  userNameEq(condition.getUserName()),
                  teamNameEq(condition.getTeamName()),
                  ageGoe(condition.getAgeGoe()),
                  ageLoe(condition.getAgeLoe())
              );
      
          // get Contents
          List<MemberTeamDto> contents = jpaQueryFactory
              .select(new QMemberTeamDto(
                  member.id.as("memberId"),
                  member.userName,
                  member.age,
                  team.id.as("teamId"),
                  team.name
              ))
              .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();
      
          return PageableExecutionUtils.getPage(contents,
                                                pageable,
                                                () -> totalCountQuery.fetchCount());
      }​
728x90
반응형