Develop/spring-data

[Querydsl] 성능 개선 1편

에디개발자 2021. 1. 29. 16:00
반응형

이 글은 우아한 형제들 콘서트에서 이동욱님의 영상을 보고 정리를 위한 글입니다.

이 글에 작성된 예시는 모두 Github에 올려두었습니다.

나를 닮았다고 한다...

 

1. 동적 쿼리 사용 시 BooleanExpression을 사용하자!

Querydsl에서 동적쿼리 사용방법은 조건문에 null을 넣으면 조건문이 무시되는 방법을 사용하면 됩니다.

public Staff dnamicQuery(String name) {
    return queryFactory
            .selectFrom(staff)
            .where(name == null ? null : name)
            .fetchOne();
}

 

위처럼 코드를 작성한다고 가정했을 때 동적 조건이 많아진다면 쿼리는 매우 복잡해질 것입니다. 그래서 BooleanExpression을 리턴하는 메소드를 생성하여 사용한다면 가독성이 훨씬 높아질 것입니다.

public Staff dynamicQuery(String name) {
    return queryFactory
            .selectFrom(staff)
            .where(eqName(name))
            .fetchOne();
}

private BooleanExpression eqName(String name) {
    if (StringUtils.isEmpty(name)) {
        return null;
    }

    return staff.name.eq(name);
}

 

위 두개의 쿼리는 동일한 쿼리를 생성합니다.

// Null 일 경우
select
    staff0_.id as id1_0_,
    staff0_.age as age2_0_,
    staff0_.last_name as last_nam3_0_,
    staff0_.name as name4_0_,
    staff0_.store_id as store_id5_0_ 
from
    staff staff0_

// Null 이 아닐경우
select
    staff0_.id as id1_0_,
    staff0_.age as age2_0_,
    staff0_.last_name as last_nam3_0_,
    staff0_.name as name4_0_,
    staff0_.store_id as store_id5_0_ 
from
    staff staff0_ 
where
    staff0_.name='용태'       // 조건문 생성

 

2. querydsl exist 사용은 자제하자.

먼저 sql문에서 exist를 사용하는 간단한 예시를 살펴보겠습니다.

select exists ( 
    select *     // 1)
    from staff 
    where name = '용태' 
)
1) 쿼리의 값이 있다면 값은 1이고 없다면 0을 리턴하는 쿼리입니다.

위처럼 사용하면 1)의 값을 1개를 찾는 순간 쿼리는 종료가 됩니다. 

Querydsl에서는 안타깝게도 위와 동일한 쿼리는 작성할 수 없습니다. 이유를 살펴보겠습니다.

 

가장 큰 이유는 Querydsl의 from 절은 필수 사항입니다. 

그럼 from절 사용하고 Querydsl의 exists 메소드를 사용해보겠습니다.

public Boolean findExist(String name) {
    BooleanExpression exists = queryFactory
            .selectOne()
            .from(staff)
            .where(staff.name.eq(name))
            .fetchAll()
            .exists();

    return queryFactory
            .select(exists)
            .from(staff)
            .fetchOne();
}

 

exists 내부를 살펴보면 전체조회를 한 후 Count가 1이상이면 true, 아니면 false를 리턴하고 있습니다.

// QuerydslJpaPredicateExecutor.java

@Override
public boolean exists(Predicate predicate) {
    return createQuery(predicate).fetchCount() > 0;
}
exsist와 count는 데이터가 많아질수록 성능차이가 발생합니다.
이유는 count는 모든 데이터를 조회하고 exsist는 데이터 1개만 찾고 결과를 리턴하기 때문입니다.

 

Querydsl에서 exists와 동일한 성능을 낼 수 있도록 fetchOne을 사용합니다

Limit 1과 동일한 기능을 합니다. 
public Boolean findLimitOneInsteadOfExist(String name) {
    Integer fetchFirst = queryFactory
            .selectOne()
            .from(staff)
            .where(staff.name.eq(name))
            .fetchFirst();          // limit(1).fetchOne()

    return fetchFirst != null;      // 값이 없으면 0이 아니라 null 반환
}

여기서 주의할 점은 값이 없으면 null이 리턴됩니다. 그래서 메서드 리턴에 위와 같이 Null 여부를 리턴하면 됩니다.

 

3. 묵시적 조인을 사용하지 말자

묵시적 조인이란?
쿼리에서 join할 테이블을 join을 사용하지 않고 조건문에 join할 테이블의 컬럼을 적용하는 것을 말합니다.

묵시적 조인을 사용할 경우 자동으로 Cross Join이 발생하게 됩니다.

Cross Join이란?
두 집합에서 나올 수 있는 모든 경우의 수를 조인하는 경우입니다.
{a, b} {1, 2, 3} 일 경우 총 6가지로 나타납니다. ( 2 * 3 )

아래의 코드는 Cross Join발생 쿼리로 성능저하가 발생하는 코드입니다.

public Bag findCrossJoinByStaffName(String name) {
    return queryFactory			// join 없음
            .selectFrom(bag)
            .where(bag.staff.name.eq(name))
            .fetchOne();
}
select
    bag0_.id as id1_0_,
    bag0_.name as name2_0_ 
from
    bag bag0_ cross 		// cross join 발생
join
    staff staff1_ 
where
    bag0_.id=staff1_.bag_id 
    and staff1_.name='임용태'

 

개선방법으로는 명시적 조인을 사용하는 것입니다. 

명시적 조인이란?
조인은 쿼리에 명시하는 것을 말합니다.
// 개선된 코드
public Bag findInnerJoinByStaffName(String name) {
    return queryFactory
            .selectFrom(bag)
            .innerJoin(bag.staff, staff)
            .where(staff.name.eq(name))
            .fetchOne();
}
select
    bag0_.id as id1_0_,
    bag0_.name as name2_0_ 
from
    bag bag0_ 
inner join		// 명시적 조인
    staff staff1_ 
        on bag0_.id=staff1_.bag_id 
where
    staff1_.name='임용태'

 

 

 

 

 

 

반응형