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 |