QueryDSL(JPA(JPQL) -> QueryDSL)

김상진 ·2024년 12월 10일
0

CS

목록 보기
17/30

QueryDSL (JPA에서 QueryDSL로의 전환)


1. JPQL과 QueryDSL 비교

JPQL(Java Persistence Query Language)은 객체 지향 쿼리 언어로, JPA를 사용하여 데이터베이스와 상호작용할 때 사용하는 문자열 기반의 쿼리 언어입니다.
하지만 JPQL은 타입 세이프하지 않으며, 쿼리의 오류가 런타임 시점에만 발생합니다.

반면 QueryDSL타입 세이프하며 컴파일 시점에서 오류를 발견할 수 있고, 동적 쿼리를 안전하게 처리할 수 있는 Java 기반의 쿼리 빌더입니다.


2. JPQL 예시와 QueryDSL로의 전환

2.1. JPQL로 쿼리 작성

예를 들어, 회원(Member)팀(Team)이 연관된 구조에서, 특정 팀에 속한 회원들의 목록을 조회하는 JPQL을 작성한다고 가정해 보겠습니다. JPQL은 문자열로 작성되므로 쿼리 작성 시 오류를 잡기 어려운 단점이 있습니다.

public List<Member> findMembersByTeam(String teamName) {
    String jpql = "SELECT m FROM Member m JOIN m.team t WHERE t.name = :teamName";
    return em.createQuery(jpql, Member.class)
             .setParameter("teamName", teamName)
             .getResultList();
}

위 JPQL은 동적 쿼리 작성에 불편하고 쿼리의 타입 안전성이 떨어집니다. 잘못된 필드명이나 잘못된 타입을 사용할 경우 런타임에 오류가 발생하게 됩니다.

2.2. QueryDSL로 쿼리 작성

QueryDSL을 사용하면 동일한 쿼리를 타입 세이프하고 동적으로 작성할 수 있습니다.
JPQL에서의 문제를 QueryDSL로 해결할 수 있습니다. QueryDSL은 Q 클래스를 통해 타입 안전성 및 코드 완성 기능을 제공합니다.

public List<Member> findMembersByTeam(String teamName) {
    QMember member = QMember.member;
    QTeam team = QTeam.team;

    return queryFactory.selectFrom(member)
            .join(member.team, team) // Member 엔터티의 team 필드와 조인
            .where(team.name.eq(teamName)) // 팀 이름 조건
            .fetch(); // 결과 반환
}

위 코드에서 QMemberQTeamQueryDSL이 자동 생성하는 Q 클래스입니다. Q 클래스는 JPA 엔티티 클래스를 바탕으로 자동으로 생성되며, 쿼리에서 사용할 타입 세이프한 변수들을 제공합니다.


3. Q 클래스란 무엇인가?

Q 클래스는 QueryDSL에서 엔티티 클래스에 대한 타입 안전 쿼리를 작성하기 위해 자동으로 생성되는 클래스입니다.

  • Q 클래스는 빌드 시점에 생성되며, 각 엔티티 클래스의 필드QueryDSL이 생성하는 메타 모델로 변환합니다.
  • QMember, QTeam과 같은 클래스는 쿼리 조건에 사용될 수 있는 필드들을 제공합니다.

3.1. Q 클래스 생성 예시

엔티티 클래스 MemberTeam이 있을 때, QueryDSL은 이들에 대해 각각 QMember, QTeam 클래스를 생성합니다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    @ManyToOne
    private Team team;
}

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

위와 같은 엔티티가 있을 때, QueryDSL은 빌드 시점에 아래와 같은 Q 클래스를 생성합니다.

  • QMember : Member 엔티티의 필드 id, name, age, team에 대한 쿼리 조건을 제공합니다.
  • QTeam : Team 엔티티의 필드 id, name에 대한 쿼리 조건을 제공합니다.
public class QMember {
    public static final QMember member = new QMember("member");
    public final StringPath name = createString("name");
    public final NumberPath<Long> id = createNumber("id", Long.class);
    public final NumberPath<Integer> age = createNumber("age", Integer.class);
    public final QTeam team = new QTeam("team");

    // 생성자, 필드 등...
}

public class QTeam {
    public static final QTeam team = new QTeam("team");
    public final StringPath name = createString("name");
    public final NumberPath<Long> id = createNumber("id", Long.class);
    
    // 생성자, 필드 등...
}

4. 동적 쿼리 작성

QueryDSL의 가장 큰 장점 중 하나는 동적 쿼리 작성입니다. 여러 조건이 있을 때 쿼리를 안전하고 효율적으로 생성할 수 있습니다.
예를 들어, 나이, 이름, 팀 이름에 따라 회원을 조회하는 동적 쿼리를 작성해보겠습니다.

4.1. 동적 조건 추가

public List<Member> findMembers(String name, Integer age, String teamName) {
    QMember member = QMember.member;
    QTeam team = QTeam.team;

    BooleanBuilder builder = new BooleanBuilder();

    if (name != null) {
        builder.and(member.name.eq(name)); // 이름이 null이 아니면 조건 추가
    }
    if (age != null) {
        builder.and(member.age.eq(age)); // 나이가 null이 아니면 조건 추가
    }
    if (teamName != null) {
        builder.and(team.name.eq(teamName)); // 팀 이름이 null이 아니면 조건 추가
    }

    return queryFactory.selectFrom(member)
            .join(member.team, team)
            .where(builder) // 동적 조건 적용
            .fetch(); // 결과 반환
}

4.2. BooleanBuilder를 이용한 동적 쿼리 작성

BooleanBuilder는 QueryDSL에서 동적 쿼리를 작성할 때 조건을 누적하여 추가할 수 있도록 돕는 클래스입니다. 위 코드에서는 name, age, teamName을 조건에 따라 동적으로 추가할 수 있습니다.


5. 페이징 처리

QueryDSL에서는 offset()limit() 메서드를 통해 손쉽게 페이징 처리를 할 수 있습니다. 예를 들어, 10명씩 회원을 조회하려면 다음과 같이 작성할 수 있습니다.

public List<Member> findMembersWithPaging(int page, int size) {
    QMember member = QMember.member;

    return queryFactory.selectFrom(member)
            .orderBy(member.name.asc()) // 이름 기준 오름차순 정렬
            .offset(page * size) // 페이지 번호에 맞게 시작 위치 설정
            .limit(size) // 페이지 크기 설정
            .fetch(); // 결과 반환
}

페이징 처리 시에는 offset()limit()을 사용하여 시작 위치와 크기를 조정할 수 있습니다. 이 방식은 SQL에서의 LIMITOFFSET과 동일하게 작동합니다.


6. 집계 함수 사용

QueryDSL을 사용하면 집계 함수(예: count(), avg(), max(), min())도 손쉽게 사용할 수 있습니다.

public Double findAverageAge() {
    QMember member = QMember.member;

    return queryFactory.select(member.age.avg()) // 나이 평균 계산
            .from(member)
            .fetchOne(); // 결과 반환
}

위 예시에서 평균 나이를 구하는 쿼리를 QueryDSL로 작성한 모습입니다. avg()는 SQL의 AVG()와 동일하게 동작하며, 평균 값을 반환합니다.

7. 서브쿼리

QueryDSL은 서브쿼리를 사용하여 복잡한 조건을 표현할 수 있습니다. 서브쿼리는 subQuery() 메서드를 사용하여 생성하고, in(), exists(), notExists() 등의 연산자와 함께 사용합니다.

// 평균 나이보다 많은 회원 조회
QMember member = QMember.member;
NumberPath<Integer> avgAge = Expressions.numberPath(Integer.class, "avgAge");

JPAQuery<Member> query = queryFactory.selectFrom(member)
        .where(member.age.gt(
                JPAExpressions.select(avgAge)
                        .from(member)
                        .select(member.age.avg())
        ));

List<Member> results = query.fetch();

8. 조건절 다양화

QueryDSL은 다양한 조건절을 지원합니다.

  • 등호, 부등호: eq, ne, gt, lt, goe, loe
  • LIKE: like, startsWith, endsWith, contains
  • NULL 체크: isNull, isNotNull
  • IN, NOT IN: in, notIn
  • OR 조건: or

결론

QueryDSL은 타입 세이프하고 동적인 쿼리를 작성할 수 있는 훌륭한 도구입니다.
JPA를 사용할 때 쿼리 작성 시 발생할 수 있는 오류를 방지하고, 보다 유연하고 안전한 쿼리를 만들 수 있습니다.
타입 안전성과 코드 완성 기능을 제공하여 쿼리 작성의 생산성을 크게 향상시킬 수 있습니다.


참고 자료

profile
알고리즘은 백준 허브를 통해 github에 꾸준히 올리고 있습니다.🙂

0개의 댓글

관련 채용 정보