스프링데이터 + JPA/QueryDSL

29. 동적쿼리 성능 최적화 조회 Where절 사용.

sdafdq 2023. 12. 3. 10:47

과거 우리가 where절은 ,로 구분하여, 여러 표현식을 인자로 주면 그게 and로 묶인다고 했다.

https://qwefdg3.tistory.com/937

 

그래서, 표현식을 만들어 where절 안에 인자로 주면, 그게 알아서 여러 개면 and로 묶여 쿼리를 만들어 준다.

 

그렇기에,

 

private BooleanExpression usernameEq(String username){
    return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName){
    return StringUtils.hasText(teamName) ? team.name.eq(teamName) : null;
}

private BooleanExpression ageGoe(Integer ageGoe){
    return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe){
    return ageLoe != null ? member.age.loe(ageLoe) : null;
}

이렇게 조건에 따라 표현식을 반환하는 메소드를 만들어 두고,

 

public List<MemberTeamDto> search(MemberSearchCondition condition){
    return query.select(new QMemberTeamDto(
                    member.id, member.username, member.age, team.id, team.name
            )).from(member).leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .fetch();
}

호출해서 쓴다.

 

그러면 알아서 저 표현식(조건식인 표현식)들은 where 안에서 and로 묶인다.

 

@Test
public void search(){
    MemberSearchCondition condition = new MemberSearchCondition();
    condition.setAgeGoe(20);
    condition.setAgeLoe(40);
    condition.setTeamName("teamB");

    List<MemberTeamDto> result = memberJpaRepository.search(condition);
}

테스트 해 보면, 

 

/* select
    member1.id,
    member1.username,
    member1.age,
    team.id,
    team.name 
from
    Member member1   
left join
    member1.team as team 
where
    team.name = ?1 
    and member1.age >= ?2 
    and member1.age <= ?3 */ select
        m1_0.member_id,
        m1_0.username,
        m1_0.age,
        m1_0.team_id,
        t1_0.name 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        t1_0.name=? 
        and m1_0.age>=? 
        and m1_0.age<=?

의도했던 대로 쿼리가 잘 나간다.

 

그런데, 아무래도 

public List<MemberTeamDto> search(MemberSearchCondition condition){
    return query.select(new QMemberTeamDto(
                    member.id, member.username, member.age, team.id, team.name
            )).from(member).leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe())
            )
            .fetch();
}

이렇게 where절에 더덕더덕 붙여놓는게 좋아보이지 않는다. 저걸 따로 빼고 싶다.

 

그런데, 이러면은 기존에

private BooleanExpression usernameEq(String username){
    return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName){
    return StringUtils.hasText(teamName) ? team.name.eq(teamName) : null;
}

private BooleanExpression ageGoe(Integer ageGoe){
    return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe){
    return ageLoe != null ? member.age.loe(ageLoe) : null;
}

이렇게 되는데, 저것들은 한번에 모아서

private BooleanExpression allEq(MemberSearchCondition condition){
    return usernameEq(condition.getUsername())
            .and(teamNameEq(condition.getTeamName()))
            .and(ageGoe(condition.getAgeGoe()))
            .and(ageLoe(condition.getAgeLoe()));
}

이걸 만들으려 해도, usernameEq에서 null이 반환되면 null.and가 불가능 하다.

 

그래서 해결할 수 있는 방향 하나가,

private BooleanExpression usernameEq(String username){
    return StringUtils.hasText(username) ? member.username.eq(username) : Expressions.TRUE;
}
private BooleanExpression teamNameEq(String teamName){
    return StringUtils.hasText(teamName) ? team.name.eq(teamName) : Expressions.TRUE;
}

private BooleanExpression ageGoe(Integer ageGoe){
    return ageGoe != null ? member.age.goe(ageGoe) : Expressions.TRUE;
}
private BooleanExpression ageLoe(Integer ageLoe){
    return ageLoe != null ? member.age.loe(ageLoe) : Expressions.TRUE;
}

이렇게 null 대신 true 표현식을 반환하는 거다.

빈 표현식 객체를 반환하려고 했는데, 없는 것 같다.

 

여튼 저렇게 해서 

@Test
public void search(){
    MemberSearchCondition condition = new MemberSearchCondition();
    condition.setAgeGoe(20);
    condition.setAgeLoe(40);
    condition.setTeamName("teamB");

    List<MemberTeamDto> result = memberJpaRepository.searchEq(condition);
}

돌려 보면,

 

/* select
    member1.id,
    member1.username,
    member1.age,
    team.id,
    team.name 
from
    Member member1   
left join
    member1.team as team 
where
    true 
    and team.name = ?1 
    and member1.age >= ?2 
    and member1.age <= ?3 */ select
        m1_0.member_id,
        m1_0.username,
        m1_0.age,
        m1_0.team_id,
        t1_0.name 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        true 
        and t1_0.name=? 
        and m1_0.age>=? 
        and m1_0.age<=?

쿼리는 잘 나간다.

그런데 문제가 저 true.

물론 기능상으로는 아무 문제가 없다.

심지어 저렇게 쿼리가 있어도,

 

예를 들어

@Test
public void search(){
    MemberSearchCondition condition = new MemberSearchCondition();
    condition.setTeamName("teamB");

    List<MemberTeamDto> result = memberJpaRepository.searchEq(condition);
}
/* select
    member1.id,
    member1.username,
    member1.age,
    team.id,
    team.name 
from
    Member member1   
left join
    member1.team as team 
where
    true 
    and team.name = ?1 
    and true 
    and true */ select
        m1_0.member_id,
        m1_0.username,
        m1_0.age,
        m1_0.team_id,
        t1_0.name 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        true 
        and t1_0.name=? 
        and true 
        and true

이렇게 되어 있어도 

저 and true 저러 한 부분들은 다 알아서 DB내에서 최적화가 된다. 또 만약 or조건으로 하고 싶어도 

 

즉, 기능상으로는 문제가 없고, 최적화 상으로도 문제가 없다.

 

그런데, 아무래도 저렇게 의미없는 쿼리가 나간다는게 유지보수상에서 조금 그렇긴 하다.

 

내 생각에 이거, 괜찮아 보인다.

만약 and true 이런 거 보고 아, 이거 동적쿼리 조건이구나? 하고 생각할 수 있는 사람끼리 보면 괜찮을 것 같다.

또 위에 조건 표현식을 삼항연산자로 바로 반환했듯이, 저런 부분은 코드 가독성 부분에서 상당히 괜찮다. (단, 용도는 써줘야 할 듯 싶다.)

 

하지만, 저렇게 의미없는 쿼리가 뭔지 한번 찾아봐야 한다는 점이 조금 그렇다.

 

 

 

+++++

https://www.inflearn.com/questions/1091244/alleq-만들기

 

allEq() 만들기 - 인프런 | 질문 & 답변

[질문 템플릿]1. 강의 내용과 관련된 질문인가요? (예)2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)[질문 내용]allEq를 만들기 위

www.inflearn.com

선생님이 좋은 방식이 아니라고 한다. 아무래도 의미없는 쿼리가 나가는 것의 모호성을 더 하자로 여기신 것 같다.

 

https://www.inflearn.com/questions/94056/강사님-where-다중-파라미터를-이용한-동적-쿼리-사용에-대한-질문입니다

 

강사님 where 다중 파라미터를 이용한 동적 쿼리 사용에 대한 질문입니다. - 인프런 | 질문 & 답변

강사님 강의 잘 보고 있습니다.다름아니라, where 다중 파라미터를 사용하면 가독성이 높아지는 건 이해했습니다.영상 8분경의 메소드 private BooleanExpression allEq(String userNameCond, Integer ageCond) { r...

www.inflearn.com

그냥 BooleanBuilder로 반환하면 된다!

private BooleanBuilder ageEq(Integer age) {
    return nullSafeBuilder(() -> member.age.eq(age));
}

private BooleanBuilder roleEq(String roleName) {
    return nullSafeBuilder(() -> member.roleName.eq(roleName));
}

public static BooleanBuilder nullSafeBuilder(Supplier<BooleanExpression> f) {
    try {
        return new BooleanBuilder(f.get());
    } catch (IllegalArgumentException e) {
        return new BooleanBuilder();
    }
}

잘 모르겠지만, 공급기는 보통 값을 공급하는 역할을 하는 콜백으로 넘기는 데, 

여기서 IllegalArgumentException 즉, 잘못된 인자 예외가 터지는 이유는,

eq()에 null이 들어가기 때문에 그렇다. 그렇다면 빈 builder를 줘버린다.

애초에 where가 builder 처리도 가능하다.

 

 

+++ 

근데 이거, eq는 되는데 like 같은 경우는 안된다.

eq만 되는 이유는,

public BooleanExpression eq(T right) {
    if (right == null) {
        throw new IllegalArgumentException("eq(null) is not allowed. Use isNull() instead");
    } else {
        return eq(ConstantImpl.create(right));
    }
}

eq는 이렇게 내부적으로 알아서 null 처리를 해준다.

그래서 저 IllegalArgumentException을 받아서 처리 한 거고.

 

근데, like나 contain같은 경우는..

 

public BooleanExpression contains(String str) {
    return contains(ConstantImpl.create(str));
}

그냥 호출해 버린다. 

그래서 따로 null 처리를 해야 한다.