JPA의 기본적인 조회 기능만으로는 복잡한 조건을 가진 동적 쿼리를 처리하기 어렵습니다. QueryDSL을 사용하면 타입 안전한 방식으로 가독성이 뛰어난 동적 쿼리를 작성할 수 있습니다. 이번 포스팅에서는 QueryDSL을 활용한 동적 쿼리 작성 및 성능 개선 기법을 소개합니다.
1. QueryDSL 기본 설정
1) QueryDSL 의존성 추가 (Gradle 기준)
implementation 'com.querydsl:querydsl-jpa:5.0.0'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
2) Q클래스 생성
QueryDSL은 엔티티 클래스를 기반으로 Q클래스를 자동 생성합니다. 예를 들어 Member 엔티티가 있다면 QMember 클래스가 생성됩니다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
}
💡 Q클래스는 target/generated-sources 폴더에 자동 생성되므로, IDE에서 해당 폴더를 소스 경로로 추가해야 합니다.
2. QueryDSL을 활용한 기본 조회
1) QueryFactory를 활용한 기본 조회
QMember member = QMember.member;
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
List<Member> members = queryFactory
.selectFrom(member)
.where(member.age.gt(20))
.fetch();
💡 JPAQueryFactory를 사용하면 EntityManager 없이 간결한 코드로 쿼리를 작성할 수 있습니다.
2) BooleanBuilder를 활용한 동적 쿼리
BooleanBuilder를 활용하면 다양한 조건을 동적으로 추가할 수 있습니다.
public List<Member> searchMembers(String name, Integer minAge, Integer maxAge) {
QMember member = QMember.member;
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(member.name.eq(name));
}
if (minAge != null) {
builder.and(member.age.goe(minAge));
}
if (maxAge != null) {
builder.and(member.age.loe(maxAge));
}
return queryFactory.selectFrom(member)
.where(builder)
.fetch();
}
💡 BooleanBuilder는 여러 조건을 조합하는 데 유용하며, 조건이 없을 경우 자동으로 무시됩니다.
3. Where절 결합을 활용한 동적 쿼리
BooleanBuilder 대신 where 조건을 메서드로 분리하여 더욱 깔끔하게 작성할 수 있습니다.
public List<Member> searchMembers(String name, Integer minAge, Integer maxAge) {
QMember member = QMember.member;
return queryFactory.selectFrom(member)
.where(nameEq(name), ageBetween(minAge, maxAge))
.fetch();
}
private BooleanExpression nameEq(String name) {
return name != null ? QMember.member.name.eq(name) : null;
}
private BooleanExpression ageBetween(Integer minAge, Integer maxAge) {
if (minAge != null && maxAge != null) {
return QMember.member.age.between(minAge, maxAge);
} else if (minAge != null) {
return QMember.member.age.goe(minAge);
} else if (maxAge != null) {
return QMember.member.age.loe(maxAge);
}
return null;
}
💡 BooleanExpression을 활용하면 쿼리 조건을 메서드로 분리하여 재사용할 수 있습니다.
4. Projections을 활용한 DTO 조회 최적화
Entity가 아닌 DTO로 데이터를 조회하면 성능을 최적화할 수 있습니다.
@Data
public class MemberDto {
private String name;
private int age;
public MemberDto(String name, int age) {
this.name = name;
this.age = age;
}
}
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.name,
member.age))
.from(member)
.fetch();
💡 DTO 조회를 사용하면 불필요한 데이터를 가져오지 않으므로 성능을 개선할 수 있습니다.
5. SubQuery 활용
QueryDSL에서는 JPAExpressions를 활용하여 서브쿼리를 작성할 수 있습니다.
QMember subMember = new QMember("sub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(subMember.age.max())
.from(subMember)
))
.fetch();
💡 서브쿼리를 활용하면 복잡한 비즈니스 로직을 효율적으로 처리할 수 있습니다.
6. QueryDSL을 활용한 성능 최적화
1) 페이징 성능 최적화 (fetchResults vs fetch)
기존 fetchResults()는 Hibernate 6에서 지원이 중단되었으므로 fetch()와 count()를 별도로 실행하는 것이 좋습니다.
JPAQuery<Member> query = queryFactory.selectFrom(member);
List<Member> content = query
.offset(0)
.limit(10)
.fetch();
long total = queryFactory
.select(member.count())
.from(member)
.fetchOne();
💡 페이징을 적용할 때는 데이터 개수를 별도로 조회하여 성능을 최적화할 수 있습니다.
2) 인덱스 활용 및 캐싱 적용
- 인덱스 설정 (@Index)를 통해 조회 성능 향상
- Redis 캐싱을 적용하여 반복적인 조회 성능 개선
@Table(indexes = {@Index(name = "idx_member_name", columnList = "name")})
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
}
💡 QueryDSL과 캐싱을 함께 사용하면 대량 데이터 조회 성능을 크게 향상할 수 있습니다.
결론
QueryDSL을 활용하면 JPA의 한계를 극복하고 성능을 최적화할 수 있습니다.
✅ BooleanBuilder 및 Where절 결합을 활용한 동적 쿼리 작성
✅ Projections을 사용한 DTO 조회 최적화
✅ 서브쿼리 및 페이징 최적화 적용
✅ 인덱스 및 캐싱을 통한 성능 개선
'BackEND > Java' 카테고리의 다른 글
실제 프로젝트에서 QueryDSL과 캐싱을 활용한 최적화 전략 (0) | 2025.04.05 |
---|---|
QueryDSL과 JPA 성능 튜닝 전략 (0) | 2025.04.04 |
JPA의 기본 조회 성능 최적화 기법 (0) | 2025.04.02 |
JPA와 QueryDSL 소개 및 비교 (0) | 2025.04.01 |
Spring Batch를 활용한 대용량 데이터 처리 (0) | 2025.03.31 |