Post

05 QueryDsl

05 QueryDsl

1. Note

  • JPA에서 자동화 방식으로 사용하다가,
  • 조건이나 뭔가 복잡한 상황에서 개발자가 직접 사용하는 패턴.
  • 뭔가 구분이 잘 안되기는 하지만,
    • 레파지토리는 DB접근, 쿼리수행, 엔티티 저장 및 조회 등의 역할
    • 엔티티는 상태랑 비즈니스규칙 관련, 값 변경 할 경우 사용하는 역할

2. Spring Data JPA / JPQL / Query DSL

1. Spring Data JPA / JPQL / Query DSL

방식작성 위치장점단점동적 조건 처리
메서드 이름 기반JpaRepository 인터페이스코드 간단, 별도 쿼리 작성 불필요조건이 복잡하면 메서드 이름 길어짐어려움
JPQL (@Query)JpaRepository 인터페이스엔티티 기준, SQL과 유사, 직관적문자열 기반, 동적 조건 복잡제한적
QueryDSL커스텀 레포지토리 구현체타입 세이프, 동적 조건 자유, 유지보수 용이설정 필요, 구현 복잡매우 용이

2. 메서드 이름 기반 쿼리 (Spring Data JPA)

  • JpaRepository에서 제공하는 기본 기능을 이용해, 메서드 이름으로 쿼리를 자동 생성합니다.
    • 장점 : 간단하고 코드가 짧음, 별도의 쿼리 작성 불필요
    • 단점 : 복잡한 조건이나 동적 쿼리는 어렵습니다.
      1
      2
      3
      4
      
      public interface ProductRepository extends JpaRepository<Product, Long> {
       List<Product> findByName(String name);
       List<Product> findByCategoryIdAndPriceLessThan(Long categoryId, BigDecimal price);
      }
      

3. JPQL 사용 (Query annotation)

  • @Query 애노테이션으로 JPQL을 직접 작성해서 조회합니다.
    • 장점 : SQL과 유사하지만 엔티티 기준으로 작성, 단순 조회는 명확하고 직관적
    • 단점 : 문자열 기반이므로 동적 조건 처리 시 복잡해짐
      1
      2
      3
      4
      
      public interface ProductRepository extends JpaRepository<Product, Long> {
       @Query("SELECT p FROM Product p WHERE p.name = :name AND p.category.id = :categoryId")
       List<Product> findByNameAndCategory(@Param("name") String name, @Param("categoryId") Long categoryId);
      }
      

4. QueryDSL 사용 (타입 세이프, 동적 쿼리)

  • QueryDSL 라이브러리를 이용해 엔티티 기준으로 동적 쿼리를 작성합니다.
    • 장점: 조건 조합 자유롭고, 컴파일 시점에서 타입 체크 가능, 유지보수 용이
    • 단점: 설정과 구현체가 필요, 조금 더 복잡
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      public class ProductRepositoryImpl implements ProductRepositoryCustom {
         @PersistenceContext
         private EntityManager em;
          
         QProduct product = QProduct.product;
          
         @Override
         public List<Product> search(String name, Long categoryId, BigDecimal maxPrice) {
             JPAQuery<Product> query = new JPAQuery<>(em);
             return query.select(product)
                         .from(product)
                         .where(
                             name != null ? product.name.eq(name) : null,
                             categoryId != null ? product.category.id.eq(categoryId) : null,
                             maxPrice != null ? product.price.loe(maxPrice) : null
                         )
                         .fetch();
         }
       }
      

3. Query DSL

1. 의존성 추가 필요

1
2
3
4
5
6
7
8
9
10
 // build.gradle
 dependencies {
    // QueryDSL JPA 라이브러리
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

    // QueryDSL 어노테이션 프로세서 (Q클래스 생성)
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
 }

2. Q 클래스 생성을 위한 gradle 설정

1
2
3
4
5
6
7
8
9
10
  //그래들 최하단에 등록
  def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

  tasks.withType(JavaCompile).configureEach {
      options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
  }
  
  clean {
      delete querydslDir
  }

3. JPQL 빈등록

1
2
3
4
5
6
7
8
9
10
11
  @Configuration
  public class QueryDslConfig {
  
      @PersistenceContext
      private EntityManager entityManager;
  
      @Bean
      public JPAQueryFactory jpaQueryFactory() {
          return new JPAQueryFactory(entityManager);
      }
  }

4. 사용 패턴

1. 기본형태

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    @Repository
    @RequiredArgsConstructor
    public class ProductQueryRepository {
    
      private final JPAQueryFactory queryFactory;
    
      public List<Product> findProducts(String name, Double minPrice, Double maxPrice) {
      
        QProduct product = QProduct.product; //선언해도 가능하지만 필드로 해도됨.
        
        return queryFactory
            .selectFrom(product) // SELECT * FROM product
            .where(
                // 각 조건이 null이면 where절이 생성 되지 않음.
                // 쉼표로 나누는 경우 and로 연결
                name != null ? product.name.contains(name) : null,
                minPrice != null ? product.price.goe(minPrice) : null, // goe: >=
                maxPrice != null ? product.price.loe(maxPrice) : null  // loe: <=
                // or 연결
                name != null ? product.name.contains(name).or(product.description.contains(name)) : null
            )
            .fetch();
      }
    }

2. where절의 메소드화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   public List<Product> findProducts(String name, Double minPrice, Double maxPrice) {
      return queryFactory
              .selectFrom(product)
              .where(
                  nameContains(name), // 메서드 호출
                  priceGoe(minPrice),
                  priceLoe(maxPrice)
              )
              .fetch();
  } 
  
  private BooleanExpression nameContains(String name) {
      return name != null ? product.name.contains(name) : null;
  }

3. join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public List<Product> findProducts(String name, Double minPrice, Double maxPrice) {
    // Q클래스 인스턴스 선언 필수
    QProduct product = QProduct.product;
    QCategory category = QCategory.category;

    return queryFactory
        .selectFrom(product)
        .join(product.category, category) //left, right 다먹음! 
        .where(
            name != null ? product.name.contains(name) : null,
            minPrice != null ? product.price.goe(minPrice) : null,
            maxPrice != null ? product.price.loe(maxPrice) : null,
            category.name.eq("도서")
        )
        .fetch();
 }

4. Paging

  • 소스
    1
    2
    3
    4
    5
    6
    
       List<Product> products = queryFactory
      .selectFrom(product)
      .orderBy(product.price.desc()) // 정렬
      .offset(page * size)           // 시작 위치
      .limit(size)                   // 가져올 데이터 수
      .fetch();                      // 실행
    
  • 구분

    pagesizeoffset데이터 범위
    05001 ~ 50
    1505051 ~ 100
    250100101 ~ 150
    350150151 ~ 200

4. QueryDSL에서 DTO 조회

1. Constructor / fields / bean / queryProjection

방식매핑 기준컴파일 안전성런타임 에러 위험성능코드 간결성QueryDSL 의존성
constructor생성자 파라미터 순서높음좋음좋음없음
fields필드명중간보통 (리플렉션)좋음없음
beansetter중간보통 (리플렉션)보통없음
QueryProjection생성자(Q타입)낮음좋음보통있음

2. 각기 패턴

1. Constructor

1
2
3
4
5
6
7
  .select(Projections.constructor(ProductDto.class,
      product.name,
      product.price
  ))
   // 생성자 기반
   // 순서 맞아야 함 (중요)
   // 컴파일 체크 X (런타임 에러)  

2. fields 방식

1
2
3
4
5
6
7
  .select(Projections.fields(ProductDto.class,
      product.name,
      product.price
  ))
   // 필드명 기준 매핑
   // 리플렉션 사용
   // private 필드도 접근 가능

3. bean

1
2
3
4
5
6
  .select(Projections.bean(ProductDto.class,
    product.name,
    product.price
  ))
  // setter 기반
  // JavaBean 규칙 필요 (get/set) 

4. @QueryProjection 방식

1
2
3
4
5
6
7
  .select(new QProductDto(
    product.name,
    product.price
  ))
  // 컴파일 시점 체크
  // 타입 안전성 최고
  // 대신 QueryDSL 의존성 생김  

5. QueryProjection

1. 프로젝션

  • 필요한 컬럼들만 선별하여 처음부터 DTO(데이터 전송 객체)에 담아 조회하는 기술
  • QueryDSL에서 DTO를 타입 안전하게 조회하기 위해 사용하는 기능

2. 사용방법

1. 사용할 DTO에 애노테이션 추가

1
2
3
4
5
6
7
8
9
10
11
12
  public class ProductDto {
  
      private String name;
      private int price;

      // DTO 생성자 애노테이션 추가
      @QueryProjection
      public ProductDto(String name, int price) {
          this.name = name;
          this.price = price;
      }
  }

2. QueryDsl impl

1
2
3
4
5
6
7
8
 // Query DSL 사용하는 impl
 return queryFactory
    .select(new QProductDto(
        product.name,
        product.price
    ))
    .from(product)
    .fetch();
This post is licensed under CC BY 4.0 by the author.