두가지 페이징 메서드를 알아볼 것인데, 

searchPageSimple() 메서드는 전체 갯수를 한번에 조회하는 방법이고,

searchPageComplex()메서드는 전체 조회와 전체 갯수를 따로 조회하는 방법이다.(가끔 카운트 쿼리가 성능을 저하시킬 수 있는 경우에 사용한다)

 

그리고 CountQuery 최적화를 위해 PageableExecutionUtils.getPage()를 이용한다 

 

1. searchPageSimple(): 전체 카운트를 한번에 조회하는 방법 

fetchResults()를 이용하면 내용과 전체 카운트를 한번에 조회할 수 있다(실제 쿼리는 2번 호출)

fetchResults()는 카운트 쿼리 실행시 필요없는 order by는 제거한다 

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, 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);

      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. searchPageComplex(): 데이터 내용과 전체 카운트를 별도로 조회하는 방법 

전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다 

코드를 리팩토링해서 내용쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다 

@Override
//회원명, 팀명, 나이(ageGoe, ageLoe)
public List<MemberTeamDto> search(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);

 

3. PageableExecutions.getPage()

이거는 스프링 데이터 라이브러리가 제공하는 것으로, count 쿼리를 생략 가능한 경우 생략해서 처리하는 기능을한다.

 

-count 쿼리 생략 가능한 경우

1) 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈가 작을 때(size(컨텐츠) < size(페이지))

2) 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구한다)

 

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);

 

4. 2번에 .fetchCount()는 이제 QueryDsl에서 지원이 안 될 예정이므로 count 쿼리는 따로 작성해준다 

@Test
public void count() {

    Long totalCount = queryFactory
            //.select(Wildcard.count) //select count(*)
            .select(member.count()) //select count(member.id)
            .from(member)
            .fetchOne();
    System.out.println("totalCount = " + totalCount);
 }

스프링 데이터 JPA에서는 save,update,delete,find...ByXXX 같은 메서드는 사용이 가능 하지만, 

search() 같은 메서드는 호출할 수 없다 

이럴 때는 사용자 정의 리포지토리를 이용한다 

 

방법은 다음과 같다 

먼저 MemberRepositoryCustom 인터페이스를 생성한 후 사용하고자 하는 메서드를 입력한 후 

public interface MemberRepositoryCustom {
      List<MemberTeamDto> search(MemberSearchCondition condition);
}

MemberRepositoryCustom를 MemberRepositoryCustomImpl에서 구현한다 

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);
} 
}

MemberRepository에서 MemberRepositoryCustom을 extends 한다 

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

 

1. BooleanBuilder 

파라미터 값이 null이 아닐때 builder.and()사용한 후 where에 builder 삽입 

private List<Member> searchMember1(String usernameCond, Integer ageCond) {

      BooleanBuilder builder = new BooleanBuilder();
      
      if (usernameCond != null) {
          builder.and(member.username.eq(usernameCond));
      }
      if (ageCond != null) {
          builder.and(member.age.eq(ageCond));
      }
      
      return queryFactory
              .selectFrom(member)
              .where(builder)
              .fetch();
}

 

2. Where 다중 파라미터 사용 

private List<Member> searchMember2(String usernameCond, Integer ageCond) {
      return queryFactory
              .selectFrom(member)
              .where(usernameEq(usernameCond), ageEq(ageCond))
              .fetch();
  }
  
  //조합 가능(username,age 동시에 체크)
  private BooleanExpression allEq(String usernameCond, Integer ageCond) {
      return usernameEq(usernameCond).and(ageEq(ageCond));
}
  
  //usernameEq 메서드 
  private BooleanExpression usernameEq(String usernameCond) {
         return usernameCond != null ? member.username.eq(usernameCond) : null;
  }
  //ageEq 메서드 
  private BooleanExpression ageEq(Integer ageCond) {
      return ageCond != null ? member.age.eq(ageCond) : null;
}

 

특징

1) where 조건에 null 값은 무시된다

2) 메서드를 다른 쿼리에서도 재활용할 수 있다

3) 쿼리 자체의 가독성이 높아진다 

 

 

Dto의 생성자에 @QueryProjection을 추가

~/MemberDto

@Data
public class MemberDto {

    private String username;
    private int age;
    
    //기본 생성자 
    public MemberDto() {
    }
    
    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
      }
}

@QueryProjection 활용 

List<MemberDto> result = queryFactory
          .select(new QMemberDto(member.username, member.age))
          .from(member)
          .fetch();

 

위 방식을 이용하면

컴파일러로 타입을 체크할 수 있어서 가장 안전하다는 장점이 있다. 

하지만 DTO가 Querydsl에 의존하는 점(Querydsl 어노테이션을 dto에서 유지하는 점)과 DTO까지 Q 파일을 생성해야하는 단점이 있다 

+ Recent posts