QueryDSL 적용기
GitHub - Bipum-In/Bipum-In-BE: 비품인 백엔드 레포입니다.
비품인 백엔드 레포입니다. Contribute to Bipum-In/Bipum-In-BE development by creating an account on GitHub.
github.com
이번에 진행했던 프로젝트에서 대시보드 기능 구현을 위해 쿼리를 작성할 일이 많았다. 기존 JPA를 사용하기에는 쿼리메소드의 이름이 길어져서 직관적이지 않다는 문제가 있었고, N+1 문제가 발생하여 성능이 떨어지는 이슈가 발생했다.
JPQL 과 Native Query를 사용해서 복잡한 이름의 쿼리 메소드들을 직접 변환하여 문제를 해결하였지만, 그 과정에서 쿼리 문법 오류에 의한 Runtime Error가 자주 발생하였다. JPQL에서는 컴파일 시, 문법 오류를 체크해줄 수 없었기 때문에 사전에 런타임 에러를 방지할 수 있는 QueryDSL 을 적용해보기로 했다.
QueryDSL 이란?
먼저, QueryDSL이 무엇인지 정확히 알고가야할 것 같다.
QueryDSL은 SQL, JPQL 등을 코드로 작성할 수 있도록 지원해주는 빌더 오픈소스 프레임워크로서, JPA 뿐만 아니라 Mongodb, SQL 등 다양한 언어에 대한 서비스를 제공한다고 한다.
QueryDSL JPA
JPA에 대한 QueryDSL인 QueryDSL JPA는 Entity 클래스와 매핑되는 QClass 라는 객체를 사용하여 쿼리를 실행한다.
QClass는 컴파일 단계에서 엔티티를 기반으로 생성되는 객체로서, JPAAnnotationProcessor 가 컴파일 시점에 작동하여 @Entity 등의 어노테이션의 찾아 해당 파일을 분석하여 QClass를 생성하게 된다. 즉, QClass는 Entity 클래스를 QueryDSL 사용을 위해 변환한 클래스라고 보면 될 것 같다.
QueryDSL 사용을 위해서, 여러가지 설정 추가가 필요하다.
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.9'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
apply plugin: "io.spring.dependency-management"
group = 'com.sparta'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
//querydsl 추가
implementation("com.querydsl:querydsl-core") // querydsl
implementation("com.querydsl:querydsl-jpa") // querydsl
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
}
// querydsl에서 사용할 경로 설정
def querydslDir = "src/main/generated"
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
tasks.named("compileJava") {
options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
clean.doLast {
file(querydslDir).deleteDir()
}
jar {
enabled = false
}
대다수의 사람들이 사용하는 QueryDSL 세팅방법으로 적용하니, Runtime 에러가 지속적으로 발생해서 모 개발 블로그에서 다른 방법으로 세팅을 진행하는 코드를 참고했다. 블로그를 다시 찾기가 어려워 링크는 참고하지 못하는 점 양해 부탁드린다.
만약, 실행 시 QClass 를 찾지 못한다는 Runtime 오류가 발생한다면 아래의 블로그를 참고하되, InteliJ 기준 Project Structure - Modules 메뉴에 빌드 후 생성될 generated 폴더가 소스 폴더로 지정되어 있는지 확인 바란다.
만약 되어있지 않다면, generated 폴더를 선택하고 폴더 모양의 Sources를 누르면 소스 폴더로 지정된다.
위 설정 때문에 시간을 엄청나게 잡아먹었던 기억이 난다.. 도움이 되길 바란다.
Intellij에서 QueryDSL 오류 (cannot find symbol Q class)
Spring Boot 프로젝트에서 Spring Data JPA를 사용할때 한 번쯤 볼수있는 오류입니다. 참고로 JPA에서 쿼리를 작성하는 방법은 쿼리메소드, @Query…
devfoxstar.github.io
Config.java
//해당 파일에서 등록한 jpaFactory 빈을 Repository에서 사용한다.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
프로젝트 전역에서 QueryDSL을 작성할 수 있도록, JPAQueryFactory를 Bean으로 등록해준다.
여러 엔티티에서 쿼리 dsl 작업을 할 것이기 때문에 한 개의 폴더에서 쿼리 DSL관련 파일을 확인할 수 있도록 폴더구조를 구성했다.
기존 Repository 구조를 유지하면서 쿼리 DSL 관련 코드를 추가하길 원했기 때문에, 관련 인터페이스를 추가하고, 인터페이스에 대한 구현체에서 추상메서드를 구현해주었다.
인터페이스
public interface NotificationRepositoryCustom {
Page<NotificationResponseForAdmin> findUserNotification(Long adminId, Pageable pageable);
Page<NotificationResponseForUser> findAdminNotification(Long userId, Pageable pageable);
List<Notification> findOldNotification();
}
구현체
@Repository
@RequiredArgsConstructor
public class NotificationRepositoryImpl implements NotificationRepositoryCustom{
private final JPAQueryFactory jpaQueryFactory;
// notification의 request_id 와 같은 요청과 그 요청에 해당하는 관리자의 정보를 가져온다 (승인/폐기 등 처리 건만)
// 요청의 수신자가 userId에 해당하는 알림 건만 가져온다.
public Page<NotificationResponseForAdmin> findUserNotification(Long adminId, Pageable pageable) {
QNotification notification = QNotification.notification;
QRequests request = QRequests.requests;
QUser user = QUser.user;
JPAQuery<NotificationResponseForAdmin> query = jpaQueryFactory.select(Projections.constructor(
NotificationResponseForAdmin.class,
notification.content, user.image, notification.createdAt.as("createdAt"),
request.requestId.as("requestId"), notification.id.as("notificationId"), request.requestType
)
)
.from(notification)
.join(request).on(notification.request.requestId.eq(request.requestId))
.join(user).on(request.user.id.eq(user.id))
.where(notification.receiver.id.eq(adminId)
.and(notification.notificationType.eq(NotificationType.REQUEST))
.and(notification.isRead.eq(false)))
.orderBy(notification.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
List<NotificationResponseForAdmin> notificationList = query.fetch();
long totalCount = query.fetchCount();
return new PageImpl<>(notificationList, pageable, totalCount);
}
// notification의 requests_id 와 같은 요청과 그 요청에 해당하는 유저의 정보를 가져온다 (요청 건만)
// 요청의 수신자가 adminId에 해당하는 알림 건만 가져온다.
public Page<NotificationResponseForUser> findAdminNotification(Long userId, Pageable pageable) {
QNotification notification = QNotification.notification;
QRequests requests = QRequests.requests;
QUser user = QUser.user;
JPAQuery<NotificationResponseForUser> query = jpaQueryFactory.select(Projections.constructor(
NotificationResponseForUser.class,
notification.content, notification.createdAt.as("createdAt"), notification.acceptResult,
requests.requestId.as("requestId"), notification.id.as("notificationId"), requests.requestType
)
)
.from(notification)
.join(requests).on(notification.request.requestId.eq(requests.requestId))
.join(user).on(requests.admin.id.eq(user.id))
.where(notification.receiver.id.eq(userId)
.and(notification.notificationType.eq(NotificationType.PROCESSED))
.and(notification.isRead.eq(false)))
.orderBy(notification.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
List<NotificationResponseForUser> notificationList = query.fetch();
long totalCount = query.fetchCount();
return new PageImpl<>(notificationList, pageable, totalCount);
}
public List<Notification> findOldNotification() {
QNotification notification = QNotification.notification;
BooleanExpression isRead = notification.isRead.eq(true);
BooleanExpression createdAt = notification.createdAt.before(LocalDateTime.now().minusDays(1));
BooleanExpression now = Expressions.asBoolean(true);
return jpaQueryFactory.selectFrom(notification)
.where(isRead, createdAt, now)
.fetch();
}
}
기존 Repository
public interface NotificationRepository extends JpaRepository<Notification, Long>, NotificationRepositoryCustom {
long countByReceiver_IdAndNotificationTypeAndIncludeCountTrue(Long id, NotificationType notificationType);
List<Notification> findByReceiver_IdAndNotificationTypeAndIncludeCountTrue(Long id, NotificationType notificationType);
List<Notification> findByRequest_RequestId(Long requestId);
List<Notification> findByReceiverAndNotificationType(User user, NotificationType notificationType);
}
기존 Repository에서 해당 인터페이스를 상속받아 사용할 수 있도록 구성했다.
참고 자료
Spring Boot에 QueryDSL을 사용해보자
1. QueryDSL PostRepository.java Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도, 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL…
tecoble.techcourse.co.kr