QueryDsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크이다.
쿼리를 문자가 아니라 진짜 자바 코드로 작성할 수 있게 도와준다. 때문에 자바 코드로 작성하지만 SQL, JPQL과 문법이 거의 같기 때문에 쉽게 학습할 수 있고, 또 쉽게 복잡한 쿼리도 작성할 수 있다.
JPA를 사용한다고 가정해보자. 간단한 쿼리라면 인터페이스에 메서드 명세만 잘 정의해 주면 별다른 문제 없이 사용할 수 있을 것이다. 예를 들면 아래처럼 “제목에 특정 문자열이 포함된 기사를 조회”하는 메서드처럼 말이다.
Post findByTitleContains(String title);
조금 더 복잡한 쿼리가 필요한 경우는 어떨까? 이런 경우에는 JPA 자체 제공 메서드만으로 해결하기 어렵기 때문에 네이티브 쿼리(Native Query)를 고려해볼 수 있다. 다음은 레벨이 특정 기준 이상인 사용자가 작성한 게시물을 조회하는 메서드다.
@Query(value = "SELECT id, title, user_id FROM post WHERE user_id IN (SELECT id FROM user WHERE level > :level)", nativeQuery = true)
List<Post> findByLevel(String level);
위에서 정의한 네이티브 쿼리를 보면 가독성은 감안하더라도 문자열을 이어 붙여가며 직접 작성하기 때문에 오타가 발생하기 아주 좋다.
위 코드를 Querydsl로 변경하면 이렇다.
public List<Post> findByUserLevel(String level) {
QPost post = QPost.post;
QUser user = QUser.user;
return queryFactory.selectFrom(post)
.where(
post.userId.in(
JPAExpressions
.select(user.id)
.from(user)
.where(user.level.gt(level))
)
)
.fetch();
}
코드의 가독성이 좋아졌고, 메서드 타입에 맞지 않는 파라미터를 넘기는 경우 친절하게 컴파일 오류를 발생시켜 잠재적인 버그를 방지해준다.
즉, 실행 시점 이전에 잘못된 쿼리 파라미터 타입까지 확인할 수 있는 장점이 있다.
Querydsl은 동적 쿼리를 위해서 많이 사용된다. 그렇다면 동적쿼리란 무엇인가?
동적 쿼리란 상황에 따라 다른 문법의 SQL을 적용하는 것을 말한다.
예를 들면 DB에서 값을 조회할 때 조회 조건이 동적으로 바뀌어야 하는 경우가 많다. 이런 상황을 Querydsl을 사용하면 손쉽게 해결할 수 있다.
WHERE name = ${name} AND age=${age}
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();
}
BooleanBuilder를 추가해서 파라미터의 상태에 따라 다른 where절을 builder에 삽입한다.
BooleanBuilder의 문제점은 where절을 통째로 보기가 어렵다. 로직을 따라가면서 신경을 기울여야 쿼리문을 이해할 수 있다. 조건이 훨씬 까다로워지면 결과를 추측하기도 힘들어지는 쿼리가 될 수 있다.
Querydsl은 아래 2가지 기능을 제공한다.
두 가지 기능을 사용하면 아래와 같이 코드를 작성하는 것이 가능하다.
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() 메서드의 인자로 사용되는 메서드명을 보고 어렵지 않게 쿼리를 파악할 수 있다.
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을 이용해서 동적 쿼리를 더 보기좋고 유지보수도 편리하게 해결할 수 있다.
다음에는 프로젝트에 적용시켜보자!
Refference
https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/
https://jaehoney.tistory.com/185