본문 바로가기
프로젝트/팀프로젝트 qp편

SpringBoot 3.x 버전 QueryDSL 설정

by Thumper 2024. 3. 21.

이번 시간에는 프로젝트에서 JPA, Querydsl을 적용한 부분을 정리하고자 한다.
최근 스프링 부트 3.0 이상 버전이 나옴에 따라 바뀐 설정 내용을 함께 공유하려고 한다.


목차는 다음과 같다.

  • Gradle 설정
  • Jpa Custom Respository 적용

1. Gradle 설정

개발환경은 다음과 같다.

  • IntelliJ
  • Spring Boot 3.1.1
  • Java 17
  • Gradle
  • Lombok
  • DB : MySQL

먼저 build.gradle을 열어 아래와 같이 Querydsl 관련 설정을 추가한다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.1'
    id 'io.spring.dependency-management' version '1.1.0'

}

group = 'inf'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //Querydsl 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"

    ...
}


/**
 * QueryDSL Build Options
 */
def querydslDir = "src/main/generated"

sourceSets {
    main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean.doLast {
    file(querydslDir).deleteDir()
}

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

bootJar {
    duplicatesStrategy = 'exclude'
}

여기까지가 querydsl 관련 설정 방법이다.
최근 spring boot 3.0 이상부터 어떻게 사용해야 하는지 정리해보자.

Spring Boot 3.0 이상 사용할 때

위와 같이 Querydsl을 설정하면 QClass 생성 방법이 다르다.

세팅

Gradle > Tasks > other > compileJava 를 실행시키면 src/main/generated에 Q 클래스들이 생성된다.

2. Jpa Custom Respository 적용

먼저 사용할 Entity는 다음과 같다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "room")
public class Room {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "room_id")
    private Long id;

    private Long studyChatBoxId; // 채팅창 pk 저장하는 변수
    private String hostEmail; // 방장 닉네임
    private String title; // 방 제목
    private int expectedUsers; // 인원수 제한

    private int studyTimer; // 스터디 타이머
    private int currentUserNum; // 현재 인원 수


    @OneToMany(mappedBy = "room", cascade = CascadeType.ALL)
    private Set<RoomHashTag> roomHashTags = new HashSet<>(); // 방에 여러 태그를 붙일 수 있다.

    @Enumerated(EnumType.STRING)
    private RoomStatus roomStatus; // 모집 상태 (자리 남았는지? OPEN /CLOSED)

    @Enumerated(EnumType.STRING)
    private RoomThumbnail thumbnail; // 섬네일 선택지

    @JsonIgnore
    @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private  Set<User> participants = new HashSet<>(); // 방 참여자들 (연관 관계)


    @Builder(builderMethodName = "createRoom")
    public Room(String hostEmail, String title, int expectedUsers, RoomThumbnail thumbnail) {
        this.hostEmail = hostEmail;
        this.title = title;
        this.expectedUsers = expectedUsers;
        this.roomStatus = RoomStatus.OPEN;
        this.thumbnail = thumbnail;
        this.studyTimer = 0;
        this.currentUserNum = 0;
    }
    ...

Spring Data Jpa에서는 Custom Repository를 JpaRepository 상속 클래스에서 사용할 수 있도록 기능을 지원한다.

전체적인 그림은 아래와 같다.
query 구조

위와 같이 구성하면 RoomRepository에서 RoomRepositoryCustomImpl의 코드도 사용할 수 있다.
RoomRepositoryCusom 인터페이스와 RoomRepositoryCusomImpl 클래스를 다음과 작성하면 된다.

RoomRepositoryCusom

package inf.questpartner.repository.room;

import inf.questpartner.controller.dto.RoomSearchCondition;
import inf.questpartner.domain.room.Room;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface RoomRepositoryCustom {
    // room 모두 조회 (room - RoomHashTag, User Join해서)
    Page<Room> findAllWithTagAndUser(Pageable pageable);

   // 특정 Tag가 포함된 Room 모두 조회
    Page<Room> findByTagOption(RoomSearchCondition condition, Pageable pageable);
}

RoomRepositoryCusomImpl

package inf.questpartner.repository.room;

import com.querydsl.core.QueryResults;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import inf.questpartner.controller.dto.RoomSearchCondition;
import inf.questpartner.domain.room.Room;
import inf.questpartner.domain.room.common.tag.TagOption;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;


import static inf.questpartner.domain.room.QRoom.room;
import static inf.questpartner.domain.room.QRoomHashTag.roomHashTag;
import static inf.questpartner.domain.users.user.QUser.*;
import static org.springframework.util.StringUtils.hasText;

public class RoomRepositoryCustomImpl implements RoomRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public RoomRepositoryCustomImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }


    @Override
    public Page<Room> findAllWithTagAndUser(Pageable pageable) {
        QueryResults<Room> result = queryFactory.selectFrom(room)
                .leftJoin(room.roomHashTags, roomHashTag).fetchJoin()
                .leftJoin(room.participants, user).fetchJoin()
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        return new PageImpl<>(result.getResults(),pageable,result.getTotal());
    }

    @Override
    public Page<Room> findByTagOption(RoomSearchCondition condition, Pageable pageable) {
        QueryResults<Room> result = queryFactory.selectFrom(room)
                .leftJoin(room.roomHashTags, roomHashTag).fetchJoin()
                .leftJoin(room.participants, user).fetchJoin()
                .where(tagOptionContains(condition.getTagOption()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        return new PageImpl<>(result.getResults(),pageable,result.getTotal());
    }

    private static BooleanExpression tagOptionContains(TagOption tagOption) {
        return tagOption != null ? roomHashTag.tagOption.eq(tagOption) : null;
    }


    private static BooleanExpression titleContains(String title) {
        return hasText(title) ? room.title.toLowerCase().like("%" + title.toLowerCase() + "%") : null;
    }
}

댓글