[우아콘2020] 수십억건에서 QueryDSL 사용하기 정리
카테고리 없음

[우아콘2020] 수십억건에서 QueryDSL 사용하기 정리

발표자: 이동욱님
영상링크: YouTube
이 글에서 나오는 이미지는 모두 발표 영상에서 가져온 이미지입니다.

발표 주제: QueryDSL & JPA 개선 팁

  • specific
    • MySQL 5.6버전에서 진행함
      • why? 인덱스 컨디션 푸시다운과 서브쿼리의 최적화가 진행되어있기 때문에 버전별 차이가 있을 수 있다.

워밍업

extends/ implements 사용하지 않기

  • 꼭 무언가를 상속/ 구현받지 않더라도, 꼭 특정 엔티티를 지정하지 않더라도 QueryDsL을 사용하는 방법이 없을까?
    • JPAQueryFactory만 있으면 QueryDSL은 사용할 수 있다. 즉, extends/implements 없이 JpaQueryFactory만 생성자 주입을 통해 사용하면 쿼리디엣셀 관련 기능을 모두 사용할 수 있다.

동적쿼리는 BooleanExpression을 사용하기

BooleanBuilder 대신 BooleanExpression을 사용하자. 왜?

  • 메서드로 만들어서 해당 값이 null로 리턴될 경우 조건절에서 제거된다. 조금 더 명시적으로 알 수 있는 쿼리 형태를 만들 수 있다.
    • 단, 만약 모든 조건이 null이 된다면 조건이 제거되기 때문에 성능에 대장애가 올 수 있다.

Exist 메서드 금지

  • 왜 이렇게 성능 차이가 날까?
    어떤 특정 조건을 만족하는 로우가 있냐 없냐를 판단하기 위해서 exist의 경우 첫번째 값이 확인되는 순간 쿼리가 종료되는 데에 반해서 카운트의 경우에는 첫번째 것이 발견되었다 하더라도 끝까지 모든 조건을 확인하기 때문에 성능상 차이가 발생한다.

즉, exist는 조건에 해당하는 row가 하나라도 발견되면 쿼리를 종료한다.

이는 스캔 대상이 앞에 있을 수록 더 많은 차이가 발생한다. exist는 발견하는 순간 쿼리가 끝나기 때문에 첫번째 대상이 어디에 있냐에 따라서 성능 차이가 더더더더 많이 발생한다.

  • 근데 왜 exist를 사용하지 말라고 하는가?
    QueryDsl의 exist는 실제로 count() > 0 으로 실행된다.

  • 이를 exist로 바꿀 순 없을까?
    바꾸고 싶어도 바꿀 수 없다. JPQL이 from 없이는 select 쿼리를 생성할 수 없기 때문이다.

따라서 exist를 사용하기 위해 직접 exist를 구현해서 사용하자.

  • 어떻게 직접 구현하나?
    • 조회 결과물이 앞까지 동일하고, 뒤에서 딱 한건만 찾으면 쿼리를 종료하는 방식으로 exist를 구현
      주의점: 조회 결과에 대해서 0보다 크냐로 비교하면 안됨. 조회 결과가 없으면 null이 반환되기 때문.

Cross Join 회피하기

cross join같은 경우는 나올 수 있는 모든 경우의 수를 대상으로 하기 때문에 성능이 좋을 수가 없다. 묵시적 Join이라고 해서 실질적 조인문을 쓰지 않더라도 where절에서 join문에 대해 직접적으로 선언하게 되면 cross join이 묵시적으로 발생된다.
(하이버네이트 이슈임!)

  • 어떻게 피하나?
    • 명시적 Join. 실제 조인문을 작성하자.
  • Cross Join 관련해서 본인이 작성한 글을 레퍼런스로 달겠다 > <
 

QueryDsl(JPA)에서의 Cross Join 문제

QueryDsl을 사용하다 Cross Join이라는 문제를 만나게 되었다. CrossJoin이란 집합에서 나올 수 있는 모든 경우의 수를 이야기한다. 즉, Cartesian Product와 같다. ex. A: {a,b}, B:{c,d}의 경우 A cross join B..

joanne.tistory.com

Select와 관련된 성능 개선

Entity보다는 Dto를 우선하자

Entity를 조회하면 다음과 같은 단점이 있다.

  • 하이버네이트 1차, 2차 캐시 문제 발생
  • 불필요한 컬럼을 조회하게 된다.
  • OneToOne N+1 쿼리 발생
  • 즉, 단순 조회 기능에서는 성능 이슈 요소가 많다.
  • 그럼 어떨 때 Entity, 어떨 때 Dto를 사용해야할까?
    • 실시간으로 Entity 변경이 필요한 경우에는 Entity를 사용하자.
    • 고강도 성능 개선 or 대량의 데이터 조회가 필요한 경우에는 Dto를 사용하자.

조회 컬럼 최소화하기

  1. 이미 알고 있는 값을 조회하지 말자.

이는 as 표현식으로 대체할 수 있다. as 컬럼은 select에서 제외된다.

  1. Select 컬럼에 엔티티를 자제하자.

예를 들어, AdBond라는 클래스를 만들기 위해 customer의 id가 필요하다고 해보자. 이때, adItem.customer를 select 절에 포함한다면, 우리는 customer의 id만을 필요로함에도 불구하고 customer의 모든 정보를 조회하게 된다.

또한 customer에 oneToOne 관계가 있다면, 매 건마다 해당 엔티티가 조회된다. (N+1 문제 발생)
즉, 한방 쿼리를 작성했는데 실제로는 N+1, (N+1)*(N+1), .. 만큼의 쿼리가 수행되게 된다. 으악

  • 연관된 Entity의 save를 위해서는 반대편 Entity의 ID만 있으면 된다.

as를 사용해보자.

  • Entity와 컬럼 조회 성능 차이 비교

여기서, 만약 distinct까지 포함되어있다면 select에 선언된 엔티티의 컬럼 전체가 distinct의 대상이 된다. 즉, distinct를 위한 임시 테이블 등을 만들기 위한 작업이 수행되기 때문에 많은 작업을 필요로 한다. 따라서 distinct의 경우에는 꼭 필요한 컬럼만 조회해오자.

Group By 최적화

인덱스를 타지 않게 되는 경우에는 FileSort가 매번 발생한다. mySQL에서는 order by null을 사용하면 fileSort가 제거되지만, 쿼리디엣셀에서는 order by null 문법을 지원하지 않는다.

  • 그럼 어떻게 해야할까?
    • Order By Null을 직접 구현하자.
      아래 예시는 OrderByNull을 가진 클래스를 만들어서 사용한다.
      단, 페이징의 경우 order by null을 사용하지 못한다.
  • 정렬이 필요하더라도, 조회 결과가 100건 이하라면 애플리케이션에서 정렬하는 것을 추천한다.
    • 왜?
      • DB 보다는 WAS의 자원이 좀 더 여유롭다. (DB는 한대를 두더라도 WAS는 여러대를 두는 등..)
      • WAS에서 가능하다면 WAS에서 정렬하는 것을 추천한다.

커버링 인덱스

  • 쿼리를 충족시키는데 필요한 모든 컬럼을 갖고 있는 인덱스
  • select / where / order by / group by 등에서 사용되는 모든 컬럼이 인덱스에 포함된 상태
  • NoOffset 방식과 더불어 페이징 조회 성능을 향상시키는 가장 보편적인 방법

커버링 인덱스에 대해서 자세히 알아보려면 다음 글을 참고하자.
1. 커버링 인덱스 (기본 지식 / WHERE / GROUP BY)

 

from 절의 서브쿼리가 커버링 인덱스

JPQL은 from절의 서브쿼리를 지원하지 않는다.

  • 또 직접 구현

Update/Insert문에서의 성능 개선

일괄 업데이트 최적화

 

트랜잭션 내부에 있을 때 엔티티를 조회해서, 해당 엔티티의 값을 바꾸면 디비에 자동으로 변경되는 방식
우측 한방 업데이트에 비해서 성능 상 이점이 매우 떨어지게 된다!!

  • 그럼 일괄 업데이트는 단점 없나?
    • 일괄 업데이트를 하게 되면 하이버네이트 캐시의 갱신이 안된다. 이럴 경우에는 업데이트 대상들에 대한 Cache Eviction을 필요로한다.
  • 그럼 언제 어떻게 사용할까?
    • Dirty Checking - 실시간 비즈니스 처리, 실시간 단건 처리
    • Querydsl.update - 대량의 데이터를 일괄로 Update 처리 시

JPA, QueryDSL은 마법이 아니다. 결국 DB에 쿼리를 날려서 조회하는 것은 동일하다. 이 쿼리가 성능 상 어떤 이슈를 주는지 확인하고, 충분히 고민해야한다. 진짜 엔티티가 필요한게 아니라면 queryDsl과 DTO를 통해 딱 필요한 것만 조회하자.

JPA Bulk Insert는 자제하자.

  • 그럼 어떻게?
    • 여러가지 시도 중.. 걸러 듣길 바란다고 하심

JdbcTemplate을 사용하지 않고, Qclass 기반으로 Native SQL을 사용할 수 있다?

  • 그럼 QClass는 어떻게 생성되나?

  • EntityQL?
    • JPA Entity 어노테이션을 기반으로 Querydsl-SQL QClass를 생성해주는 오픈소스 프로젝트이다.

EntityQL을 사용하면 Bulk Insert가 지원되기 때문에 개선 가능

  • 그럼 왜 걸러들어라고 했나?
  • EntityQL의 단점

마무리

  • 상황에 따라 ORM/전통 Query 방식을 골라 사용할 것
  • JPA/QueryDsl로 발생하는 쿼리 한번 더 확인하기