Server/Spring JPA

Querydsl - 동적 쿼리(Dynamic SQL) 사용하기 !

JaeHoney 2022. 5. 7. 20:13

동적 쿼리란 ?

동적 쿼리란 상황에 따라 다른 문법의 SQL을 적용하는 것을 말한다.

 

예를 들면 DB에서 값을 조회할 때 조회 조건이 동적으로 바뀌어야 하는 경우가 많다. 이런 상황을 Querydsl을 사용하면 손쉽게 해결할 수 있다.

  • name 값이 들어오면 WHERE name = ${name}
  • age 값이 들어오면 WHERE age = ${age}
  • name과 age가 모두 들어오면 WHERE name = ${name} AND age=${age}
  • name과 age 모두 들어오지 않으면 WHERE 절을 사용하지 않는다.

 

이를 해결하기 위한 방법을 살펴보자.

1. BooleanBuilder

동적 쿼리를 해결하려고 BooleanBuilder를 사용하는 걸 자주 볼 수 있다.

private List<Member> searchMember(String nameCond, Integer ageCond) {
    BooleanBuilder builder = new BooleanBuilder();
    
    if(nameCond != null) {
        builder.and(member.name.eq(nameCond));
    }
    if(ageCond != null) {
        builder.and(member.age.eq(ageCond));
    
    return queryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();
}

iBatis나 myBatis에서 사용하던 것과 유사한 방법이다.

BooleanBuilder를 추가해서 파라미터의 상태에 따라 다른 where절을 builder에 삽입한다.

 

이 방법의 문제점은 where절을 통째로 보기가 어렵다. 로직을 따라가면서 신경을 기울여야 쿼리문을 이해할 수 있다. 조건이 훨씬 까다로워지면 결과를 추측하기도 힘들어지는 쿼리가 될 수 있다.

2. BooleanExpression

다음 방법은 BooleanExpression을 사용하는 방법이다.

Querydsl은 아래 2가지 기능을 제공한다.

  • where()에 null이 들어오면 무시한다.
  • where()에 , 을 and 조건으로 사용한다.

두 가지 기능을 사용하면 아래와 같이 코드를 작성하는 것이 가능하다.

private List<Member> searchMember(String nameCond, Integer ageCond) {
    return queryFactory
            .selectFrom(member)
            .where(nameEq(nameCond), ageEq(ageCond))
            .fetch();
}

private BooleanExpression nameEq(String nameCond) {
    if (nameCond == null) {
        return null;
    }
    return member.name.eq(nameCond);
}

private BooleanExpression ageEq(Integer ageCond) {
    if (ageCond == null) {
        return null;
    }
    return member.age.eq(ageCond);
}

결과적으로 유지보수를 할 때 searchMember()를 보게 되므로 queryFactory의 where() 메서드의 인자로 사용되는 메서드명을 보고 어렵지 않게 쿼리를 파악할 수 있다.

 

추가로 필자의 경우에는 삼항 연산자를 사용하는 것을 선호한다.

private BooleanExpression nameEq(String nameCond) {
    return nameCond != null? member.name.eq(nameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
    return ageCond != null? member.age.eq(ageCond) : null;
}

BooleanExpression의 큰 장점 중 하나가 BooleanExpression 객체들을 조립할 수 있다. 복잡한 서비스의 경우 검색 조건이 여러가지 상태를 의존하는 경우가 많다. 해당 기능을 사용하면 Composition(조립, 구성)으로 깔끔하게 쿼리를 작성할 수 있다.

private BooleanExpression allEq(String usernameCond, Integer ageCond, String op) {
    if(op.equals("and")) {
        usernameEq(usernameCond).and(ageEq(ageCond));
    }
    if(op.equals("or")) {
        return usernameEq(usernameCond).or(ageEq(ageCond));
    }
    return  null;
}

이렇게 작성한 메서드는 다른 Repository 메서드에서 재사용도 할 수 있다.

그래서 QueryDSL의 BooleanExpression을 이용해서 동적 쿼리를 더 보기좋고 유지보수도 편리하게 해결할 수 있다.

 

추가. StringUtils

우리는 RequestDto를 받아서 해당 내용을 기반으로 쿼리를 동적으로 작성할 때가 있다. 가령 GET으로 파라미터를 받아서 Where 조건으로 사용한다고 생각해보자.

 

파라미터에 name이 존재하면 name을 조건으로 사용하지만 name이 존재하지 않으면 조건으로 사용하지 않는다. 그런데 null 체크로는 완벽하지 않다. 웹에서 파라미터가 넘어올 때 null이 아니라 ""로 넘어오는 경우가 많다.

 

이 때는 StringUtils.hasText()를 사용하면 된다.

public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {

    BooleanBuilder builder = new BooleanBuilder();
    if (StringUtils.hasText(condition.getUsername())) {
        builder.and(member.username.eq(condition.getTeamName()));
    }
    if (StringUtils.hasText(condition.getTeamName())) {
        builder.and(team.name.eq(condition.getUsername()));
    }

    return queryFactory
            .select(new QMemberTeamDto(
                    member.id.as("memberId"),
                    member.username,
                    team.id.as("teamId"),
                    team.name.as("teamName")))
            .from(member)
            .leftJoin(member.team, team)
            .where(builder)
            .fetch();
}

 


Reference