JPA에서 일반적으로 사용하는 Method Query에 QueryDSL을 연동

 

 

1. QuerydslPredicateExecutor

Predicate를 JpaRepository에서 추출할 수 있도록 해줌

public interface ProductRespository extends JpaRepository<ProductEntity, UUID>,
		ProductRepositoryCustom,
        QuerydslPredicateExecutor<ProductEntity>
{
}
  @GetMapping
  public ResponseEntity<ResProductGetDtoApiV1> getProductList(
      @QuerydslPredicate(root = ProductEntity.class) Predicate predicate,
      @PageableDefault(sort = "id", direction = Direction.DESC) Pageable pageable
  ) {
    return ResponseEntity.ok(productService.getProductList(predicate, pageable)
    );
  }
  @Override
  @Transactional(readOnly = true)
  public ResProductGetDtoApiV1 getProductList(Predicate predicate, Pageable pageable) {
    Page<ProductEntity> productEntityPage = productRepository.findAll(predicate, pageable);
    return ResProductGetDtoApiV1.of(productEntityPage);
  }

 

 

 

 

2. BooleanBuilder

BooleanBuilder도 Predicate임

만약 id가 1, 2, 3인 상품을 검색하고 싶다면?

id=1,2,3 안됨

  @GetMapping
  public ResponseEntity<ResProductGetDtoApiV1> getProductList(
      @RequestParam(required = false) List<Long> idList,
      @QuerydslPredicate(root = ProductEntity.class) Predicate predicate,
      @PageableDefault(sort = "id", direction = Direction.DESC) Pageable pageable
  ) {
    return ResponseEntity.ok(productService.getProductList(predicate, pageable)
    );
  }
  @Override
  @Transactional(readOnly = true)
  public ResProductGetDtoApiV1 getProductList(List<Long> idList, Predicate predicate, Pageable pageable) {
  	BooleanBuilder bb = new BooleanBuilder(predicate);
    if(!idList.isEmpty() && idList != null) {
    	bb.and(productEntity.id.in(idList));
    }
    bb.and(productEntity.stock.gt(0));
    Page<ProductEntity> productEntityPage = productRepository.findAll(bb, pageable);
    return ResProductGetDtoApiV1.of(productEntityPage);
  }

 

 

 

3. QuerydslBinderCustomizer

추출된 Predicate의 조건을 수정할 수 있음

1, 2번은 문자 검색 시 equals 조건만 검색

QuerydslBinderCustomizer는 contains 조건으로 바꿀 수 있음

public interface ProductRepositoryV2 extends JpaRepository<ProductEntity, UUID>,
    ProductRepositoryCustom,
    QuerydslPredicateExecutor<ProductEntity>,
    QuerydslBinderCustomizer<QProductEntity> {

  @Override
  default void customize(QuerydslBindings bindings, @NotNull QProductEntity qProductEntity) {
    bindings.bind(String.class).all((StringPath path, Collection<? extends String> values) -> {
      List<String> valueList = new ArrayList<>(values.stream().map(String::trim).toList());
      if (valueList.isEmpty()) {
        return Optional.empty();
      }
      BooleanBuilder bb = new BooleanBuilder();
      for (String s : valueList) {
        bb.or(path.containsIgnoreCase(s));
      }
      return Optional.of(bb);
    });
  }
}

 

'DB > QueryDsl' 카테고리의 다른 글

[QueryDSL] 대용량 최적화  (0) 2025.04.24
[QueryDSL] 설정  (1) 2025.04.24
QueryDSL  (0) 2025.04.23

1.  동적 쿼리 구현 시 BooleanBuilder 대신 BooleanExpression

BooleanBuilder는 조건이 많아질 수록 가독성이 떨어짐.

BuildExpression을 사용하면 null일 경우 조건절이 제거되므로 깔끔한 동적 쿼리 구성 가능

 

2.  count()보단 exists() 사용

count()는 조건에 맞는 로우를 모두 세어야 함 → 비용↑

exists()는 매칭되는 첫 번째 로우를 찾으면 바로 종료 → 빠름

 

JPA와 QueryDSL에서 제공하는 fetchOne()이나 특정 메서드들은 내부적으로 count 쿼리 사용

이를 개선하기 위해 limit 1 또는 fetchFisrt() != null을 사용하여 데이터 존재 여부만 빠르게 확인 → exist()와 유사한 성능 기대

 

3.  Entity 대신 DTO 조회

엔티티 직접 조회 시

  • 필요 없는 컬럼 조회
  • 1:1 관계 시 N+1 문제 발생
  • 2차 캐시 등 불필요한 기능 작동으로 오버헤드 증가

단순 조회나 대용량 조회에서는 DTO를 통해 컬럼만 가져오는 것이 성능에 유리

@Getter
public class MemberSimpleDto {
    private Long id;
    private String name;
    private String city; // Address에서 필요한 정보만
    
    @QueryProjection // DTO도 Q객체가 만들어짐
    public MemberSimpleDto(Long id, String name, String city) {
        this.id = id;
        this.name = name;
        this.city = city;
    }
}
// Q객체 생성 됐을 때 (@QueryProjection)
List<MemberSimpleDto> result = jpaQueryFactory
    .select(new QMemberSimpleDto(member.id, member.name, address.city))
    .from(member)
    .join(member.address, address) // 필요한 join 한 번에 처리
    .where(member.age.gt(20))
    .fetch();
    
    
// Q객체 없을 때
List<MemberSimpleDto> result = queryFactory
    .select(Projections.constructor(
        MemberSimpleDto.class,
        member.id,
        member.name,
        member.address.city
    ))
    .from(member)
    .join(member.address, address) // 필요한 join 한 번에 처리
    .where(member.age.gt(20))
    .fetch();

 

데이터 수정? 어쩔 수 없이 엔티티 가져와야겠즤~

 

4.  더티체킹보다 벌크 업데이트(Bulk Update)

만약 100개의 데이터를 업데이트 하려고 하면 100개의 update문이 날아감

소규모 업데이트는 더티체킹해도 전혀 문제가 없지만 대규모 업데이트는 벌크 업데이트를 해야함

 →  무조건 JPA가 만능이 아니다 (JDBC Template or Spring Batch)

 

'DB > QueryDsl' 카테고리의 다른 글

[QueryDSL] JPA + QueryDSL = Search  (0) 2025.04.28
[QueryDSL] 설정  (1) 2025.04.24
QueryDSL  (0) 2025.04.23
// build.gradle

dependencies {
    ...
    implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    implementation 'org.jetbrains:annotations:24.1.0'
}

...

clean {
    delete file('src/main/generated') //IntelliJ가 직접 Java를 실행해 빌드할 경우에는 삭제해줘야함
}

 

> complieJava

Q타입은 컴파일 시점에서 자동 생성 되므로 버전관리에 보함하지 않는 것이 좋음

 

 

 


 

 

N+1 문제 해결

@ManyToOne(fetch = FetchType.LAZY)

 

Lazy옵션 사용 시 N+1 문제 발생~!

 

@Transactional(readOnly=true)
  jpa:
  ...
    properties:
      hibernate:
        default_batch_fetch_size: 500 # 한 번 조회시 세트로 조회
    open-in-view: false # 잘 이해안됨

 

or

 

fetchjoin()

...
.from(productImage)
.join(product).fetchJoin()

'DB > QueryDsl' 카테고리의 다른 글

[QueryDSL] JPA + QueryDSL = Search  (0) 2025.04.28
[QueryDSL] 대용량 최적화  (0) 2025.04.24
QueryDSL  (0) 2025.04.23
만약 조회 기능에 '나이', '이름' 이라는 검색 조건을 추가한다면?
String sql = 
    "select * from member" +
    "where name like ?" +
    "and age between ? and ?";
    
// "select * from memberwhere name like?and age between ? and ?"

빌드는 성공함

 

 

Query 문제점

  • 쿼리는 문자  Type-check 불가능  실행하기 전까지 작동 여부 확인 불가

 

에러의 종류

1. 컴파일 에러 (좋은 에러)

  • 타입이 잘못 됐을 때.. 빌드도 안됨 문제가 터지기 전에 인지 가능

2. 런타임 에러 (나쁜 에러)

  • 호출 했을 때  발생

 

🤔 만약 SQL이 클래스처럼 타입이 있고 자바 코드로 작성할 수 있다면? → Type-safe

  • 컴파일 시 에러 체크 가능

 

QueryDSL

쿼리를 Java로 Type-safe하게 개발할 수 있게 지원하는 프레임워크로 주로 JPA쿼리(JPQL)에 사용

D: Domain

S: Specific

L: Language

 

 

 

김씨 성을 가진 20~40살인 사람을 나이 많은 순서로 3명을 출력?

 

JPA에서 Query 방법

1. JPQL(HQL) : type-safe 하지 않아서 동적 쿼리 생성이 어려움

  @Test
  public void jpql() {
    
    String query = 
        "select m from Memeber m " +
            "where m age between 20 and 40 " +
            "and m.name like '김%' " +
            "order by m.age desc";

    List<Member> resultList = 
        entityManager.createQuery(query, Member.class)
            .setMaxResult(3).getResultList();
  }

 

2. Criteria API : 복잡

3. MetaModel Criteria API(type-safe) : 복잡

 

 

QueryDSL을 쓴다면?

APT: Annotation Processor Tool

 

// QueryDLS

  JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
  QMember m = Qmember.member;
  
  List<Member> list = query
      .select(m)
      .from(m)
      .where(
          m.age.between(20, 40).and(m.name.like("김%"))
      )
      .orderBy(m.age.desc())
      .limit(3)
      .fetch(m);

query → JPQL → SQL

 

 

 

SpringDataJPA + QueryDSL

  • SpringData 프로젝트의 약점은 조회
  • QueryDSL로 복잡한 조회 기능 보완 (복잡한 쿼리, 동적 쿼리)

 


 

쿼리 실행 메서드에 따른 반환 타입:

  • .fetch(): List<T> (결과 목록)
  • .fetchOne(): T (단일 결과)
  • .fetchFirst(): T (첫 번째 결과)
  • .fetchCount(): long (결과 개수)
  • .fetchResults(): QueryResults<T> (페이징 정보 포함 결과)
  • .execute(): long (업데이트/삭제된 행 수)

'DB > QueryDsl' 카테고리의 다른 글

[QueryDSL] JPA + QueryDSL = Search  (0) 2025.04.28
[QueryDSL] 대용량 최적화  (0) 2025.04.24
[QueryDSL] 설정  (1) 2025.04.24

+ Recent posts