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를 위한 공간, 시간 때문에 성능이 줄어든다.
- @OneToOne라면 N + 1 문제가 발생할 수 있음
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 최적화
- 무분별한 변경 감지를 사용하는 대신 일괄 업데이트를 사용하자
- 하지만, 하이버네이트 캐시는 일괄 업데이트 시 캐시 갱신이 안됨
- 이 경우, 업데이트 대상들에 대한 cache eviction이 필요함
- 이 경우, 업데이트 대상들에 대한 cache eviction이 필요함
- 변경 감지는 실시간 비즈니스 처리, 실시간 단건 처리에 사용
- 대량 데이터를 일괄 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가 별도로 필요함
'맛있지만 저작권 문제 > 분류하긴 애매한데 하여간' 카테고리의 다른 글
스프링 캠프 2023 - 실무에서 적용하는 테스트 코드 작성 방법과 노하우 (0) | 2023.07.24 |
---|---|
현장에 레거시 코드 리팩토링 문화 만들기 (0) | 2023.06.14 |
스프링캠프 2019 - 자바에서 null을 안전하게 다루는 방법 (0) | 2023.01.14 |
지속가능한 SW 개발을 위한 코드 리뷰 (0) | 2023.01.11 |
애플리케이션 아키텍처와 객체지향 (1) | 2022.12.10 |