BackEND/Java

QueryDSL을 활용한 동적 쿼리 작성 및 성능 개선

mingmingIT 2025. 4. 3. 10:21

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 조회 최적화
서브쿼리 및 페이징 최적화 적용
인덱스 및 캐싱을 통한 성능 개선