Coding/Server

[내일배움캠프] QueryDSL

kangplay 2025. 5. 14. 20:19

1. QueryDSL이란?

QueryDSL은 미리 컴파일된 Q타입 클래스를 사용하여 타입 안전한 JPQL 쿼리를 동적으로 생성하는 기술이다. JPQL은 JPA의 쿼리 언어로, 데이터베이스에서 JPA 엔티티를 조회하거나 조작하는 데 사용된다. QueryDSL은 JPQL을 더 안전하고 유연하게 작성할 수 있도록 타입 안전한 메커니즘을 제공한다.

2. Q타입 클래스란?

Q타입 클래스는 JPA 엔티티의 메타모델로, 쿼리의 필드명과 타입을 컴파일 타임에 체크할 수 있게 도와준다. 이를 통해 오타나 잘못된 필드명이 있을 경우, 컴파일 타임에 오류를 잡을 수 있다. 또한, Q타입 클래스는 동적 쿼리를 포함한 쿼리를 더 유연하게 작성할 수 있도록 하는 여러 메소드들을 제공한다.  즉, QueryDSL가 JPQL를 작성하기 위해 최적화된 클래스라고 볼 수 있다. 

3. QueryDSL 사용

  • JPAQueryFactory는 QueryDSL에서 JPA를 사용할 때 쿼리를 생성하고 실행하는 데 사용되는 핵심 클래스로, JPA 쿼리 를 빌드하고, 실행하는 데 필요한 여러 메서드를 제공한다.
  • Q 클래스 인스턴스를 사용하는 방법은 2가지가 있는데, 일반적으로 위 코드처럼 static 멤버 변수로 접근하는 경우를 사용한다. 만약 같은 테이블을 조인해야하는 경우, 두 테이블에 다른 alias를 지정해야하므로, 아래 방법은 사용해야한다.
QMember m = new QMember("m");
  • 검색 조건 쿼리
    • where(member.age.eq(10), null) 처럼, AND 조건은 여러 파라미터로 추가할 수도 있고, null이 인자로 들어간 경우  메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있다!
  • 결과 조회
    • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
    • fetchOne() : 단 건 조회
      • 결과가 없으면 : null
      • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
    • fetchFirst() : limit(1).fetchOne()
    •  fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
    •  fetchCount() : count 쿼리로 변경해서 count 수 조회
      • fetchResults()는 개수를 조회할 때 간단하게 가져올 수 있는데 복잡하게 조회해서 성능 측면에서 효율적이지 않을 수 있다. 따라서 fetchCount()로 개수를 조회하는 별도의 쿼리로 분리하는 게 나은 경우도 있다. 
//페이징에서 사용
QueryResults<Member> results = queryFactory
 .selectFrom(member)
 .fetchResults();

results.getTotal();  //쿼리 1번(페이지 수)
List<Member> content = results.getResults(); //쿼리 2번 

//count 쿼리로 변경
long count = queryFactory
 .selectFrom(member)
 .fetchCount();
  • 집합 쿼리
    • 반환값이 Tuple인 것에 유의하자. (데이터 타입이 여러 개라서)
    • 실무에서는 Tuple보다는 직접 DTO로 반환받는다.
@Test
    public void aggregation() throws Exception {
        List<Tuple> result = queryFactory
                .select(member.count(),
                        member.age.sum(),
                        member.age.avg(),
                        member.age.max(),
                        member.age.min())
                .from(member)
                .fetch();
    }
      • 페치조인
        • join(), leftJoin()` 조인 기능 뒤에 `fetchJoin()` 이라고 추가하면 된다.
      • 서브쿼리
        • com.querydsl.jpa.JPAExpressions 사용 
        • JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 ) 지원하지 않는다. 당연히 Querydsl 지원하지 않는다.
          • 해결 방법으로는, 서브쿼리를 join으로 변경하거나 (높은 확률로 가능하다.) 애플리케이션에서 쿼리를 2 분리해서 실행한다. 혹은, nativeSQL 사용하면 된다.

4. QueryDSL 프로젝션

프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있지만, 둘 이상이면 튜플이나 DTO로 조회해야한다. 튜플은 QueryDSL에 의존적인 자료구조이므로, 레포지토리 레이어외에 다른 layer에서 쓰면, 외부 기술에 의존한다는 단점이 생긴다. 따라서 실무에서는 DTO를 많이 사용한다.  

 

순수 JPA에서는 DTO를 프로젝션으로 사용할 때, 생성자 방식만 지원했던 것과 달리, QueryDSL은 생성자, 필드, 수정자 방식을 모두 지원한다. 

//수정자
List<MemberDto> result = queryFactory
			.select(Projections.bean(MemberDto.class,member.username,member.age))
			.from(member)
			.fetch();

//필드
List<MemberDto> result = queryFactory
			.select(Projections.fields(MemberDto.class,member.username,member.age))
			.from(member)
			.fetch();

//생성자
List<MemberDto> result = queryFactory
			.select(Projections.constructor(MemberDto.class,member.username,member.age))
			.from(member)
			.fetch();

 

@QueryProjection 활용하여, DTO 클래스를 Q타입 클래스로 변환하여 사용하는 방법도 있다.
이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다만 DTO에 QueryDSL 어노테이션을 유지해야 하는 점과 DTO까지 Q 파일을 생성해야 하는 단점이 있다. (결합도 향상)

5. QueryDSL 동적쿼리

private List<Member> searchMember2(String usernameCond, Integer ageCond) {
	return queryFactory
		.selectFrom(member)
		.where(usernameEq(usernameCond), ageEq(ageCond))
		.fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
	return usernameCond != null ? member.username.eq(usernameCond) : null;
}

private BooleanExpression ageEq(Integer ageCond) {
	return ageCond != null ? member.age.eq(ageCond) : null;
}
  • where 조건에 null 값은 무시된다.
  • 메서드를 다른 쿼리에서도 재활용 있다.
  • 쿼리 자체의 가독성이 높아진다.
BooleanBuilder를 사용하여 동적쿼리를 작성할 수도 있지만, where 다중 파라미터를 사용하는 것이 가독성이 높아지기 때문에 where 다중 파라미터를 많이 사용한다.

6. Spring Data JPA와 QueryDSL 

스프링 데이터 JPA 레포지토리는 인터페이스만 정의하고, 구현체는 스프링이 자동으로 생성해준다. 따라서, 개발자가 스프링 데이터 JPA가 제공하는 인터페이스를 확장하여 직접 구현하려면, 직접 구현해야하는 기능이 너무 많아진다. 

 

QueryDSL처럼 Spring Data JPA에 사용자가 정의한 메소드를 추가(확장)하기 위해서는 사용자 정의 인터페이스와 구현체를 생성해서 Spring Data JPA를 상속한 인터페이스가 상속하도록 해줘야한다. 이때, 사용자 정의 구현체 이름을 (SpringData JPA를 사용한 인터페이스명)+Impl로 설정해주어야한다!! 

항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다. 예를 들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 직접 사용해도 된다. 물론 이경우, 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다. (복잡도 감소)