수십억건에서 QueryDSL 사용하기

밀크야살빼자·2024년 5월 1일
0

1. 워밍업

extends / implements 사용하지 않기

꼭 무언가를 상속/구현 받지 않더라도, 꼭 특정 Entity를 지정하지 않더라도 QueryDSL을 사용할 수 있는 방법이 무엇일까?


JPAQueryFactory 를 Bean 으로 사용함으로서 extends/implemets 를 제거할 수 있습니다.

동적 쿼리

사진에서는 if문이 3개지만 컬럼이 5개, 10개 또는 그 이상이 된다면, if문 만으로 한 화면을 다 차지하게 됩니다. 이렇게 되면 어떤 쿼리인지 예상하기 어렵습니다.

2. 성능 개선 - Select

Querydsl의 exist 금지

  • 성능 차이가 나는 이유는 무엇일까?
    어떤 특정 조건을 만족하는 로그가 있는지 없는지를 판단하는데 있어서 exist는 첫 번째 값이 발견되는 순간 쿼리가 종료되지만, count는 첫 번째가 발견된다고 하더라도 끝까지 모든 조건들을 다 체크하기 때문입니다.
    즉, exist는 조건에 해당하는 row가 하나라도 발견되면 쿼리를 종료합니다. 이는 exist는 스캔 대상이 앞에 있을 수록 더 심한 성능차이가 발생합니다.


하지만 QueryDSL의 exists는 count를 사용해서 체크합니다. 따라서 QueryDSL의 exist를 사용할 수가 없다.

  • exist를 바꿀 수 있는 방법은?
    JPQL은 from 없이는 select 쿼리를 생성할 수 없기 때문입니다.

exists가 빠른 이유는 조건에 해당하는 row 1개만 찾으면 바로 쿼리가 종료한다! → 직접 구현하자!

  • 어떻게 구현해야 하나?
    조회 결과물이 앞까지 동일하고, 뒤에서 딱 한건만 찾으면 쿼리를 종료하는 방식으로 exist를 구현합니다.
    주의점 : 조회 결과를 0보다 크거나 작거나로 판단하는 것이 아니라 null로 판단해야합니다. 그 이유는 조회 결과가 없을 때, 0이 반환되는 것이 아니라 null로 반환되기 때문입니다.

위와 같이 직접 구현해서 쿼리를 실행해 본 결과 SQL.exist와 QueryDSL.limit와 성능의 큰 차이가 나지 않습니다. 따라서 SQL.exist와 비슷한 성능을 내는 쿼리를 만들 수 있습니다.

Cross Join 회피

Cross Join은 나올 수 있는 모든 경우의 수를 대상으로 하기 때문에 성능이 좋을 수가 없습니다.

묵시적 조인을 사용하게되면 실질적 조인문을 쓰지 않더라도 where절에서 조인문에 대해 직접적으로 선언하게 되면 크로스 조인이 묵시적으로 발생하게 됩니다. 이는 Hibernate 이슈이다. Spring Data Jpa를 사용해도 동일하게 발생합니다.

이를 피하기 위해서는 명시적 조인을 사용할 수 있습니다. 명시적 조인을 사용하게되면 Inner Join이 발생하게 될 것 입니다.

Entity 보다는 Dto를 우선

Entity를 조회할 때의 단점

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

조회 컬럼 최소화하기


이미 알고 있는 값들에 대해서는 select절로 조회할 필요 없이, as 표현식으로 대체할 수 있다.

as 컬럼은 select에서 제외됩니다.

Select 컬럼에 Entity 자제

예를 들어, AsBond라는 클래스를 만들기 위해 customer의 id만 필요함에도 불구하고, adItem.customer를 select 절에 포함하게 된다면, 모든 컬럼을 조회하게 됩니다.

또한, customer에 OneToOne 관계가 있다면, 매 건마다 해당 엔티티가 조회됩니다.(N+1 문제 발생) 즉, 한방 쿼리를 작성했는데 실제로는 N+1, (N+1)*(N+1), … 만큼의 쿼리가 수행되게 됩니다. 연관된 Entity의 save를 위해서는 반대편 Entity의 ID만 있으면 됩니다.

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

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

Group By 최적화

인덱스를 타지 않게 되는 경우에는 FileSort가 매번 발생하게 됩니다. MySQL에서는 order by null을 사용하면 fileSort가 제거되지만, QueryDSL에서는 order by null 문법을 지원하지 않습니다.

Using filesort는 정렬이 필요한 데이터를 메모리에 올리고 정렬 작업을 수행한다는 의미로, 이미 정렬된 인덱스를 사용함으로써 해결할 수 있습니다. 강의에서는 모든 Group By가 index를 탄다는 보장이 없다고 말하고 있지만, 8.0 이상을 사용하는 환경이라면 더 이상 고려하지 않아도 됩니다.

  • 그럼 어떻게 해야할까?
    • Order By Null 직접 구현하자
      아래 예시는 OrderByNull을 가진 클래스를 만들어서 사용한다. 단, 페이징의 경우 order by null을 사용하지 못 합니다.

정렬이 필요하더라도, 조회 결과가 100건 이하 또는 Paging이 필요하지 않고 정렬 건수가 적다면 애플리케이션에서 정렬하는 것을 추천헙니다.

그 이유는, DB보다는 WAS의 자원이 좀 더 여유롭고 저렴하기 때문입니다.(DB는 한대를 두더라도 WAS는 여러대를 두는 등)

또한, WAS에서 가능하다면 WAS에서 정렬하는 것을 추천합니다.

커버링 인덱스

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

커버링 인덱스(기본 지식/where/group by)

from 절의 서브쿼리가 커버링 인덱스입니다.

JPQL은 from절의 서브쿼리를 지원하지 않는다. 따라서 직접 구현해야 합니다.

3. Update / Insert 문에서의 성능 개선

일괄 업데이트 최적화

트랜잭션 내부에 있을 때 엔티티를 조회해서, 해당 엔티티의 값을 바꾸면 디비에 자동으로 변경되는 방식입니다.

우측 방향 업데이트에 비해서 성능 상 이점이 매우 떨어지게 됩니다.

일괄 업데이트의 단점으로는 일괄 업데이트를 하게 되면 하이버네이트 캐시의 갱신이 안 됩니다. 이럴 경우에는 업데이트 대상들에 대한 Cache Eviction을 필요로 합니다.

  • Dirty Checking - 실시간 비즈니스 처리, 실시간 단건 처리
  • Querydsl.update - 대량의 데이터를 일괄로 Update 처리 시

JPA Bulk Insert는 자제하자

Jdbc Template를 사용하지 않고, Qclass 기반으로 Native SQL을 사용할 수 있습니다.

QClass 생성

EntityQL

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

EntityQL - Bulk Insert

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

EntityQL의 단점


참고 자료

profile
기록기록기록기록기록

0개의 댓글