Spring/Spring Data JPA

[자바 ORM 표준 JPA 프로그래밍] 10-3. 객체지향 쿼리 언어: QueryDSL

noahkim_ 2024. 8. 2. 11:11

김영한 님의 "자바 ORM 표준 JPA 프로그래밍" 책을 정리한 포스팅 입니다.

 

1. QueryDSL

  • Java에서 쿼리를 생성할 수 있게 해주는 오픈소스 프레임워크

 

특징

항목 설명
타입 안전성
자바 코드 기반
- 컴파일 시점에 타입 체크
유연성
동적 쿼리 생성
- 주어진 조건에 따라 동적으로 JPQL 쿼리를 생성하여 실행함
Fluent API
Method Chaining 방식
- 직관적이고 가독성이 높은 쿼리 작성
다양한 지원
다양한 데이터베이스 및 기술 지원 (SQL, JPA, MongoDB, Lucene 등)

 

예시) 타입 안전성

더보기
@Test
@Transactional
void type_safety() {
    QUser user = QUser.user;

    JPAQuery<User> query = new JPAQuery<>(entityManager);

    User response = query.select(user)
            .from(user)
            .where(
                user.name.eq("updated").and(user.id.lt(10)) // 필드 & 인자값의 타입 안전이 보장됨 
            ).fetchOne();

    assertEquals(response.getName(), "updated");
}

 

예시) 유연성

더보기
@Test
@Transactional
void flexibility_test() {
    QUser user = QUser.user;

    JPAQuery<User> query = new JPAQuery<>(entityManager);
    query.select(user).from(user);

    String searchName = "updated";
    Integer minId = 10;

    // AND 조건 누적을 위한 BooleanBuilder 사용
    BooleanBuilder builder = new BooleanBuilder();

    if (searchName != null) builder.and(user.name.eq(searchName));
    if (minId != null) builder.and(user.id.lt(minId));

    User response = query.where(builder).fetchOne();

    assertEquals(response.getName(), "updated");
}

 

예시) Fluent API

더보기
QUser user = QUser.user;

JPAQuery<User> query = new JPAQuery<>(entityManager);
List<User> users = query.select(user)
        .from(user)
        .where(user.id.lt(10))
        .orderBy(user.name.asc())
        .limit(10)
        .fetch();

 

vs Criteria API

항목 Criteria API QueryDSL
기본 스타일 JPA 표준 스펙
서드파티 라이브러리 (open-source)
코드 가독성 ❌ 복잡하고 장황
✅ 간결하고 직관적
타입 안전성 ✅ 지원
✅ 지원 (더 자연스러움)
IDE 자동완성 ❌ 한계 있음
✅ 깔끔한 자동완성
런타임 오류 가능성 ✅ 낮음 ✅ 낮음
동적 쿼리 ❌ 코드 복잡해짐
✅ BooleanBuilder 등으로 자연스럽게 작성 가능
JPQL 기반 쿼리 작성 ✅ 지원 (CriteriaQuery)
✅ 지원 (JPAQuery, QueryFactory)
복잡한 쿼리 작성 난이도 ❌ 높음
✅ 상대적으로 쉬움
러닝 커브 ⬆ 초반 진입장벽 낮음 (표준이라 문서 많음)
⬆ 초반에는 설정이 필요함 (Q타입 생성 등)
프레임워크 통합 ✅ JPA 표준이라 어디서나 사용 가능
🔶 별도 설정 필요 (Gradle/Maven에서 Q타입 생성 등)

 

2. 셋팅

dependencies {
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

def querydslDir = "src/main/generated"

sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean.doLast {
    file(querydslDir).deleteDir()
}

 

3. 컴포넌트

항목 Q Class
JPAQuery / JPAQueryFactory
역할 엔티티 기반 메타데이터 클래스 (필드 정보 포함)
타입 안전한 쿼리 생성 및 실행
생성 방식 컴파일 시 자동 생성 (querydsl-apt 플러그인)
직접 인스턴스 생성 (new JPAQuery<>, new JPAQueryFactory(...))
용도 쿼리에서 사용할 엔티티 필드 참조
select, from, where, fetch 등 쿼리 구성
타입 안전성 필드에 대한 타입 안전성 제공
쿼리 전반에 걸쳐 타입 안전성 제공
특징 - 엔티티 필드 정보를 메타데이터로 표현
- IDE 자동완성 지원
- Fluent API
- 동적 조건 처리 용이
- 메서드 체이닝으로 직관적인 쿼리 구성

 

 

예제

더보기
public class UserRepositoryCustomImpl implements UserRepositoryCustom {

    @PersistenceContext
    private EntityManager em;

    @Override
    public List<User> findByCustomPredicate(String firstname) {
        JPAQuery query = new JPAQuery(em);

        // Q Class
        QMember member = QMember.member; 

        // JPA Query
        List<Member> members = query.from(member)
                            .where(member.username.eq(firstname))
                            .list(member);
    }
}

 

4. 기능

주제 설명
조회 fetch 조회 지원
조인 엔티티 간 관계를 SQL Join처럼 연결
서브쿼리 쿼리 안에 쿼리 작성
프로젝션 및 결과 반환 컬럼 지정 조회 및 DTO 매핑
수정/삭제 쿼리 업데이트/삭제 배치 처리
동적 쿼리 조건에 따라 동적으로 where 절 생성
메서드 위임 동적 조건을 메서드화 하여 재사용

 

예제) 조회

더보기
메서드 결과가 1건일 때 결과가 여러 건일 때 결과가 없을 때 반환 형태
fetchOne() ✅ 정상 반환 ❌ NonUniqueResultException ❌ null 단일 객체
fetch() ✅ 리스트 반환 ✅ 전체 리스트 반환 ✅ 빈 리스트 반환 List<T>

 

fetchOne()

@Test
@Transactional
void unique_list_success() {
    QUser user = QUser.user;

    JPAQuery<User> query = new JPAQuery<>(entityManager);

    User response = query.select(user)
            .from(user)
            .where(
                user.name.eq("updated").and(user.id.lt(10)) // 인자값의 타입 안전이 보장됨
            ).fetchOne();

    assertEquals(response.getName(), "updated");
}
@Test
@Transactional
void unique_list_failure_nonuniqueresultexception() {
    QUser user = QUser.user;

    JPAQuery<User> query = new JPAQuery<>(entityManager);

    assertThrows(NonUniqueResultException.class, () -> query.select(user)
            .from(user)
            .where(user.id.lt(10)) // 인자값의 타입 안전이 보장됨
            .fetchOne());
}
@Test
@Transactional
void unique_list_failure_no_count() {
    QUser user = QUser.user;

    JPAQuery<User> query = new JPAQuery<>(entityManager);
    User resp = query.select(user)
            .from(user)
            .where(user.id.gt(10000)) // 인자값의 타입 안전이 보장됨
            .fetchOne();

    assertNull(resp);
}

 

fetch()

@Test
@Transactional
void fetch_success() {
    QUser user = QUser.user;

    JPAQuery<User> query = new JPAQuery<>(entityManager);

    List<User> resp = query.select(user)
            .from(user)
            .where(
                    user.name.eq("updated").and(user.id.lt(10)) // 인자값의 타입 안전이 보장됨
            ).fetch();

    assertTrue(!resp.isEmpty());
}
@Test
@Transactional
void fetch_fail_empty() {
    QUser user = QUser.user;

    JPAQuery<User> query = new JPAQuery<>(entityManager);

    List<User> resp = query.select(user)
            .from(user)
            .where(
                user.id.gt(100000) // 인자값의 타입 안전이 보장됨
            ).fetch();

    assertTrue(resp.isEmpty());
}

 

예제) where

더보기
JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;

List<Item> list = query.from(item)
                       .where(item.name.eq("좋은상품").and(item.price.gt(20000)))
                       .list(item);  //조회할 프로젝션 지정

 

예제) limit

더보기
QueryModifiers queryModifiers = new QueryModifiers(20L, 10L);  //limit, offset;

List<Item> list = query.from(item)
                       .restrict(queryModifiers)
                       .list(item);

 

예제) order by

더보기
QItem item = QItem.item;

List<Item> list = query.from(item)
                       .where(item.price.gt(20000))
                       .orderBy(item.price.desc(), item.stockQuantity.asc())
                       .offset(10).limit(20)
                       .list(item);

 

예제) group by

더보기
List<Item> list = query.from(item)
                       .groupBy(item.price)
                       .having(item.price.gt(10000))
                       .list(item);

 

예제) 조인

더보기
@Test
@Transactional
void join() {
    List<User> result = query
            .from(user)
            .join(user.team, team)
            .where(team.name.eq("gsw"))
            .fetch();

    for (User row : result) {
        System.out.println(row);
    }
}

 

예제) 서브쿼리

더보기
@Test
@Transactional
void subquery_max() {
    QUser subUser = new QUser("subUser");

    List<User> result = query
            .from(user)
            .where(user.age.eq(new JPAQuery<>()
                    .select(subUser.age.max())
                    .from(subUser))
            )
            .fetch();

    for (User row : result) {
        System.out.println(row);
    }
}
@Test
@Transactional
void subquery_in() {
    QTeam subTeam = new QTeam("subTeam");

    List<User> result = query
            .from(user)
            .where(user.team.in(new JPAQuery<>()
                    .select(subTeam)
                    .from(subTeam)
                    .where(subTeam.name.eq("gsw")))
            ).fetch();

    for (User row : result) {
        System.out.println(row);
    }
}

 

예제) 프로젝션

더보기
@Test
@Transactional
void projection_single_column() {
    List<String> result = userQuery
            .select(user.name)
            .from(user)
            .fetch();

    for (String row : result) {
        System.out.println(row);
    }
}
@Test
@Transactional
void projection_multi_column() {
    List<Tuple> result = tupleQuery
            .select(user.name, user.age)
            .from(user)
            .fetch();

    for (Tuple row : result) {
        System.out.println("[" + row.get(user.age) + "] " + row.get(user.name));
    }
}
@Test
@Transactional
void projection_dto() {
    List<UserDto> result = userDtoQuery
            .select(Projections.constructor(UserDto.class, user.id, user.name))
            .from(user)
            .fetch();

    for (UserDto row : result) {
        System.out.println("[" + row.getId() + "] " + row.getName());
    }
}

 

예제) 수정/삭제

더보기

수정

@Test
@Transactional
void update() {
    JPAUpdateClause update = new JPAUpdateClause(entityManager, user);
    update.set(user.age, user.age.add(1))
            .where(user.age.loe(37))
            .execute();
}

 

삭제

@Test
@Transactional
void delete() {
    JPADeleteClause delete = new JPADeleteClause(entityManager, user);
    long execute = delete.where(user.age.eq(31)).execute();            

    assertTrue(execute > 0);
}

 

 

예제) 메서드 위임

더보기
@Test
@Transactional
void method_delegation() {
    List<User> result = userQuery
            .from(user)
            .where(nameEq("Stephen Curry"), ageGoe(27))
            .fetch();

    for (User row : result) {
        System.out.println("[" + row.getId() + "] " + row.getName());
    }
}

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

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