Querydsl

 

Querydsl 을 공부하면서 내가 온전히 이해한 건지 정리하면서도 다른 개발자분들도 이해하기 쉽게 정리해 보았습니다.

 

시작하기에 앞서

JPA 를 기본적으로 알고있다고 가정하고 블로그를 정리합니다.

 

간단하게 순수 JPQL 과 Querydsl 을 사용했을 때 차이점을 설명하면서 시작하겠습니다.

 

우선 테스트용 Member 를 설정했습니다.

@SpringBootTest
@Transactional
public class QuerydslBasicTest {
  
  @PersistenceContext
  EntityManager em;
  
  @BeforeEach
  public void before() {
    
    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);
  }
}

 

위에서 만든 @BeforeEach 는 테스트 시작 전에 먼저 실행되는 메서드를 지정하고 싶을 때 사용합니다.

 

아래는 순수 JPQL 과 Querydsl 로 변경하였을 때의 차이점입니다.

 

순수 JPQL

@Test
public voidi startJPQL() {
  
  String qlString = "select m from Member m where m.username = :username";
  
  Member findMember = em.createQuery(qlString, Member.class)
    .setParameter("username", "member1").getSingleResult();
  
  assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

assertThat 에 Assertions 가 없는 이유는 import static 으로 Assertions.* 을 불러왔기 때문입니다.

 

Querydsl

@Test
public void startQuerydsl() {

  @PersistenceContext
  EntityManger em;

  JPAQueryFactory queryFactory = new JPAQueryFactory(em);
  QMember m = new QMember("m");
  
  Member findMember = queryFactory
    .select(m)
    .from(m)
    .where(m.username.eq("member1"))
    .fetchOne();
    
  assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

위 코드를 보면 QMember 라는게 나오는데 인텔리제이 기준으로 Gradle 을 이용해서 complieQuerydsl 을 눌러주면 됩니다.

 

그러면 아래와 같은 경로에 Q Class 가 생성된 것을 볼 수 있습니다.

경로 설정은 application.properties, application.yml 설정을 통해 만들 수 있습니다.

 

위와 같이 QHello 가 생성된 경로에는 git 에 올리면 안되기 때문에 .gitignore 을 통해 막아줘야 합니다.

 

new QMember("m")

코드를 보면 아래와 같이 파라미터로 "m" 이 들어간 것을 확인할 수 있습니다.

 

파라미터 안에 들어가는 값은 어떤 이름으로 만들어도 상관은 없습니다.

하지만 SQL 에서 Alias 처럼 Member 을 m 이라는 이름으로 사용하겠다! 라는 의미입니다.

같은 테이블(Member) 을 여러번 조인해야 하는 경우 사용하게 됩니다.

 

QMember m1 = new QMember("m1");
QMember m2 = new QMember("m2");

List<Member> result = queryFactory
  .select(m1)
  .from(m1)
  .join(m1.team, m2)
  .where(m1.username.eq("A").and(m2.username.eq("B")))
  .fetch();

 

같은 테이블을 조인해야 하는 경우가 아니라면 기본 인스턴스를 사용하는 것을 추천합니다.
기본 인스턴스를 사용하려면 QMember member1 = QMember.member 라고 사용하시면 됩니다.


아래와 같이 Q Class 인스턴스를 만드는 방법은 2가지 입니다.

QMember qMember = new QMember("m");
QMember qMember = QMember.member;

 

동시성 문제

JPAQueryFactory 를 필드로 제공하면 동시성 문제가 발생할 수 있다는 의문점이 생길 수도 있습니다.

동시성 문제는 JPAQueryFactory 를 생성할 때 제공하는 EntityManager(em) 에 달려있습니다.

스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager 에 접근을 하여도,

트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정을 하지 않아도 됩니다.

 

static import 활용하기

아래와 같이 기본 인스턴스를 static import 를 활용하면 매번 Q Class 를 만들어 줄 필요가 없습니다.

import static study.querydsl.entity.QMember.*;

@Test
public void startQuerydsl() {
  
  Member findMember = queryFactory
    .select(member)
    .from(member)
    .where(member.username.eq("member1"))
    .fetchOne();
}

 

만약에 실행되는 JPQL 을 보고싶다면 application.properties 를 아래와 같이 설정하면 됩니다.

spring.jpa.properties.hibernate.use_sql_comments: true

 

본격적인 Querydsl

이제부터 Querydsl 을 이용해 보는 예시를 알려드리겠습니다.

기본검색조건쿼리

@Test
public void search() {
  
  Member findMember = queryFactory
    .selectFrom(member)
    .where(member.username.eq("member1").and(member.age.eq(10)))
    .fetchOne();
}

 

위와 같이 검색 조건은 .and 또는 .or 을 이용하여 메서드 체인으로 연결할 수 있습니다.

위에 코드를 보시면 selectFrom 이 있는데 select, from 에서 같은 파라미터값을 사용한다면 묶어줄 수 있습니다.

 

Querydsl 이 제공하는 검색조건들

member.username.eq("member1") // username = "member1";
member.username.ne("member1") // username != "member1";
member.username.eq("member1").not // username != "member1";

member.username.isNotNull() // 이름이 is Not Null 조건

member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10, 30) // age between 10 and 30

member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30

member.username.like("member") // like
member.username.contains("member") // like %member% 검색
member.username.startsWith("member") // like member% 검색

 

AND 조건을 .and 가 아닌 파라미터로 처리

@Test
public void searchAndParam() {
  List<Member> result1 = queryFactory
    .selectFrom(member)
    .where(member.username.eq("member1"), member.age.lt(30))
    .fetch();
}

 

.and 가 아닌 파라미터(,) 을 추가하면 AND 기능으로 동작을 합니다.

 

만약 해당 데이터가 없다면 NULL 로 처리하게 됩니다.

 

결과 조회 옵션

fetch() : 리스트조회, 데이터가 없으면 빈 리스트를 반환

fetchOne() : 단건조회, 결과가 없으면 NULL, 결과가 둘 이상이라면 NonUniqueResultException 발생

fetchFirst() : 첫번째로 나오는 데이터를 반환

fetchResults() : 페이징 정보를 포함, total count 쿼리 추가 발생

fetchCount() : COUNT 로 변경해서 COUNT 수 조회

fetchResults() 를 사용하면 QueryResults<Member> results 라고 받아와야 합니다.

 

fetchResults, fetchCount 사용하지 말기

Querydsl 의 fetchCount(), fetchResults() 는 개발자가 작성한 select 쿼리를 기반으로 count 용 쿼리를 내부에서 자동으로 만들어 줍니다. 그런데 이 기능은 select 구문을 단순히 count 처리하는 용도로 바꾸는 정도라서 단순한 쿼리에서는 잘 동작하지만 복잡한 쿼리에서는 제대로 동작하지 않습니다.

또한 Querydsl 은 향후 이 두 기능을 지원히지 않기로 결정하였습니다.

 

정렬

만약 요구조건에서 회원 나이를 내림차순, 회원 이름을 올림차순, 회원 이름이 없으면 마지막에 출력이면 아래와 같이 정의해 주시면 됩니다.

 

List<Member> result = queryFactory
  .selectFrom(member)
  .where(member.age.eq(30))
  .orderBy(member.age.desc(), member.username.asc().nullsLast())
  .fetch();

 

페이징

List<Member> result = queryFactory
  .selectFrom(member)
  .orderBy(member.username.desc())
  .offset(1) // 0 부터 시작
  .limit(2)  // 최대 2건을 조회
  .fetch();

 

이렇게 설정을 하시면 데이터 2개를 가져오게 됩니다.

만약 offset(2), limit(2) 라면 5~8 까지의 데이터를 가져오게 됩니다.

 

집합함수

 

Tuple 은 List 안에 뭐가 들어갈지 모르는 경우 Querydsl 에서 지원해주는 기능입니다.

실무에서는 DTO 로 변환해주는 것이 좋습니다.

 

List<Tuple> result = queryFactory
  .select(member.count(),
          member.age.sum(),
          member.age.avg(),
          member.age.max(),
          member.age.min())
  .from(member)
  .fetch();

Tuple tuple = result.get(0);

 

GROUP BY, HAVING

List<Tuple> result = queryFactory
  .select(team.name, member.age.avg())
  .from(member)
  .join(member.team, team)
  .groupBy(team.name)
  .having(team.age.gt(30))
  .fetch();

 

조인(기본조인)

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(Alias) 으로 사용할 Q 타입을 지정하시면 됩니다.

join(조인대상, 별칭으로 사용할 Q타입)

 

// 팀 A 에 소속된 모든 회원

@Test
public void join() throws Exception {

  QMember member = QMember.member;
  QTeam team = QTeam.team;
  
  List<Member> result = queryFactory
    .selectFrom(member)
    .join(member.team, team)
    .where(team.name.eq("A"))
    .tech();
    
}

 

이 밖에도 join(), innerJoin(), leftJoin(), rightJoin() 이 있습니다.

 

세타조인

연관관계가 없는 필드로 조인하는 방법을 Querydsl 에서는 세타조인으로 지원해 줍니다.

 

// 회원의 이름이 팀 이름과 같은 회원 조회

List<Member> result = queryFactory
  .select(member)
  .from(member, team)
  .where(member.username.eq(team.name))
  .fetch();

 

join 과는 다르게 연관관계가 없는 필드로 조인하기 위해서는 from 절에 넣어주시면 됩니다.

 

조인(ON 절 사용)

ON 절을 활용한 조인(JPA 2.1 version 부터 지원) 도 이용할 수 있습니다.

 

// 회원과 팀을 조회하는데, 팀 이름이 teamA 인 팀만 조회하고, 회원은 그냥 모두 조회
List<Tuple> result = queryFactory
  .select(member, team)
  .from(member)
  .leftJoin(member.team, team).on(team.name.eq("teamA"))
  .fetch();

 

조인(패치조인)

패치 조인은 SQL 에서 제공하는 기능은 아니지만, JPQL 이 지원해주는 성능 최적화 기능입니다.

JPA 를 공부하신 분들은 join fetch 를 생각하시면 됩니다.

 

Member findMember = queryFactory
  .selectFrom(member)
  .join(member.team, team).fetchJoin()
  .fetch();

 

패치조인은 그냥 join 뒤에다가 .fetchJoin() 을 넣어주시면 됩니다.

이렇게 설정을 하면 member 안에 team 인 데이터를 바로 가져오게 됩니다.

 

서브쿼리

// 나이가 가장 많은 회원을 조회

List<Member> result = queryFactory
  .selectFrom(member)
  .where(member.age.eq(
    JPAExpressions
    .select(memberSub.age.max())
    .from(memberSub)
  )).fetch();

 

하지만 사용하다보면 JPAExpressions 를 계속 넣어줘야하는 불편함이 있습니다.

따라서 import static 을 이용하여 코드 가독성을 높이는 방법도 있습니다.

 

import static com.querydsl.jpa.JPAExpressions.select;

List<Member> result = queryFactory
  .selectFrom(member)
  .where(member.age.eq(
    select(memberSub.age.max())
    .from(memberSub)
  )).fetch();

 

혹시나 from 절에서 서브쿼리를 사용해도 되지 않을까? 라는 개발자 분들이 있다면 JPQL 의 한계점 때문에 from 절에서는 서브쿼리를 사용할 수가 없습니다. 따라서 from 절에서 사용해야 하는 경우가 생긴다면, 쿼리를 2번 분리하거나 서브쿼리를 join 으로 변경을 해야 합니다.

 

CASE 절

List<String> result = queryFactory
  .select(member.age
    .when(10).then("열살")
    .when(20).then("스무살")
    .otherwise("기타"))
  .from(member)
  .fetch();

 

프로젝션

 

프로젝션은 쿼리 결과로 어떤 컬럼을 선택해서 가져올지 정하는 것을 의미합니다.

SQL 로 예를 들어보면 아래와 같습니다.

SELECT username, age FROM member;

 

이때 바로 username, age 컬럼을 "프로젝션" 했다 라고 말합니다.

 

Querydsl 에서는 프로젝션 대상이 하나면 아래와 같은 코드로 끝날 수 있습니다.

List<String> result = queryFactory
  .select(member.username)
  .from(member)
  .fetch();

 

이렇게 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있습니다.

프로젝션 대상이 둘 이상이면 Tuple 이나 DTO 로 조회해야 합니다.

 

위에서 본 바와 같이 튜플은 프로젝션 대상이 둘 이상일 때 사용하면 좋습니다.

하지만 Tuple 을 쓰지 않고 업무에 적합한 DTO 를 사용하는게 더 좋으며 실무에서는 DTO 로 변환하여 사용하는 게 더 일반적입니다.

 

Querydsl 에서는 다음과 같이 3가지 방법으로 DTO 변환을 지원해줍니다.

 

1. 프로퍼티 접근

2. 필드 직접 접근

3. 생성자 사용 

 

프로퍼티 접근

List<MemberDto> result = queryFactory
  .select(Projections.bean(MemberDto.calss, member.username, member.age))
  .from(member)
  .fetch();

 

프로퍼티 접근은 setter 가 DTO 에 정의되어 있어야 합니다.

Projections.bean 은 해당 setter 를 참조하여 값을 넣어주기 때문입니다.

 

필드직접접근

List<MemberDto> result = queryFactory
  .select(Projections.fields(MemberDto.class, member.username, member.age))
  .from(member)
  .fetch();

 

필드직접접근은 필드에 직접 접근해서 넣어주는 방식입니다.

따라서 setter 가 필요가 없습니다.

 

만약 별칭이 다르다면 (username -> name) 을 아래와 같이 as 를 사용하시면 됩니다.

List<MemberDto> result = queryFactory
  .select(Projections.fields(MemberDto.class, member.username.as("name"), member.age))
  .from(member)
  .fetch();

 

생성자 사용 방법

List<MemberDto> result = queryFactory
  .select(Projections.constructor(MemberDto.class, member.username, member.age))
  .from(member)
  .fetch();

 

생성자 사용 방법은 MemberDto 에 생성자를 만들어 주어야 가능한 방법입니다.

따라서 아래처럼 추가적인 생성자 코드가 필요합니다. 만약 없다면 오류가 발생하게 됩니다.

public MemberDto(String username, int age) {...}

 

생성자사용방법의 장점은 필드명이 안맞아도 됩니다.

하지만 순서, 타입이 틀리면 런타임 오류가 발생하며, 컴파일러가 체크할 수 없습니다.

 

프로젝션 결과 반환

위에 3가지 방법 말고도 마지막으로 하나의 방법이 더 있습니다.

사용법은 생성자에 @QueryProjection 어노테이션을 붙이는 방법입니다.

 

생성자 + @QueryProjection 으로 사용함으로써 간단하게 DTO 로 변환시킬 수 있습니다.

 

@Data
public class MemberDto {
  
  private String username;
  private String age;
  
  public MemberDto {}
  
  @QueryProjection
  public MemberDto(String username, int age) {
    this.username = username;
    this.age = age;
  }
}

// QMemberDto 만들기
./gradlew complieQuerydsl

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

 

이러한 방법은 컴파일러로 타입을 체크할 수 있어 가장 안전한 방법입니다.

다만 DTO 에 Querydsl 어노테이션을 유지해야 하는 점과 DTO 까지 Q 파일을 생성해야 하는 큰 단점이 존재하기 때문에

잘 고려해서 사용하시는 것을 추천합니다.

 

DISTINCT

List<String> result = queryFactory
  .select(member.username).distinct()
  .from(member)
  .fetch();

 

이렇게 설정을 하면 중복되는 username 은 제외하고 데이터를 출력하게 됩니다.

 

동적쿼리 해결

동적 쿼리를 해결하는 2가지 방법은 아래와 같습니다.

 

1. BooleanBuilder

2. Where 다중 파라미터 사용

 

BooleanBuilder 사용

@Test
public void 동적쿼리_BooleanBuilder() throws Exception {
  
  String usernameParam = "member1";
  Integer ageParam = 10;
  
  List<Member> result = searchMember1(usernameParam, ageParam);
}

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

 

Where 다중 파라미터 사용 

@Test
public void 동적쿼리_WhereParam() throws Exception {
  
  String usernameParam = "member1";
  Integer ageParam = 10;
  
  List<Member> result = searchMember2(usernameParam. ageParam);
}

private List<Member> searchMember2(String usernameCond, Integer ageCond) {
  
  return queryFactory
    .selectFrom(member)
    .where(usernameEq(usernameCond), ageEq(ageCond))
    .fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
  return usernameCond != null ? member.username.eq(usernameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
  return ageCond != null ? memeber.age.eq(ageCond) : null ;
}

 

Where 다중 파라미터는 where 조건에 null 은 무시하게 됩니다.

Where 다중 파라미터의 가장 큰 장점은 메서드를 다른 쿼리에서도 재활용 할 수 있다는 것입니다.

또한 쿼리 자체에 가독성이 높아지며 조합도 가능하게 됩니다.

private BooleanExpression allEq(String usernameCond, Integer ageCond) {
  return usernameEq(usernameCond).and(ageEq(ageCond));
}

 

수정, 삭제 벌크연산

long count = queryFactory
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(20))
                .execute();


// 기존 숫자에 1 더하기 
long count = queryFactory
               .update(member)
               .set(member.age, member.age.add(1))
               .excute();

// 삭제
long count = queryFactory
               .delete(member)
               .where(member.age.gt(10))
               .execute();

 

이것 JPQL 과 마찬가지로 영속성 컨텍스트는 무시하고 진행하기 때문에 꼭 영속성 컨텍스트를 깨끗하게 정리하고 다시 값을 불러오는 것이 좋습니다.

 

실제 사용 예시 ( Spring Data JPA + Querydsl )

MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long> {
  List<Member> findByUsername(String username);
}

 

Test

@Test
@Transactional 
class MemberRepositoryTest {
  
  @PersistenceContext
  EntityManger em;
  
  @Autowired
  MemberRepository memberRepository;
  
  @Test
  public void basicTest() {
    
    Member member = new Member("member1", 10);
    memberRepository.save(member);
    
    Member findMember = memberRepository.findById(member.getId()).get();
    Assertions.assertThat(findMember).isEqualTo(member);
    
    List<Member> result1 = memberRepository.findAll();
    Assertions.assertThat(result1).containExactly(member);
    
    List<Member> result2 = memberRepository.findByUsername("member1");
    Assertions.assertThat(result2).containsExactly(member);
  }
}

 

이렇게 테스트를 하고, Querydsl 전용 기능인 회원 search 기능을 작성하기 위해 추가적인 작업이 필요합니다.

 

사용자 정의 리포지토리를 시용하려면 아래와 같은 순서로 작성하시면 됩니다.

 

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

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

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

 

@Data
public class MemberSearchCondition {
  private String username;
  private String teamName;
  private Integer ageCoe;
  private Integer ageLoe;
}

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

public class MemberRepositoryImpl implements MemberRepositoryCustom {

  private final JPAQueryFactory queryFactory;
  
  public MemberRepositoryImpl(EntityManager em) {
    this.queryFactory = new JPAQueryFactory(em);
  }
  
  @Override
  List<MemberDto> search(MemberSearchCondition condition) {
    return queryFactory
             .select(new QMemberDto(
               member.id,
               member.username,
               member.age,
               team.id,
               team,name
             ))
           .from(member)
           .leftJoin(member.team, team)
           .where(usernameEq(condition.getUsername()),
                  teamNameEq(condition.getTeamName()),
                  ageQoe(condition.getAgeCoe()),
                  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 getAgeGoe(Integer ageGoe) {
    return isEmpty(ageGoe) ? null : member.age.goe(ageGoe);
  }
  
  private BooleanExpression getAgeLoe(Integer ageLoe) {
    return isEmpty(ageLoe) ? null : member.age.loe(ageLoe);
  }
}

 

이렇게 만든 후에 스프링 데이터 리포지토리에 사용자 정의 인터페이스를 상속하면 됩니다.

 

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryImpl {
  List<Member> findByUsername(String username);
}

 

커스텀 리포지토리에 동작 테스트를 추가합니다.

 

@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 searchCondition = new MemberSearchCondition();
  
  searchCondition.setAgeGoe(30);
  searchCondition.setAgeLoe(30);
  searchCondition.setTeamName("teamB");
  
  List<MemberDto> result = memberRepository.search(searchCondition);
  
  Assertions.assertThat(result).extracting("username").containsExactly("member4");
}

 

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

아까 만든 사용자 정의 인터페이스에 페이징을 2개 추가합니다.

public interface MemberRepositoryCustom {
  
  List<MemberDto> search(MemberSearchCondition condition);
  
  Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition);
  
  Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition);
}

 

이렇게 만든 인터페이스를 구현체를 통하여 만들어 주시면 됩니다.

 

// searchPageSimple(), searchPageComplex()

// searchPageSimple()
@Override
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {

  QueryResults<MemberTeamDto> results = queryFactory
    .select(new QMeberDto(
      member.id,
      member.username.as("name"),
      member.age,
      team.id,
      team.name))
    .from(member)
    .leftJoin(member.team, team)
    .where(usernameEq(condition.getUsername()),
                      teamNameEq(condition.getTeamName()),
                      ageQoe(condition.getAgeCoe()),
                      ageLoe(condition.getAgeLoe()))
    .offet(pageable.getOffset())
    .limit(pageable.getPageSize())
    .fetchResults();
}
  
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
  
// searchPageComplex()
@Override
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {

  List<MemberTeamDto> results = queryFactory
    .select(new QMeberDto(
      member.id,
      member.username.as("name"),
      member.age,
      team.id,
      team.name))
    .from(member)
    .leftJoin(member.team, team)
    .where(usernameEq(condition.getUsername()),
                      teamNameEq(condition.getTeamName()),
                      ageQoe(condition.getAgeCoe()),
                      ageLoe(condition.getAgeLoe()))
    .offet(pageable.getOffset())
    .limit(pageable.getPageSize())
    .fetch();
    
  long total = queryFactory
    .select(member)
    .from(member)
    .leftJoin(member.team, team)
    .where(usernameEq(condition.getUsername()),
                      teamNameEq(condition.getTeamName()),
                      ageQoe(condition.getAgeCoe()),
                      ageLoe(condition.getAgeLoe()))
    .fetchCount();
  
  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 getAgeGoe(Integer ageGoe) {
  return isEmpty(ageGoe) ? null : member.age.goe(ageGoe);
}
  
private BooleanExpression getAgeLoe(Integer ageLoe) {
  return isEmpty(ageLoe) ? null : member.age.loe(ageLoe);
}

 

전체 카운트를 조회하는 방법은 최적화 할 수 있으면 이렇게 분리하면 됩니다.

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

 

이렇게 설정하면 Controller 에서 불러 사용하면 됩니다.

 

@RestController
@RequiredArgsConstructor
public class MemberController {
  
  private final MemberRepository memberRepository;
  
  @GetMapping("/v1/members")
  public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
    return memberRepository.search(condition);
  }
  
  @GetMapping("/v2/members")
  public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
    return memberRepository.searchPageSimple(condition, pageable);
  }
}

'Spring' 카테고리의 다른 글

Swagger  (0) 2025.10.02
WAR 배포 및 분석  (0) 2025.09.25
Spring 외부설정, 조회방법  (0) 2025.09.22
JPA 의 OSIV  (1) 2025.09.11
JPA 에 대하여  (8) 2025.08.25