본문 바로가기
Java/Spring Boot

JPQL과 QueryDSL

by curious week 2025. 3. 31.

 

1. QueryDSL이란?

  • JPA의 JPQL을 타입 안전하게 사용할 수 있게 도와주는 라이브러리
  • IDE 자동완성, 리팩토링 용이, 동적 쿼리에 매우 강력함
  • 실무에선 JPQL보다 거의 대부분 QueryDSL을 사용

2. 설치 방법 (Gradle 기준)

1) 의존성 추가

// build.gradle.kts (Kotlin DSL 예시)
dependencies {
    implementation("com.querydsl:querydsl-jpa")
    annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jpa")
    annotationProcessor("jakarta.annotation:jakarta.annotation-api")
    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
}

2) Q클래스 생성 (엔티티마다 자동 생성됨)

./gradlew clean build

실행하면 build/generated/ 하위에 다음과 같은 클래스가 생겨:

QUser.java
QPost.java
QUserGroup.java

3. 기본 구조

QUser user = QUser.user;

User result = queryFactory
    .selectFrom(user)
    .where(user.email.eq("test@example.com"))
    .fetchOne();
selectFrom(...) 기본 SELECT 쿼리
where(...) 조건문
fetchOne() 단일 결과
fetch() 리스트 반환
fetchFirst() limit 1

4. 실무용 예시: 페치 조인 + 조건

QUser user = QUser.user;
QPost post = QPost.post;

List<User> users = queryFactory
  .selectFrom(user)
  .leftJoin(user.posts, post).fetchJoin()
  .where(user.email.contains("@gmail.com"))
  .fetch();
  • fetchJoin() → N+1 방지
  • .contains(...) → LIKE '%gmail.com%' 역할

5. 동적 쿼리 (BooleanBuilder)

BooleanBuilder builder = new BooleanBuilder();

if (name != null) {
    builder.and(user.name.eq(name));
}
if (email != null) {
    builder.and(user.email.contains(email));
}

List<User> users = queryFactory
  .selectFrom(user)
  .where(builder)
  .fetch();

사용자 입력 필터 조건이 많은 경우 유용해!


6. 정렬, 페이징

queryFactory
  .selectFrom(user)
  .orderBy(user.createdAt.desc())
  .offset(0) // 페이지 번호 * size
  .limit(10)
  .fetch();

7. DTO로 직접 매핑

List<UserDto> users = queryFactory
  .select(Projections.constructor(UserDto.class, user.id, user.name))
  .from(user)
  .fetch();
  • constructor(...), fields(...), bean(...) 등 다양한 방식 제공
  • @QueryProjection도 있음 (자동 생성 필요)

정리

단건 조회 .fetchOne()
리스트 조회 .fetch()
페이징 .offset().limit()
조건 분기 BooleanBuilder
N+1 방지 .fetchJoin()
DTO 조회 .select(...).fetch()


QueryDSL의 DTO 매핑 방법인 Projections.bean, fields, constructor


공통점

  • 세 가지 모두 Entity가 아닌 DTO로 직접 조회하는 방식
  • select 절에서 필요한 필드만 선택해서 DTO로 매핑할 수 있어
  • 단순 Entity 반환보다 더 가볍고 빠름 (조회 전용 성능 최적화)

1️⃣ Projections.bean(...)

JavaBeans 표준 방식 → setter 기반

.select(Projections.bean(UserDto.class, user.id, user.name))
public class UserDto {
  private Long id;
  private String name;

  // 반드시 setter 있어야 함!
  public void setId(Long id) { this.id = id; }
  public void setName(String name) { this.name = name; }
}

🟢 장점

  • 가독성 좋고 필드 순서 상관없음
  • @QueryProjection 없이 사용 가능

🔴 단점

  • 리플렉션 사용 → 성능 조금 손해 있음
  • setter가 반드시 필요 (불변 객체 X)

2️⃣ Projections.fields(...)

필드에 직접 주입하는 방식

.select(Projections.fields(UserDto.class, user.id, user.name))
public class UserDto {
  public Long id;
  public String name;
}

🟢 장점

  • setter 없어도 됨 (public 필드만 있으면 됨)
  • 불필요한 생성자/메서드 없이 간단하게 테스트 용으로 좋음

🔴 단점

  • 접근제어 미흡 → 캡슐화 X
  • 실무에선 잘 안 씀 (테스트나 빠른 구현용)

3️⃣ Projections.constructor(...)

생성자 기반 매핑

.select(Projections.constructor(UserDto.class, user.id, user.name))
public class UserDto {
  private final Long id;
  private final String name;

  public UserDto(Long id, String name) {
    this.id = id;
    this.name = name;
  }
}

🟢 장점

  • 불변 객체 가능 (final 필드)
  • 생성자만 맞으면 setter 없어도 됨
  • 테스트에 강하고 리팩토링에도 안정적

🔴 단점

  • 순서가 반드시 일치해야 함
  • 컴파일 타임 안전성 없음 (실수로 순서 틀리면 런타임 오류 발생)

Tip: @QueryProjection 방식

@QueryProjection
public UserDto(Long id, String name) { ... }

.select(new QUserDto(user.id, user.name))

컴파일 타임에 체크됨 (타입 안전성 ↑)
Q파일 생성 필요 (build/generated 안에 생성됨)


 

실무에서는 다음 조합으로

  • 빠른 개발/프로토타입 → bean 또는 constructor
  • 불변 객체 + 타입 안정성 → @QueryProjection 강추!

@QueryProjection 설정 + 사용법


1. @QueryProjection이란?

  • QueryDSL에서 제공하는 어노테이션
  • DTO에 컴파일 타임 타입 안정성 부여
  • Q파일로 생성되기 때문에 오타, 순서 실수 없음
  • 타입이 안 맞으면 빌드 타임에 오류 발생 → 실무에 강력 추천 💯

2. 설정 (Gradle 기준)

1) build.gradle 설정

dependencies {
    implementation 'com.querydsl:querydsl-jpa'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}

Kotlin DSL이면 kapt(...) 대신 annotationProcessor(...) 사용


2) DTO에 @QueryProjection 추가

import com.querydsl.core.annotations.QueryProjection;

public class UserDto {
    private Long id;
    private String name;

    @QueryProjection
    public UserDto(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // Getter 생략 가능 (Lombok 써도 OK)
}

3) Q타입 생성

./gradlew clean build

실행하면 다음 경로에 생성됨:

build/generated/...
com.example.dto.QUserDto.java

이게 있어야 .select(new QUserDto(...)) 사용 가능함


3. 사용 예시

1) QueryDSL에서 사용

List<UserDto> result = queryFactory
    .select(new QUserDto(user.id, user.name))  // QUserDto 생성자 사용
    .from(user)
    .fetch();

오타 방지, 타입 안정성 보장
user.namee → 컴파일 에러 발생!


2) Repository 커스텀으로 분리도 가능

public interface UserQueryRepository {
    List<UserDto> findAllAsDto();
}
@RequiredArgsConstructor
public class UserQueryRepositoryImpl implements UserQueryRepository {

    private final JPAQueryFactory queryFactory;

    public List<UserDto> findAllAsDto() {
        QUser user = QUser.user;

        return queryFactory
                .select(new QUserDto(user.id, user.name))
                .from(user)
                .fetch();
    }
}

실무에서는 QueryRepository 따로 빼서 관리하면 깔끔해져요

 

추천 사용 케이스

  • 조회 성능이 중요한 API
  • 실무에서 DTO 필드가 많고, 타입 안정성이 중요한 경우
  • QueryDSL을 메인 조회 도구로 쓰는 프로젝트

 

1. QueryDSL + 동적 검색 조건 조합

상황 예시

GET /users?name=heesoo&email=gmail&createdAfter=2024-01-01
  • 조건이 있을 수도 있고, 없을 수도 있는 경우
  • 조건이 많아질수록 where() 안이 동적으로 바뀌어야 함

방법 1: BooleanBuilder

public List<UserDto> search(String name, String email) {
    QUser user = QUser.user;
    BooleanBuilder builder = new BooleanBuilder();

    if (name != null && !name.isBlank()) {
        builder.and(user.name.containsIgnoreCase(name));
    }
    if (email != null && !email.isBlank()) {
        builder.and(user.email.containsIgnoreCase(email));
    }

    return queryFactory
            .select(new QUserDto(user.id, user.name))
            .from(user)
            .where(builder)
            .fetch();
}

특징:

  • 간단하고 직관적
  • 조건이 많아지면 builder 코드가 길어질 수 있음

방법 2: where(...)에 null 무시 기능 활용

public List<UserDto> search(String name, String email) {
    QUser user = QUser.user;

    return queryFactory
            .select(new QUserDto(user.id, user.name))
            .from(user)
            .where(
                nameEq(name),
                emailContains(email)
            )
            .fetch();
}

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

private BooleanExpression emailContains(String email) {
    return (email == null || email.isBlank()) ? null : QUser.user.email.containsIgnoreCase(email);
}

특징:

  • 깔끔한 함수 분리
  • where(...) 안에 null이면 자동으로 무시됨
  • 실무에서 가장 많이 사용되는 방식

선택 기준

조건이 적고 단순함 BooleanBuilder
조건이 많거나 복잡함 null 무시 방식 (BooleanExpression 분리)

2. Repository 커스텀 구성 베스트 프랙티스

2-1. 구조

UserRepository             ← Spring Data JPA 기본
└─ UserQueryRepository     ← 인터페이스
   └─ UserQueryRepositoryImpl ← 구현체 (QueryDSL)

2-2. 코드 구성

UserQueryRepository.java

public interface UserQueryRepository {
    List<UserDto> search(String name, String email);
}

UserQueryRepositoryImpl.java

@RequiredArgsConstructor
public class UserQueryRepositoryImpl implements UserQueryRepository {

    private final JPAQueryFactory queryFactory;

    @Override
    public List<UserDto> search(String name, String email) {
        QUser user = QUser.user;

        return queryFactory
                .select(new QUserDto(user.id, user.name))
                .from(user)
                .where(
                    nameEq(name),
                    emailContains(email)
                )
                .fetch();
    }

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

    private BooleanExpression emailContains(String email) {
        return (email == null || email.isBlank()) ? null : user.email.containsIgnoreCase(email);
    }
}

UserRepository.java

public interface UserRepository extends JpaRepository<User, Long>, UserQueryRepository {
}

스프링은 자동으로 UserQueryRepositoryImpl을 찾아서 조립해 줌!


장점

관심사 분리 조회 쿼리는 QueryRepository로 분리
테스트 용이 쿼리 레이어만 단위 테스트 가능
확장성 ↑ 복잡한 조건, 동적 정렬 등 확장 가능

  • 이름 패턴 중요! UserQueryRepository → UserQueryRepositoryImpl로 끝나야 자동 연결됨
  • JPAQueryFactory는 보통 Bean으로 등록해서 주입받음
  • @QueryProjection DTO를 미리 만들면 생성자 타입 실수 방지

 

1. 기본적인 QueryDSL 페이징 처리

예시: 사용자 리스트를 페이지 단위로 조회

public Page<UserDto> searchUsers(String keyword, Pageable pageable) {
    QUser user = QUser.user;

    List<UserDto> content = queryFactory
            .select(new QUserDto(user.id, user.name))
            .from(user)
            .where(user.name.containsIgnoreCase(keyword))
            .offset(pageable.getOffset())     // (page - 1) * size
            .limit(pageable.getPageSize())    // size
            .fetch();

    long total = queryFactory
            .select(user.count())
            .from(user)
            .where(user.name.containsIgnoreCase(keyword))
            .fetchOne();

    return new PageImpl<>(content, pageable, total);
}

PageImpl<>(내용, pageable, 총 개수)로 반환


2. 성능 문제: Count 쿼리 최적화

  • 실무에서 조인 fetch 쿼리를 쓸 경우, count 쿼리까지 같은 조인을 하면 매우 느려질 수 있어!

잘못된 예 (성능 ↓)

queryFactory
  .selectFrom(user)
  .leftJoin(user.posts, post).fetchJoin() // ⛔ count에도 fetchJoin 들어가면 느려짐

3. 해결 방법: QuerydslRepositorySupport 또는 분리 쿼리 전략

방법 1: PageableExecutionUtils.getPage(...) 사용

return PageableExecutionUtils.getPage(
    content,
    pageable,
    () -> queryFactory
            .select(user.count())
            .from(user)
            .where(user.name.containsIgnoreCase(keyword))
            .fetchOne()
);

특징:

  • content 개수가 pageSize보다 작으면 count 쿼리 생략됨
  • 또는 마지막 페이지일 때도 생략 가능
  • Spring Data에서 제공하는 유틸 함수 (의존성 없이 사용 가능)

방법 2: count 쿼리를 따로 구성

public Page<UserDto> searchUsers(String keyword, Pageable pageable) {
    QUser user = QUser.user;

    List<UserDto> content = queryFactory
            .select(new QUserDto(user.id, user.name))
            .from(user)
            .where(user.name.containsIgnoreCase(keyword))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

    JPAQuery<Long> countQuery = queryFactory
            .select(user.count())
            .from(user)
            .where(user.name.containsIgnoreCase(keyword));

    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}

정리

PageImpl 직접 작성 간단하고 명확 소규모 데이터에 적합
PageableExecutionUtils.getPage() count 생략 최적화 자동 처리 실무에서 가장 많이 사용
count 쿼리 따로 구성 조인 fetch 분리 가능 복잡한 페이징에 유리

보너스: QuerydslPredicateExecutor는 어떨 때 쓰나요?

public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> { }
  • 장점: findAll(Predicate) 형태로 바로 사용 가능
  • 단점: 복잡한 조인, DTO 매핑 불가 → 실무에선 조회 쿼리는 직접 작성 권장

 

1. 검색 조건을 Request DTO로 받기

예: 사용자 검색 조건

@Data
public class UserSearchCondition {
    private String name;
    private String email;
    private LocalDateTime createdAfter;
    private LocalDateTime createdBefore;
    private String sortBy; // "name", "createdAt" 등
    private String direction; // "asc", "desc"
}

클라이언트에서 query string으로 받거나, @RequestBody로 받을 수 있음


2. QueryDSL로 필터링 + 동적 정렬 처리

public Page<UserDto> search(UserSearchCondition cond, Pageable pageable) {
    QUser user = QUser.user;

    List<UserDto> content = queryFactory
        .select(new QUserDto(user.id, user.name))
        .from(user)
        .where(
            nameContains(cond.getName()),
            emailContains(cond.getEmail()),
            createdAfter(cond.getCreatedAfter()),
            createdBefore(cond.getCreatedBefore())
        )
        .orderBy(getOrderSpecifier(cond)) // 동적 정렬!
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();

    return PageableExecutionUtils.getPage(content, pageable, () ->
        queryFactory
            .select(user.count())
            .from(user)
            .where(
                nameContains(cond.getName()),
                emailContains(cond.getEmail()),
                createdAfter(cond.getCreatedAfter()),
                createdBefore(cond.getCreatedBefore())
            )
            .fetchOne()
    );
}

3. 조건 분기 함수들

private BooleanExpression nameContains(String name) {
    return (name == null || name.isBlank()) ? null : QUser.user.name.containsIgnoreCase(name);
}

private BooleanExpression emailContains(String email) {
    return (email == null || email.isBlank()) ? null : QUser.user.email.containsIgnoreCase(email);
}

private BooleanExpression createdAfter(LocalDateTime after) {
    return after == null ? null : QUser.user.createdAt.goe(after);
}

private BooleanExpression createdBefore(LocalDateTime before) {
    return before == null ? null : QUser.user.createdAt.loe(before);
}

4. 정렬 조건 분기 (OrderSpecifier)

private OrderSpecifier<?> getOrderSpecifier(UserSearchCondition cond) {
    String sortBy = cond.getSortBy();
    String dir = cond.getDirection();

    if (sortBy == null) return QUser.user.createdAt.desc(); // 기본 정렬

    PathBuilder<User> path = new PathBuilder<>(User.class, "user");

    if ("asc".equalsIgnoreCase(dir)) {
        return path.get(sortBy).asc();
    } else {
        return path.get(sortBy).desc();
    }
}

PathBuilder는 문자열로 필드를 지정해서 유연한 정렬을 가능하게 해줌
이 방식은 "createdAt", "name" 같은 정렬 필드명을 클라이언트에서 넘겨도 OK


응용 예

클라이언트에서는 다음과 같이 요청할 수 있어요:

GET /api/users?name=heesoo&sortBy=createdAt&direction=desc

 

1. 정렬 필드를 enum으로 정의하기

public enum UserSortField {
    NAME("name"),
    CREATED_AT("createdAt");

    private final String field;

    UserSortField(String field) {
        this.field = field;
    }

    public String getField() {
        return field;
    }
}
  • 프론트엔드는 "NAME", "CREATED_AT"처럼 enum 이름만 보내면 됨
  • 내부에서는 안전하게 실제 필드명 "name"이나 "createdAt"으로 변환 가능

2. 검색 조건 DTO에서 enum 사용

@Data
public class UserSearchCondition {
    private String name;
    private String email;
    private LocalDateTime createdAfter;
    private LocalDateTime createdBefore;

    private UserSortField sortBy = UserSortField.CREATED_AT; // 기본 정렬 필드
    private String direction = "desc"; // asc or desc
}
  • sortBy는 enum이기 때문에 자동완성 가능하고 잘못된 값 차단됨
  • 필요하다면 direction도 enum으로 관리 가능 (아래 참고)

3. OrderSpecifier를 enum 기반으로 처리

private OrderSpecifier<?> getOrderSpecifier(UserSearchCondition cond) {
    UserSortField sortField = cond.getSortBy();
    String dir = cond.getDirection();

    PathBuilder<User> path = new PathBuilder<>(User.class, "user");
    String sortProperty = sortField.getField();

    return "asc".equalsIgnoreCase(dir)
            ? path.get(sortProperty).asc()
            : path.get(sortProperty).desc();
}

장점:

  • 필드명을 enum으로 고정 → 실수로 "createdd" 같은 필드명 전달 X
  • 유지보수성과 안정성 ↑

추가로: direction도 enum으로 제한하고 싶다면?

public enum SortDirection {
    ASC, DESC;

    public boolean isAsc() {
        return this == ASC;
    }
}
private OrderSpecifier<?> getOrderSpecifier(UserSearchCondition cond) {
    UserSortField field = cond.getSortBy();
    SortDirection direction = cond.getDirection();

    PathBuilder<User> path = new PathBuilder<>(User.class, "user");
    return direction.isAsc()
            ? path.get(field.getField()).asc()
            : path.get(field.getField()).desc();
}

 

  • 잘못된 필드명, 정렬방향은 컴파일 타임에 방지됨
  • API 명세가 명확해지고, 프론트에서도 자동완성 사용 가능
  • 유지보수가 쉬워지고, 확장이 매우 쉬움

 

1. 기본 SELECT

JPQL

SELECT u FROM User u WHERE u.name = :name

QueryDSL

QUser user = QUser.user;

User result = queryFactory
    .selectFrom(user)
    .where(user.name.eq(name))
    .fetchOne();

2. 여러 조건 WHERE

JPQL

SELECT u FROM User u WHERE u.name = :name AND u.age >= :minAge

QueryDSL

queryFactory
    .selectFrom(user)
    .where(
        user.name.eq(name),
        user.age.goe(minAge) // .goe == greater or equal
    )
    .fetch();

.where() 안에 여러 조건은 ,로 나열하면 자동 AND 처리됨


3. LIKE 검색

SELECT u FROM User u WHERE u.name LIKE %:keyword%
queryFactory
    .selectFrom(user)
    .where(user.name.containsIgnoreCase(keyword))
    .fetch();

contains → %keyword%
startsWith → keyword%
endsWith → %keyword


4. JOIN

JPQL

SELECT u FROM User u JOIN u.team t WHERE t.name = :teamName

QueryDSL

QTeam team = QTeam.team;

queryFactory
    .selectFrom(user)
    .join(user.team, team)
    .where(team.name.eq(teamName))
    .fetch();

5. JOIN FETCH

JPQL

SELECT u FROM User u JOIN FETCH u.team

QueryDSL

queryFactory
    .selectFrom(user)
    .join(user.team, team).fetchJoin()
    .fetch();

fetchJoin() 붙이면 Lazy 로딩 없이 바로 연관 엔티티까지 한 번에 가져옴!


6. ORDER BY, GROUP BY

SELECT u.team.name, COUNT(u) 
FROM User u 
GROUP BY u.team.name 
ORDER BY COUNT(u) DESC
queryFactory
    .select(user.team.name, user.count())
    .from(user)
    .groupBy(user.team.name)
    .orderBy(user.count().desc())
    .fetch();

7. DTO로 매핑하기

SELECT new com.example.UserDto(u.id, u.name) FROM User u
queryFactory
    .select(new QUserDto(user.id, user.name)) // @QueryProjection 쓰는 경우
    .from(user)
    .fetch();

또는:

Projections.constructor(UserDto.class, user.id, user.name)

요약 비교표

JPQL vs QueryDSL

SELECT u selectFrom(user)
WHERE u.name = :name where(user.name.eq(name))
LIKE %:keyword% user.name.contains(keyword)
JOIN u.team t join(user.team, team)
JOIN FETCH u.team join(user.team, team).fetchJoin()
GROUP BY groupBy(...)
ORDER BY orderBy(...)
new DTO(...) Projections.constructor(...)

조건 동적 조합

queryFactory
  .selectFrom(user)
  .where(
    name != null ? user.name.eq(name) : null,
    email != null ? user.email.contains(email) : null
  )
  .fetch();

또는 BooleanExpression으로 추출해서 깔끔하게 만들 수도 있다


 

1. 기본 SELECT

@Query("SELECT u FROM User u WHERE u.name = :name")
User findByName(@Param("name") String name);

🔁 QueryDSL:

queryFactory.selectFrom(user)
            .where(user.name.eq(name))
            .fetchOne();

2. DTO 생성 (생성자 방식)

@Query("SELECT new com.example.dto.UserDto(u.id, u.name) FROM User u")
List<UserDto> findAllUserDto();

🔁 QueryDSL:

queryFactory
    .select(new QUserDto(user.id, user.name)) // @QueryProjection 사용 시
    .from(user)
    .fetch();

또는:

Projections.constructor(UserDto.class, user.id, user.name)

3. JOIN

@Query("SELECT u FROM User u JOIN u.team t WHERE t.name = :teamName")
List<User> findByTeamName(@Param("teamName") String teamName);

🔁 QueryDSL:

queryFactory.selectFrom(user)
            .join(user.team, team)
            .where(team.name.eq(teamName))
            .fetch();

4. JOIN FETCH (Lazy 방지)

@Query("SELECT u FROM User u JOIN FETCH u.team WHERE u.id = :id")
User findUserWithTeam(@Param("id") Long id);

🔁 QueryDSL:

queryFactory.selectFrom(user)
            .join(user.team, team).fetchJoin()
            .where(user.id.eq(id))
            .fetchOne();

5. LIKE, BETWEEN, IN

@Query("SELECT u FROM User u WHERE u.name LIKE %:keyword%")
List<User> searchByName(@Param("keyword") String keyword);

🔁 QueryDSL:

queryFactory.selectFrom(user)
            .where(user.name.containsIgnoreCase(keyword))
            .fetch();

@Query("SELECT u FROM User u WHERE u.age BETWEEN :min AND :max")
List<User> findBetween(@Param("min") int min, @Param("max") int max);

🔁 QueryDSL:

queryFactory.selectFrom(user)
            .where(user.age.between(min, max))
            .fetch();

@Query("SELECT u FROM User u WHERE u.name IN :names")
List<User> findByNames(@Param("names") List<String> names);

🔁 QueryDSL:

queryFactory.selectFrom(user)
            .where(user.name.in(names))
            .fetch();

6. GROUP BY + COUNT

@Query("SELECT u.team.name, COUNT(u) FROM User u GROUP BY u.team.name")
List<Object[]> groupByTeam();

🔁 QueryDSL:

queryFactory.select(user.team.name, user.count())
            .from(user)
            .groupBy(user.team.name)
            .fetch();

7. ORDER BY

@Query("SELECT u FROM User u ORDER BY u.createdAt DESC")
List<User> findAllByCreatedDesc();

🔁 QueryDSL:

queryFactory.selectFrom(user)
            .orderBy(user.createdAt.desc())
            .fetch();

8. EXISTS / COUNT

@Query("SELECT COUNT(u) > 0 FROM User u WHERE u.email = :email")
boolean existsByEmail(@Param("email") String email);

🔁 QueryDSL:

boolean exists = queryFactory
    .selectOne()
    .from(user)
    .where(user.email.eq(email))
    .fetchFirst() != null;

또는 .fetchCount() > 0 (deprecated됨)


요약표

@Query(JPQL) vs QueryDSL

SELECT SELECT u FROM ... .selectFrom(entity)
조건절 WHERE .where(...)
DTO 매핑 new UserDto(...) Projections.constructor() or @QueryProjection
조인 JOIN u.team .join(user.team, team)
페치 조인 JOIN FETCH u.team .join(...).fetchJoin()
정렬 ORDER BY .orderBy(...)
집계 COUNT(u) .select(user.count())
그룹 GROUP BY .groupBy(...)
존재 여부 COUNT > 0 .selectOne().where(...).fetchFirst() != null

 

'Java > Spring Boot' 카테고리의 다른 글

Spring Boot Test 관련 (JUnit, Mockito, AssertJ,Spring Boot Test, Testcontainers )  (0) 2025.03.31
Optional<T>  (0) 2025.03.31
JPA  (3) 2025.03.31
OAuth 구현  (0) 2025.03.28
Redis, kafka  (0) 2025.03.27