query dsl을 이 pr 링크에서 처음 시도해보았으나 기존에 내가 계속 리펙토링 하던 로직보다 성능이 나아지지 못하여서 보다 활용 원리를 더 공부하고 다른 플젝에 이슈를 생성하여 적용하고자 해당 게시글을 작성하게 되었다. 어렵지만 공부하고 적용해서 계속 추가할 예정이다.
제목 그대로 성능 개선을 위해 Querydsl을 어떻게 활용해야 할까?
extends / implements 사용하지 않기
일반적으로 repository를 만들면 spring data jpa 기능을 이용하기 위해 jpa repository를 extends로 상속 받고, query dsl에서도 custom repository를 만든 뒤 상속 받으며 이를 구현한 구현 객체가 필요하다. 혹은 query dsl의 경우 QuerydslRepositorySupport 클래스를 통해 이를 사용자 정의 클래스에서 상속 받도록 하는 방법도 있다.
하지만 이렇게 매번 상속 받는 것이 불편할 수도 있기 때문에 JpaQueryFactory를 빈으로 등록하여 이를 통해 주입을 시킨다면 더 편리하게 동적 쿼리를 만들 수 있다.
@Repository
@RequiredArgsConstructor
public class MemberRepositoryCustom {
private final JpaQueryFactory queryFactory; // 물론 이를 위해서는 빈으로 등록을 해줘야 한다
}
동적 쿼리는 BooleanExpression 사용하기
동적 쿼리를 작성하는 방법은 아래와 같다.
1. BooleanBuilder를 이용하는 방법(어떤 쿼리가 나갈지 예측하기 어렵다는 단점이 존재)
2. Where 절과 파라미터로 Predicate를 이용하는 방법
3. Where절과 파라미터로 Predicate를 상속한 BooleanExpression을 사용하는 방법
3번 사용을 보편적으로 권장하는 이유는 BooleanExpression은 and와 or과 같은 메소드들을 이용해서 BooleanExpression을 조합해 새로운 BooleanExpression을 만들 수 있기 때문에 재사용성이 높다는 이점이 있어서이다. 또한 null 반환 시 where 절에서 조건이 무시되어 안전성이 높아진다.
exist 메소드 사용하지 않기
Querydsl에서 exist는 count 쿼리를 사용하기 때문에 사용을 지양!하는 것이 좋다. SQL exist 쿼리의 경우 먼저 조건에 맞는 값을 찾으면 바로 반환하지만 count 쿼리는 전체 튜플을 모두 조회하여 성능이 더 떨어지기 때문에 Querydsl에서는 exist보다는 이를 우회하는 fetchFirst()를 사용하면 좋다. fetchFirst는 limit(1)이 내부적으로 실행되기 때문에 결과를 하나만 가져오므로 SQL exist 문과 크게 차이점이 없다.
public Boolean exist(Long memberId) {
Integer fetchOne = queryFactory
.selectOne()
.from(member)
.where(member.id.eq(memberId))
.fetchFirst();
return fetchOne != null;
}
cross join을 우회하기
cross join을 하게 되는 상황은 아래와 같다.
묵시적 조인이라는 조인을 명시하지 않고 entity에서 다른 entity를 조회해서 비교하는 경우 jpa가 알아서 cross join을 하게 된다. 이 상황에서는 일반 join들보다 select 되는 것이 많아져 excessive problem이 나타날 수 있어 성능이 저하될 수 있다. 따라서 cross join을 회피하기 위해 hibernate에서 sql이 cross join이 확인된다면 명시적 조인(inner join 등)을 이용해서 해결하면 된다.
조회할 때는 entity보다 dto를 우선적으로 가져오기
entity를 먼저 가져올 때 나타날 수 있는 문제점은 OneToOne N+1 문제 등 성능 이슈가 될 만한 부분들이 많이 존재한다.
OneToOne N+1 문제는 외래 키를 가지고 있는 주인 테이블에서는 지연로딩이 제대로 동작하지만 mappedBy 로 연결된 반대편 테이블에서는 지연로딩이 동작하지 않고 N+1 쿼리가 터지는 문제다.
어떨 때 entity를 먼저, 어떨 때 dto를 먼저 가져올지에 대해서는 아래와 같은 기준을 적용해보려고 한다.
실시간으로 entity 변경이 필요한 경우엔 entity 조회를 하자.
성능 개선이나 대량의 데이터 조회가 필요한 경우엔 dto 조회를 하자.
select 칼럼에 entity는 지양하기
select 절 안에 entity를 넣으면 entity 모든 칼럼이 조회가 되기 때문에 필요한 칼럼만 가지고 오자.
group by 최적화하기
MySQL에서는 index가 없다면 group by column에 의해 filesort라는 정렬 알고리즘이 실행되는데 이게 속도 감소의 원인이 되어 query dsl에서도 이를 피하고자 order by null을 사용하면 좋지만 지원하지 않는 문제가 있다. 그래서 OrderByNull 클래스를 만들어서 queryFactory에서 orderBy에 이를 사용해보자.
public class OrderByNull extends OrderSpecifier {
public static final OrderByNull DEFAULT = new OrderByNull();
private OrderByNull(){
super(Order.ASC, NullExpression.DEFAULT, NullHandling.Default);
}
}
그러나 페이징 쿼리일 경우 OrderByNull을 사용하지 못한다.
정렬 작업의 경우 100건 이하라면 애플리케이션 메모리로 가져와서 정렬하는 것이 db 자원에서 정렬하는 것보다 비용적으로 싸서 우수하다.
'SpringBoot > 성능 개선 흔적들' 카테고리의 다른 글
[Spring] 레디스 없이 caffeine cache로 조회 API 성능 개선 (0) | 2024.08.26 |
---|---|
[Spring] Redis @Cacheable를 이용한 조회 API 성능 개선 (0) | 2024.08.15 |
[Spring] 반복문 CompletableFuture과 스트림 parallelStream 성능 비교 (0) | 2024.07.26 |
[Spring] Querydsl fetchjoin을 통한 쿼리 수 감소 성능 향상 (0) | 2024.06.24 |
[Spring] N+1 문제를 해결하여 조회 쿼리 성능을 높여보자 (0) | 2024.05.04 |