728x90
반응형
SMALL

이전 포스팅까지는 Querydsl의 기본 사용 방법에 대해 쭉 정리를 해봤는데, 이제 그 지식을 바탕으로 실무에 가까운 코드를 작성해보자.

먼저, 순수한 JPA 레포지토리와 Querydsl을 사용해보고, 이후에 스프링 데이터 JPAQuerydsl을 사용해보는 것도 하나씩 해보자.

 

순수 JPA 레포지토리와 Querydsl

MemberJpaRepository

package cwchoiit.querydsl.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import cwchoiit.querydsl.entity.Member;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

import static cwchoiit.querydsl.entity.QMember.member;

@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    public void save(Member member) {
        em.persist(member);
    }

    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(em.find(Member.class, id));
    }

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

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

    public List<Member> findByUsername(String username) {
        return em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", username)
                .getResultList();
    }

    public List<Member> findByUsernameQuerydsl(String username) {
        return queryFactory
                .selectFrom(member)
                .where(member.username.eq(username))
                .fetch();
    }
}
  • 순수 JPA 레포지토리는 워낙 많이 만들어봤기 때문에 다른 설명이 필요없다. 그런데 이 레포지토리에서 Querydsl을 사용하기 위해 JPAQueryFactory를 주입받는 방법이 취향차이인데, 지금 방식은 빈으로 등록해서 주입받는 방법이다. 빈으로는 어디에 등록했나? 빈을 등록할 수 있는 어디든 상관은 없다. 나의 경우 엔트리 클래스에 했다. 아래 코드처럼.
package cwchoiit.querydsl;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class QuerydslApplication {

    public static void main(String[] args) {
        SpringApplication.run(QuerydslApplication.class, args);
    }

    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager em) {
        return new JPAQueryFactory(em);
    }
}
  • 빈으로 JPAQueryFactory를 등록해서 여기저기 주입받아 사용한다. 그럼 여기서 의문이 든다. 동시성 문제가 있지 않을까? 그 걱정은 하지 않아도 된다. 왜냐하면 여기서 스프링이 주입해주는 엔티티 매니저는 실제 동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시 엔티티 매니저를 주입해준다. 이 프록시가 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저를 할당해준다.
  • 이렇게 빈으로 등록하고 주입받는 방법을 사용해도 되고, 아래와 같이 사용해도 상관없다.
@Repository
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    public MemberJpaRepository(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }
    
    ...
}
  • 각자 장단점이 있는데, 이렇게 생성자에서 new로 새로 만들어내는 방법은 테스트 코드를 작성할 때 더 편리하다. 빈으로 등록한 것을 주입 시킬 필요가 없으니까. 근데 이제 실제 코드에서 생성자를 만드는 코드를 작성하는 게 좀 귀찮아진다. 

 

그리고, findAll(), findByUsername() 이 두 메서드를 보면 Querydsl로 만든 버전이 있고 순수 JPA로 만든 버전이 있는데 차이가 명확하다.

public List<Member> findAll() {
    return em.createQuery("select m from Member m", Member.class)
            .getResultList();
}

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

public List<Member> findByUsername(String username) {
    return em.createQuery("select m from Member m where m.username = :username", Member.class)
            .setParameter("username", username)
            .getResultList();
}

public List<Member> findByUsernameQuerydsl(String username) {
    return queryFactory
            .selectFrom(member)
            .where(member.username.eq(username))
            .fetch();
}
  • 순수 JPA로 작성하려면 JPQL을 작성해줘야 하는데 이 부분에서 실수를 해도 컴파일 시점에 그 부분을 잡아내지는 못한다. 왜냐? 단순 문자열이니까. 그렇지만 Querydsl을 사용하는 경우, 컴파일 시점에 문제를 다 잡아낼 수 있다. 왜냐? 자바 코드니까.
  • 또 한가지는, 파라미터 바인딩 코드가 Querydsl은 필요가 없다. 
  • Querydsl을 사용하면 여러모로 장점이 참 많다.

 

동적 쿼리와 성능 최적화 조회 - Builder 사용

동적 쿼리가 존재하는 순수 JPA 레포지토리도 만들어보고, 극한의 최적화를 위해 딱 필요한 것만 조회해오는 DTO로 성능 최적화를 해보자.

MemberTeamDto

package cwchoiit.querydsl.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;

@Data
public class MemberTeamDto {

    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;

    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
}
  • 쿼리를 날릴 때 SELECT절에 딱 필요한 데이터만 메모리에 퍼올리기 위해 원하는 필드만 있는 DTO를 만들었다.
  • 그리고, @QueryProjection을 사용해서, Querydsl을 사용할 때 DTO를 편리하게 사용할 수 있게 했다.

MemberSearchCondition

package cwchoiit.querydsl.dto;

import lombok.Data;

@Data
public class MemberSearchCondition {

    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}
  • 동적 쿼리에 사용될 조건 객체를 만들었다.

MemberJpaRepository 일부분

public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {

    BooleanBuilder builder = new BooleanBuilder();

    if (StringUtils.hasText(condition.getUsername())) {
        builder.and(member.username.eq(condition.getUsername()));
    }

    if (StringUtils.hasText(condition.getTeamName())) {
        builder.and(team.name.eq(condition.getTeamName()));
    }

    if (condition.getAgeGoe() != null) {
        builder.and(member.age.goe(condition.getAgeGoe()));
    }

    if (condition.getAgeLoe() != null) {
        builder.and(member.age.loe(condition.getAgeLoe()));
    }

    return 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(builder)
            .fetch();
}
  • BooleanBuilder를 사용해서 동적 쿼리를 먼저 만들어봤다. 이전에도 이 녀석을 사용해보고 이후에 WHERE 다중 파라미터 방식을 했던것처럼 이번에도 그렇게 해보자.
  • BooleanBuilder를 사용해도 코드가 나쁘지 않다. 느낌은 JDBC를 사용해서 동적 쿼리를 만들어내는 거랑 비슷한데, 확연히 다른 점은 단순 문자열로 동적 쿼리를 만들어내는 게 아니란 점이다. 이게 정말 가장 강력한 장점이다.
  • 한가지 위 코드에는 오점이 있다. 오점이라기 보단 불 필요한 부분. 바로 SELECT절에 퍼올리는 데이터에 as(...)로 별칭을 주는 부분이다. 저렇게 작성한 이유는 데이터베이스에는 MemberID 필드가 `member_id`인데 DTO`memberId`이기 때문에 기본적으로 저렇게 별칭을 주는게 맞다. 근데 저건 Q클래스의 DTO이고 생성자이다. 즉, 필드가 들어가는 순서가 명확하고 어떤 타입인지도 이미 컴파일러는 알고 있으며, 생성자를 사용해 DTO 타입의 객체를 만들어내기 때문에 저렇게 할 필요가 없다. 그러니까 아래처럼 작성해도 된다.
return queryFactory
    .select(new QMemberTeamDto(
            member.id,
            member.username,
            member.age,
            team.id,
            team.name))
    .from(member)
    .leftJoin(member.team, team)
    .where(builder)
    .fetch();

 

그러나, 이 BooleanBuilder를 사용하는 것보다 무조건 WHERE절 다중 파라미터를 사용하는 게 더 좋다. 일단 코드를 보면 무슨말인지 확 이해가 된다.

 

동적 쿼리와 성능 최적화 조회 - WHERE절 다중 파라미터 사용

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 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 ageGoe != null ? member.age.goe(ageGoe) : null;
}

private BooleanExpression ageLoe(Integer ageLoe) {
    return ageLoe != null ? member.age.loe(ageLoe) : null;
}
  • 같은 코드가 이렇게 바뀌었다. 쿼리만 따로 보면 이렇다.
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();
}
  • 이게 가장 큰 장점이 뭐냐? 쿼리가 읽힌다는 점이다. 동적 쿼리를 처리하는 방법은 다양하다. JDBC를 사용해서 동적쿼리를 처리하나 위에서 다뤘던 BooleanBuilder를 사용하나 쿼리만 딱 봤을 때 어떤 쿼리가 나갈지 예상하기 쉽지 않다. 위에 "어떤 코드가 있나~?" 하고 보고 내려와야 하니까. 
  • 그런데 이 방식은 그럴 필요가 없다. 쿼리만 봐도 어떤 쿼리가 나갈지 그냥 눈에 보인다. 이게 정말 정말 큰 장점이다. 그리고 또 하나의 장점은 조건에 사용되는 usernameEq, teamNameEq, ageGoe, ageLoe 와 같은 메서드는 메서드이기 때문에 재사용이 가능하다는 점이다. 다른 동적 쿼리를 작성할 때 이렇게 범용적으로 사용될 것 같은 조건들은 또 사용될 가능성이 높다. 그럴때 그냥 만들어 둔 이 녀석들을 가져다가 사용하면 된다. 
  • 또다른 장점은, 각 메서드를 조합하는 게 가능하다는 점이다. 다 이전 포스팅에서 다뤘던 내용이지만 한번 더 강조하고자 말하는 중이니 어떻게 조합한다는 거지?에 대한 의문은 이전 포스팅이 해결해 줄 것이다.

 

조회 API 컨트롤러 개발

이제 이 동적쿼리를 실제로 어디선가 호출하는 그런 케이스를 만들어보자. REST API로 어떤 데이터를 조회할 때 저 동적 쿼리가 사용된다고 가정해보는 것이다. 이걸 테스트해보기 위해서는 더미데이터가 데이터베이스에 좀 있어야 한다. 그래서 이 부분을 먼저 좀 처리해보자.

 

우선, 프로파일을 나눠서 테스트 코드에는 영향이 끼치지 않도록 아래와 같이 해보자.

 

`src/main/resources/application.yaml`

spring:
  profiles:
    active: local
...

 

`src/test/resources/application.yaml`

spring:
  profiles:
    active: test
...

 

InitMember

package cwchoiit.querydsl.controller;

import cwchoiit.querydsl.entity.Member;
import cwchoiit.querydsl.entity.Team;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {

    private final InitMemberService memberService;

    @PostConstruct
    public void init() {
        memberService.init();
    }

    @Component
    @RequiredArgsConstructor
    static class InitMemberService {
        private final EntityManager em;

        @Transactional
        public void init() {
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
            em.persist(teamA);
            em.persist(teamB);

            for (int i = 0; i < 100; i++) {
                Team selectedTeam = i % 2 == 0 ? teamA : teamB;
                em.persist(new Member("member"+i, i, selectedTeam));
            }
        }
    }
}
  • 이 클래스는 오직 프로파일이 `local`인 경우에만 살아있을 것이다. @Profile("local") 애노테이션을 달았기 때문이다.
  • 그리고, @PostConstruct 애노테이션을 사용해서, 스프링 띄우고 바로 호출되도록 했다. 뭘 바로 호출해야 하냐? 가 데이터를 만드는 작업이다. 
  • 그런데, 가 데이터를 만들 땐 트랜잭션이 필요하다. 그래서 @Transactional@PostConstruct를 분리해야 한다. 이 둘은 동시에 적용될 수 없다. 
  • 이렇게 가 데이터를 만들게 하고 나서, 컨트롤러 하나를 추가해보자.

MemberController

package cwchoiit.querydsl.controller;

import cwchoiit.querydsl.dto.MemberSearchCondition;
import cwchoiit.querydsl.dto.MemberTeamDto;
import cwchoiit.querydsl.repository.MemberJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberJpaRepository memberJpaRepository;

    @GetMapping("/v1/members")
    public List<MemberTeamDto> searchMembersV1(MemberSearchCondition condition) {
        return memberJpaRepository.search(condition);
    }
}
  • 컨트롤러 하나를 간단히 만들고, 파라미터로 위에서 동적 쿼리 조건 처리할 때 사용하는 MemberSearchCondition을 사용했다.

 

이제 이 API를 호출해보자.

### GET Members
GET http://localhost:8080/v1/members?teamName=teamB&ageGoe=31&ageLoe=50&username=member49

 

스프링 데이터 JPAQuerydsl

이제 순수 JPA 레포지토리를 스프링 데이터 JPA로 변경해보자.

 

MemberRepository

package cwchoiit.querydsl.repository;

import cwchoiit.querydsl.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsername(String username);
}
  • 스프링 데이터 JPA로 변경하려면 이렇게 만들면 끝난다. 기존에 있던, save(), findAll(), findById()와 같은 공통 메서드들은 이미 스프링 데이터 JPA는 다 만들어서 제공해주기 때문에 그런 공통 메서드가 아닌 findByUsername()만 만들면 되는데 이마저도 스프링 데이터 JPA가 제공하는 메서드 쿼리 기능을 사용해서 시그니처만으로 끝낼 수 있다.
  • 문제는 Querydsl을 사용했던 부분들이다. 걔네들은 어떻게 구현하면 될까?

 

사용자 정의 레포지토리

Querydsl을 사용하려면 구현 코드를 작성해야 하는데, 스프링 데이터 JPA는 인터페이스이기 때문에 조금 복잡한 과정이 필요하다.

 

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

package cwchoiit.querydsl.repository;

import cwchoiit.querydsl.dto.MemberSearchCondition;
import cwchoiit.querydsl.dto.MemberTeamDto;

import java.util.List;

public interface MemberQuerydslRepository {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}
  • 인터페이스 이름은 원하는대로 만들면 된다. 그리고, 이전에 순수 JPA 레포지토리에서 만들었던 search()를 그대로 만들어준다.

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

package cwchoiit.querydsl.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import cwchoiit.querydsl.dto.MemberSearchCondition;
import cwchoiit.querydsl.dto.MemberTeamDto;
import cwchoiit.querydsl.dto.QMemberTeamDto;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;

import java.util.List;

import static cwchoiit.querydsl.entity.QMember.member;
import static cwchoiit.querydsl.entity.QTeam.team;

@RequiredArgsConstructor
public class MemberQuerydslRepositoryImpl implements MemberQuerydslRepository {

    private final JPAQueryFactory queryFactory;

    @Override
    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 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 ageGoe != null ? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.loe(ageLoe) : null;
    }
}
  • 구현체를 만들고, 순수 JPA 레포지토리에서 만들었던 그 메서드를 그대로 가져와보자. 

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

package cwchoiit.querydsl.repository;

import cwchoiit.querydsl.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemberRepository extends JpaRepository<Member, Long>, MemberQuerydslRepository {
    List<Member> findByUsername(String username);
}
  • 스프링 데이터 JPA 레포지토리에 위에서 만든 우리의 커스텀 레포지토리를 상속시키면 끝이다.

 

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

  • Querydsl을 사용하면서 스프링 데이터의 Page, Pageable을 활용해보자.
package cwchoiit.querydsl.repository;

import cwchoiit.querydsl.dto.MemberSearchCondition;
import cwchoiit.querydsl.dto.MemberTeamDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface MemberQuerydslRepository {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    Page<MemberTeamDto> search(MemberSearchCondition condition, Pageable pageable);
}
  • 우선, 반환타입이 Page<MemberTeamDto>이고, 파라미터로 Pageable을 받는 메서드를 하나 추가한다.
  • 이제 이 녀석을 새로 구현하면 된다.
@Override
public Page<MemberTeamDto> search(MemberSearchCondition condition, Pageable pageable) {
    List<MemberTeamDto> contents = 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();

    List<String> totalContents = queryFactory
            .select(member.username)
            .from(member)
            .where(usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe()))
            .fetch();

    return new PageImpl<>(contents, pageable, totalContents.size());
}
  • 이전에는 fetchResults()라는 메서드를 사용하면, 데이터와 전체 개수를 같이 받아오는 메서드가 있었는데 이제는 Deprecated됐다. 그래서 데이터를 가져오는 쿼리와 카운트 쿼리를 따로 날려야 한다. 그리고 이후에 설명하겠지만 이게 맞다.
  • 위 코드를 보면, 파라미터로 받은 Pageable에서 offset, limit을 뽑아올 수가 있다. Querydsl을 사용해서 그냥 offset, limit을 넣어주면 페이징은 끝이다.
  • 그럼 카운트 쿼리를 자세히 보자. 이게 중요하다.
List<String> totalContents = queryFactory
                .select(member.username)
                .from(member)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .fetch();
  • 카운트 쿼리를 보니, 데이터를 가져오는 쿼리랑 다르다. 조인도 안하고 있고, SELECT절에서 가져오는 것들도 다르다. 이게 최대한의 성능 최적화를 할 수 있는 방법이다. 전체 개수를 가져오는 쿼리는 생각보다 굉장히 무겁다. 데이터가 100백만건이면 그 데이터를 메모리에 퍼 올리는 것만으로도 문제가 될 수 있다. 거기에 더해서, 조인을 하고 SELECT절에 필요하지 않은 데이터까지 다 퍼올리면 이 부분도 성능에 마이너스 요소이다. 
  • LEFT JOIN을 하면 전체 개수는 LEFT JOIN을 아예 하지 않아도 동일하다. 어차피 조인한 결과가 없으면 빈 값으로 그대로 레코드를 가져오기 때문이다. 그래서 굳이 조인을 하지 않아도 전체 개수에 아무런 영향을 끼치지 않는다.
  • 또한, SELECT절에는 다른 데이터가 전혀 필요없이 딱 하나만 있어도 전체 개수에 아무런 영향을 끼치지 않는다. 그래서 굳이 데이터를 가져오는 쿼리에서 봤던 여러 다른 데이터를 가져올 필요가 없다. 
  • 이렇게 전체 개수를 가져오는 쿼리를 최적화할 수 있으면 해야 한다. 매번 이렇게 할 수 있는건 아니지만 가능하면 반드시 해야 한다.
return new PageImpl<>(contents, pageable, totalContents.size());
  • 이제 반환을 하면 된다. 첫번째 파라미터는 contents, 두번째 파라미터는 pageable, 세번째 파라미터는 전체 개수를 받는다. 
  • 이것 또한, 최적화가 가능하다. 다음과 같이 말이다.
JPAQuery<String> countQuery = queryFactory
                .select(member.username)
                .from(member)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()));

return PageableExecutionUtils.getPage(contents, pageable, () -> countQuery.fetch().size());
  • PageableExecutionUtils.getPage()를 사용하면 어떤 최적화가 가능하냐면, contents를 가져왔을 때 전체 개수 쿼리를 날릴 필요가 없으면 날리지 않는다. 엥? 어떤 경우에 이럴까? 예를 들어 한 페이지의 개수가 100개인 페이징 쿼리를 날렸는데 한 페이지에 데이터가 3개만 들어왔다. 이 말은 그 쿼리 자체의 전체 개수가 3개라는 말이다. 그럼 카운트 쿼리를 날릴 필요없이 그냥 전체 개수는 3인 것이다. 

 

스프링 데이터 페이징 활용2 - 컨트롤러 개발

package cwchoiit.querydsl.controller;

import cwchoiit.querydsl.dto.MemberSearchCondition;
import cwchoiit.querydsl.dto.MemberTeamDto;
import cwchoiit.querydsl.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/v1/members")
    public List<MemberTeamDto> searchMembersV1(MemberSearchCondition condition) {
        return memberRepository.search(condition);
    }

    @GetMapping("/v2/members")
    public Page<MemberTeamDto> searchMembersV2(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.search(condition, pageable);
    }
}
  • searchMembersV2()를 보자. 페이징 처리가 가능한 REST API를 만들었다.
  • 우리가 위에서 만든 Querydsl을 사용해서 페이징 쿼리를 날려보자.
### GET Members
GET http://localhost:8080/v2/members?page=0&size=200
  • 의도적으로 한 페이지에 200개를 가져오게 해봤다. 위에서 말한대로, 카운트 쿼리를 날릴 필요가 없으면 카운트 쿼리를 날리지 않게 최적화했으니 실제로 그런지 확인해보자! 전체 데이터는 100개뿐인데 한 페이지에 200개를 뽑아오게 하면 들어오는 데이터는 100개라 전체 카운트 쿼리를 날리지 않을 것이다.

  • 카운트 쿼리는 예상대로 날라가지 않았다. 그럼 카운트 쿼리가 날라가도록 바꿔서 테스트도 해보자.
### GET Members
GET http://localhost:8080/v2/members?page=0&size=50

  • 카운트 쿼리가 날라가야 하는 경우 제대로 날리는 모습을 확인할 수 있다.

 

 

728x90
반응형
LIST

'Querydsl' 카테고리의 다른 글

참고: 리포지토리 지원 - QuerydslRepositorySupport  (0) 2024.12.30
Querydsl 중급 문법  (0) 2024.12.22
Querydsl 기본 문법  (0) 2024.12.21
[Renewal] QueryDsl  (0) 2024.12.07
Spring Boot 3.1.5에서 QueryDSL 설치하기  (0) 2023.11.26

+ Recent posts