QueryDSL

이동욱·2023년 9월 28일
post-thumbnail

탄생 배경

JPA는 Java 진영에서 지원하는 대표적인 ORM(Object Relation Mapping) 인터페이스이며 RDBMS의 데이터들을 좀 더 객체지향적으로 사용 가능하게 지원하는 프레임워크이며 Hibernate라는 구현체를 사용한다

사용자가 HQL(Hibernate Query Language)를 작성하여 Hibernate에 전달하면 Hibernate는 이를 SQL문으로 바꾸어 JDBC에 전달한다.

// HQL
String qlString =
              "select m from Member m " +
              "where m.username = :username";
      Member findMember = em.createQuery(qlString, Member.class)
              .setParameter("username", "member1")
              .getSingleResult();
              
// QueryDSL              
JPAQueryFactory queryFactory = new JPAQueryFactory(em); QMember m = new QMember("m");
      Member findMember = queryFactory
              .select(m)
.from(m)
.where(m.username.eq("member1"))//파라미터 바인딩 처리
              .fetchOne();

위 코드를 보면 HQL은 문자열 기반으로 쿼리문을 작성하여 만일 쿼리를 잘못 작성한다면 컴파일 시점이 아닌 요청에 의해 코드가 실행되었을때 에러를 알 수 있다.
반면 QueryDSL은 메서드 기반으로 작성되기에 컴파일 시점에서 에러를 알 수 있다는 장점이 있으며 HQL에 비해 복잡한 쿼리나 동적 쿼리를 쉽게 생성 할 수 있다는 장점이 존재한다.

QueryDSL 환경설정(Gradle)

plugins {
	id 'java'
	id 'war'
	id 'org.springframework.boot' version '3.1.4'
	id 'io.spring.dependency-management' version '1.1.3'
    
    // QueryDSL 설정
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

group = 'com.fundingForAll'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// QueryDSL 설정
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

// QueryDSL 설정
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}
sourceSets {
	main.java.srcDir querydslDir
}
configurations {
	querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}

tasks.named('test') {
	useJUnitPlatform()
}

QueryDSL의 각 설정들을 살펴보면

id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"

QueryDSL의 plugin을 설치한다 해당 plugin을 통해 아래와 같은 기능들을 수행한다.

  • QueryDSL 코드를 생성하는 Annotation 프로세서를 실행
  • QueryDSL 코드를 프로젝트의 소스 세트에 추가
  • QueryDSL 코드를 컴파일러가 사용 가능하게 함
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

gradle에서는 compile classpath와 annotation classpath를 구분하여 사용하는데 annotationProcessor를 통해 해당 라이브러리들을 annotation classpath에 등록하여 라이브러리들이 제공하는 annotation 기능들을 사용 가능하게 한다.

해당 내용들을 정리하다 plugin과 dependency의 차이가 궁금하여 찾아보았는데 차이점은 아래와 같다.

특징plugindependency
역할Gradle의 기능을 확장프로젝트에 필요한 라이브러리를 추가
빌드 프로세스에서 수행되는 작업프로젝트의 빌드 프로세스를 제어하고, 새로운 빌드 도구와 기능을 제공프로젝트의 런타임 클래스 경로에 라이브러리를 추가
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}

QueryDSL은 빌드 과정에서 도메인 객체들을 Q-type이라는 객체로 변경하며 해당 객체를 통해 QueryDSL 메소드들을 HQL로 변경한다.
위 설정을 통해 생성한 Q-type 객체들을 $buildDir/generated/querydsl 폴더에 저장한다.

sourceSets {
	main.java.srcDir querydslDir
}

querydslDir에 생성된 Q-type 객체들을 main에서 사용 가능하게 설정한다.

configurations {
	querydsl.extendsFrom compileClasspath
}

이 부분에 대해서는 정확히 모르겠지만 아마 QueryDSL을 classpath에 추가 한다고 추측된다.

compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}

QueryDSL 어노테이션을 실행할 때 사용할 어노테이션 라이브러리 경로를 지정한다.

사용법

where절

QUser qUser = QUser.user;

User findUser = queryFactory.select(qUser)
	.from(qUser)
	.where(qUser.email.eq(email))
	.fetchFirst();
  • ,을 통해 and조건을 추가 가능하며 조건이 false일경우 where절을 생략한다.

where절 동적쿼리(BooleanExpression), OrderBy절 동적쿼리(OrderSpecifier)

public List<Fund> getFundList(Search search) {
        QFund qFund = QFund.fund;
        QContent qContent = QContent.content;

        SearchType searchType = search.getSearchType();
        SortType sortType = search.getSortType();
        String searchKeyword = search.getSearchKeyword();

        List<Fund> fundList = queryFactory
                .selectFrom(qFund)
                .where(eqSearchType(searchType, searchKeyword))
                .orderBy(createOrderSpecifier(sortType))
                .offset(search.getStartRowNum())
                .limit(search.getEndRowNum())
                .fetch();

        return fundList;
    }
    
private BooleanExpression eqSearchType(SearchType searchType, String searchKeyword) {
        QFund qFund = QFund.fund;

        return switch (searchType) {
            case ID -> qFund.user.id.like(searchKeyword);
            case TITLE -> qFund.title.contains(searchKeyword);
            case NONE -> null;
        };
    }

private OrderSpecifier createOrderSpecifier(SortType sortType) {
        QFund qFund = QFund.fund;

        return switch (sortType) {
            case VIEWS -> new OrderSpecifier(Order.DESC, qFund.views);
            case REG_DATE -> new OrderSpecifier(Order.DESC, qFund.regDate);
            default -> new OrderSpecifier(Order.ASC, NullExpression.DEFAULT, OrderSpecifier.NullHandling.Default);
        };
    }
profile
Backend Developer

0개의 댓글