[Spring] 실전 QueryDsl + Spring Data Jpa-동적 쿼리와 성능 최적화 조회 (6)

hyewon jeong·2023년 6월 14일
0

TIL

목록 보기
132/138

2. 스프링 데이터 JPA 리포지토리로 변경

실전QueryDsl + 순수 Jpa -> 스프링 데이터 JPA 리포지토리로 변경

스프링 데이터 JPA - MemberRepository 생성

package study.querydsl.repository;
  import org.springframework.data.jpa.repository.JpaRepository;
  import study.querydsl.entity.Member;
  import java.util.List;
  public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}

스프링 데이터 JPA 테스트

 @SpringBootTest
  @Transactional
  class MemberRepositoryTest {
      @Autowired
      EntityManager em;
      @Autowired
      MemberRepository memberRepository;
      @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);
}
}        
          

**⚡️ Spring Data JPA 는 단순한 쿼리는 인터페이스를 구현하면 자동으로 구현체를 만들어들어 주는데, QueryDsl 처럼 복잡한 쿼리의 경우는 직접 사용자 정의 인터페이스와 구현체를 만들어 스프링 데이터 레포지토리에 사용자 정의 인터페이스를 상속해야한다.

**

2-1. 사용자 정의 리포지토리

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

  • 규칙
    사용자 정의 인터페이스 클래스 명
    = 사용자 정의 인터페이스 명 or 스프링 데이터 리포지토리명 + Impl
    해야 스프링 데이터 jpa 인식함

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

import study.querydsl.dto.MemberSearchCondition;
  import study.querydsl.dto.MemberTeamDto;
  import java.util.List;
  public interface MemberRepositoryCustom {
      List<MemberTeamDto> search(MemberSearchCondition condition);
  }

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

import static org.springframework.util.StringUtils.isEmpty;
  import static study.querydsl.entity.QMember.member;
  import static study.querydsl.entity.QTeam.team;
  public class MemberRepositoryImpl implements MemberRepositoryCustom {
      private final JPAQueryFactory queryFactory;
      public MemberRepositoryImpl(EntityManager em) {
          this.queryFactory = new JPAQueryFactory(em);
} @Override
//회원명, 팀명, 나이(ageGoe, ageLoe)
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
        .select(new QMemberTeamDto(
                member.id,
                member.username,
                member.age,
                team.id,
                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 isEmpty(username) ? null : member.username.eq(username);
}
      private BooleanExpression teamNameEq(String teamName) {
          return isEmpty(teamName) ? null : team.name.eq(teamName);
}
      private BooleanExpression ageGoe(Integer ageGoe) {
          return ageGoe == null ? null : member.age.goe(ageGoe);
}
      private BooleanExpression ageLoe(Integer ageLoe) {
          return ageLoe == null ? null : member.age.loe(ageLoe);
} }

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

  import java.util.List;
  public interface MemberRepository extends JpaRepository<Member, Long>,
  MemberRepositoryCustom {
      List<Member> findByUsername(String username);
  }

2-1-4. 커스텀 리포지토리 동작 테스트 추가

@Test
  public void searchTest() {
      Team teamA = new Team("teamA");
      Team teamB = new Team("teamB");
      em.persist(teamA);
      em.persist(teamB);
      Member member1 = new Member("member1", 10, teamA);
      Member member2 = new Member("member2", 20, teamA);
      Member member3 = new Member("member3", 30, teamB);
      Member member4 = new Member("member4", 40, teamB);
      em.persist(member1);
      em.persist(member2);
      em.persist(member3);
      em.persist(member4);
      MemberSearchCondition condition = new MemberSearchCondition();
      condition.setAgeGoe(35);
      condition.setAgeLoe(40);
      condition.setTeamName("teamB");
List<MemberTeamDto> result = memberRepository.search(condition);
   assertThat(result).extracting("username").containsExactly("member4");
  }

2-2. 스프링 데이터 페이징 활용1 - Querydsl 페이징 연동

스프링 데이터의 Page, Pageable을 활용해보자.

  • 전체 카운트를 한번에 조회하는 단순한 방법
  • 데이터 내용과 전체 카운트를 별도로 조회하는 방법

사용자 정의 인터페이스에 페이징 2가지 추가

package study.querydsl.repository;
  import org.springframework.data.domain.Page;
  import org.springframework.data.domain.Pageable;
  import study.querydsl.dto.MemberSearchCondition;
  import study.querydsl.dto.MemberTeamDto;
  import java.util.List;
  public interface MemberRepositoryCustom {
      List<MemberTeamDto> search(MemberSearchCondition condition);
      Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
  Pageable pageable);
      Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
  Pageable pageable);
  }

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

  • searchPageSimple(), fetchResults() 사용
/**
* 단순한 페이징, fetchResults() 사용
*/
  @Override
  public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
  Pageable pageable) {
      QueryResults<MemberTeamDto> results = queryFactory
              .select(new QMemberTeamDto(
                      member.id,
                      member.username,
                      member.age,
                      team.id,
                      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();
      List<MemberTeamDto> content = results.getResults();
      long total = results.getTotal();
      return new PageImpl<>(content, pageable, total);
  }

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

2-2-2. 전체 카운트를 별도로 조회하는 방법

searchPageComplex()

/**
* 복잡한 페이징
* 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
Pageable pageable) {
    List<MemberTeamDto> content = queryFactory
            .select(new QMemberTeamDto(
                    member.id,
                    member.username,
                    member.age,
                    team.id,
                    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();
    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);
    }
  • 전체 카운트를 조회 하는 방법을 최적화 할 수 있으면 이렇게 분리하면 된다. (예를 들어서 전체 카운트를 조 회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.)
  • 코드를 리펙토링해서 내용 쿼리과 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.

2-2-3. 스프링 데이터 페이징 활용2 - 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 new PageImpl<>(content, pageable, total);
    return PageableExecutionUtils.getPage(content, pageable,
  countQuery::fetchCount);
()-> countQuery.fetchCount()  
//countQuery::fetchCount

스프링 데이터 라이브러리가 제공

count 쿼리가 생략 가능한 경우 생략해서 처리 경우 2가지

  • 페이지 사이즈 : 한 페이지에 표시해야 할 데이터 갯수
  • 컨텐츠 사이즈 : 현재 페이지에 포함된 실제 데이터 갯수
  • offset : 원하는 페이지 번호

1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때

예 ) 페이지 사이즈가 10이고, 컨텐츠 사이즈가 3이라고 하면 count 쿼리가 생략 가능하다.

2. 마지막 페이지 일 때 (offset*pageSize + 컨텐츠 사이즈를 더해서 전체 사이즈 구함, 더 정확히는 마지막 페이지 이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때)

offset (마지막 페이지 일 경우) = currentPage-1

전체 사이즈 (total) = (currentPage - 1) * pageSize + 컨텐츠 사이즈
로 계산됩니다.

PageableExecutionUtils.getPage()

는 스프링 데이터의 유틸리티 클래스인 PageableExecutionUtils에 있는 메서드입니다. 이 메서드는 컨텐츠 데이터와 페이지 정보를 사용하여 Page 객체를 생성합니다.

PageableExecutionUtils.getPage() 메서드는 다음과 같은 형식을 가지고 있습니다:

public static <T> Page<T> getPage(List<T> content, Pageable pageable, Supplier<Long> totalSupplier)

content: 페이지에 포함될 데이터 목록입니다.
pageable: 페이지 요청 정보를 나타내는 Pageable 객체입니다.
totalSupplier: 전체 데이터 개수를 제공하는 Supplier 객체입니다. 이 객체는 get() 메서드를 호출하여 전체 데이터 개수를 동적으로 가져올 수 있어야 합니다.

getPage() 메서드는 content, pageable, totalSupplier를 사용하여 Page 객체를 생성합니다. Page 객체는 페이지에 필요한 정보를 담고 있는 인터페이스로, 컨텐츠 데이터와 페이지 정보를 함께 제공합니다. 이를 통해 클라이언트는 페이지 관련 정보를 받아와 페이징 처리를 할 수 있습니다.

주로 getPage() 메서드는 데이터베이스나 다른 데이터 소스에서 페이징된 결과를 가져올 때 사용됩니다. totalSupplier를 통해 전체 데이터 개수를 지연 로딩하여 필요한 경우에만 데이터베이스 쿼리를 실행하고, 그렇지 않은 경우에는 생략할 수 있습니다.

2-2-4. 컨트롤러 개발

@RestController
 @RequiredArgsConstructor
 public class MemberController {
     private final MemberJpaRepository memberJpaRepository;
     private final MemberRepository memberRepository;
     @GetMapping("/v1/members")
     public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition)
 {
         return memberJpaRepository.search(condition);
     }
     @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);
     }
}

http://localhost:8080/v2/members?size=5&page=2


참고
김영한 QueryDsl 강의 자료
실전QueryDsl + 순수 Jpa(1)-동적 쿼리와 성능 최적화 조회

profile
개발자꿈나무

0개의 댓글