1. no implements, no extends 

  • JpaRepository를 상속받는 TestRepository에 대해서, TestRepositoryCustom 인터페이스를 새로 만들고, TestRepositoryImpl을 주입시키는 구조.
  • QuerydslRepositorySupport를 상속받고 super에 entity를 등록하는 구조

 

너무 불편해! -> 사실은 JpaQueryFactory만 있으면 충분함 -> 주입받아서 사용하자.

 

2. 동적 쿼리

  • BooleanBuilder를 사용해도 괜찮지만, 쿼리를 예상하기 어렵다.
  • 대신에 BooleanExpression을 사용해 보자.
  • 모든 조건이 null인 경우를 주의하자.

 

3. 조회 성능

querydsl의 exist 금지

  • exist 메서드와 count 메서드를 이용해서 특정 조건을 만족하는 대상을 찾는 경우를 생각
    • exist는 최초 발견 시 종료
    • count는 끝까지 탐색
    • 스캔 대상이 앞에 있을수록 성능 차이가 발생함
  • 하지만 QueryDSL의 exists는 실제로 count() > 0을 실행한다.
  • 대안으로 fetchFirst() != null 을 사용하자.

 

*나의 QuerydslJpaPredicateExecutor는 아래와 같이 패치된 상태이다. 

spring data jpa 2.7.x 기준 QuerydslJpaPredicateExecutor
createQuery(predicate).select(Expressions.ONE).fetchFirst() != null;

 

QuerydslPredicateExecutor를 상속받는 jpaRepository의 exists를 호출하지 말라는 이야기 같다. 위 코드는 구현체 내부에 exists임! 그니까는 아래처럼 spring data jpa에서 지원하는 QuerydslPredicateExecutor 인터페이스를 사용할 때, 구현체인 QuerydslJpaPredicateExecutor 내부에서 과거에는 exists가 fetchCount > 0으로 되어 있어서 비효율적인 거라고 이해해도 되는 건가?

interface MemberRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {}

 

cross join 회피

  • cross join은 성능이 잘 나오지 않는다. (모든 경우의 수를 대상으로 함, 일부의 DB는 최적화됨)
  • 묵시적 join으로 cross join이 발생
  • hibernate 이슈라 spring data jpa도 동일하게 발생
  • 명시적 join으로 inner join을 사용하자!

 

Entity 보다는 Dto를 우선

  • 엔티티를 조회하면 성능 이슈 요소가 많음..
    • hibernate 캐시
    • 불필요한 컬럼 조회
    • OneToOne N + 1 쿼리
  • Entity 조회와 Dto 조회의 목적을 나누자
    • Entity 조회는 실시간으로 Entity 변경이 필요한 경우
    • Dto 조회는 고강도 성능 개선 or 대량의 데이터 조회가 필요한 경우
  • 조회 컬럼을 최소화하자
    • 이미 알고 있는 값은 Expressions.[asNumber | asString].as('fieldName')으로 대체 가능함
    • Select 컬럼에 Entity 자제
      • @OneToOne라면 N + 1 문제가 발생할 수 있음
        • default가 Eager이며, 양방향인 경우 연관관계 주인이 아닌 엔티티를 조회하면 Lazy가 동작하지 않음
        • QueryDSL은 기본적으로 jpql 빌더이니, 즉시 로딩 N + 1을 피하기 어려움
        • 근데, 사실 연관된 Entity의 save를 위해서는 반대편 Entity의 ID만 있으면 됨!
      • distinct로 시작하면 select에 선언된 Entity의 컬럼 전체가 distinct 대상이 됨
        • distinct를 위한 공간, 시간 때문에 성능이 줄어든다.

 

Group by 최적화

  • MySQL에서 group by를 실행하면 Filesort가 필수로 발생함(인덱스를 타지 않는다면)
  • MySQL에서는 이 부분을 order by null을 사용해서 불필요한 정렬 작업을 제거할 수 있다.
  • 하지만 QueryDSL에서 order by null을 지원하지 않음 ->직접 구현해야 함
  • 정렬이 필요해도, 100건 이하라면 애플리케이션에서 정렬하는 것을 권장(was 자원이 상대적으로 여유로움)
  • 단, 페이징의 경우 order by null을 사용하지 못함

 

커버링 인덱스

  • 쿼리를 충족시키는데 필요한 모든 컬럼을 갖고 있는 인덱스를 의미
  • select / where / order by / group by등에서 사용되는 모든 컬럼이 인덱스에 포함된 상태
  • NoOffset 과 더불어 페이징 조회 성능을 향상시키는 가장 보편적인 방법
  • jpql은 from 절 서브쿼리를 지원하지 않기 때문에 우회해야 함
    • 커버링 인덱스 조회를 나눠서 진행해야 하는데..
      • PK를 커버링 인덱스로 빠르게 조회
      • 조회된 Key로 Select 컬럼들을 후속 조치

 

4. 삽입/수정 성능

일괄 Update 최적화

  • 무분별한 변경 감지를 사용하는 대신 일괄 업데이트를 사용하자
  • 하지만, 하이버네이트 캐시는 일괄 업데이트 시 캐시 갱신이 안됨
  • 변경 감지는 실시간 비즈니스 처리, 실시간 단건 처리에 사용
  • 대량 데이터를 일괄 update 할 때는 변경 감지 x

 

진짜 Entity가 필요한 게 아니면 QueryDSL, DTO를 통해 필요한 항목만 조회, 업데이트하라!

 

일괄 Insert 최적화

  • JDBC는 rewriteBatchedStatements 이라는 insert 합치기 옵션이 존재한다.
  • 하지만 JPA에서 auto_increment일때 insert 합치기가 적용되지 않는다.
  • -> JPA로 일괄 insert를 자제하자. 
  • -> 대신 JdbcTemplate로 처리하는 게 더 낫긴 하는데...
    • 컴파일체크, 코드-테이블 간의 불일치 체크등 Type Safe 개발이 어려움...
    • 어쩌지..? -> Q클래스 기반 Native SQL을 일부 적용해서 풀어낼 수도 있음

 

QueryDSL != QueryDSL-jpa

  • QueryDSL과 QueryDSL-jpa는 다르다.
  • QueryDSL은 추상화된 최상위 계층임
  • JPA, SQL, MondoDB... 와 같은 하위 모듈들이 존재함
  • 일괄 Insert 최적화를 위해서 Native SQL을 Q클래스 기반으로 사용하려면 번거로움
    • QueryDSL-SQL 플러그인으로 테이블 긁어와서 Q클래스 만들어야 함
    • 대신 EntityQL이라는 오픈소스로 JPA 엔티티 기반으로 QueryDSL-SQL Q클래스를 만들 수 있다고 함
  • 근데 단점이 쫌...
    • Gradle 5 이상 필요
    • 애노테이션에 (name="") 필수(@Column, @Table)
    • 원시 타입 못쓰고, Wrapper로 선언해야 함
    • 설정이 복잡
    • Embedded 미지원
    • QueryDSL-SQL의 미지원으로 insert 쿼리를 @Column의 name으로 만들 수가 없음
      • 컬럼명과 필드명이 일치해야 하는 BeanMapper만 지원함
      • 즉, @Column용 Mapper가 별도로 필요함

 

출처 : [우아콘2020] 수십억건에서 QUERYDSL 사용하기

이하눌