[Spring Boot/JPA] QueryDSL 설정과 Repository에서의 사용

2024. 1. 23. 02:42
반응형

 

 

 

 

이번 포스팅에서는 Spring Boot에서 이전에 사용했던 JPQL와 JpaRepository 보다 조금 더 객체지향스럽고 유동적인 동적 쿼리를 작성할 수 있도록 QueryDSL 사용을 위한 설정을 해보도록 하자.

 

QueryDSL에 사용에 대한 견해를 미리 말해보자면 유연한 쿼리 작성과 안정적인 코드를 작성하기 위해서 매우 유용할 것이라는 말을 하고 싶다.

 

개인적으로는 C# .NET 진영에서 사용했던 Entity Framework와 유사하게 Builder 패턴을 이용한 동적 쿼리 작성과 객체지향 코드라는 점이 유사하여 진입하기가 괜찮았기도 했다. 다른 진영의 객체지향 ORM을 사용해보았다면 적응하기 쉬울 것 같다.

 

 

 

 


QueryDSL의 필요성

 

    return em.createQuery("select a, count(b) " +
                    " from author a " +
                    " join a.books b " +
                    " where a.name = :authorName " +
                    " group by a",
    Author.class).getResultList();
    return queryFactory
            .select(author, author.books.size())
            .from(author)
            .leftJoin(author.books).fetchJoin()
            .where(author.name.eq(authorName))
            .groupBy(author)
            .fetch();

 

위쪽의 쿼리는 JPQL을 사용한 경우이고, 아래쪽은 QueryDSL을 이용한 경우이다.

 

기존에 사용해왔던 JPQL 기반의 쿼리는 간단하게 쿼리 작성을 지원하지만, 한계점을 가지고 있었다.

문자열 기반의 쿼리이기 때문에 오타가 발생하거나 관리하기가 어렵고 컴파일 단계에서 에러를 찾아낼 수 없다는 문제점이 있다.

또한 복잡하고 여러 개의 파라미터가 들어가야 하는 동적 쿼리를 작성하기에 부적합하다.

 

QueryDSL을 사용하면 Builder Pattern 스타일로 동적 쿼리를 작성할 수 있어 문자열에서 코드로 전환됨에 따라서 가독성과 객체지향성을 높일 수 있다. 또한 QueryDSL은 QClass이라고 불리는 엔티티 클래스를 자바 코드로 표현한 메타모델 클래스를 빌드하여 사용하기 때문에 타입에 대한 안정성을 높이고, 오류를 사전에 잡아낼 수 있게 할 수 있다.

 

 

 

 


Dependencies 추가 (Spring Boot 3)

 

 

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.7.4'
	compileOnly 'org.projectlombok:lombok'

	testImplementation("org.junit.vintage:junit-vintage-engine") {
		exclude group: "org.hamcrest", module: "hamcrest-core"
	}

	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

	implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.4.2'

	//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에서 Spring Boot 3 기준으로 아래 네 줄을 추가해주면 QueryDsl에 대한 의존성이 추가된다.

 

 

 

 

 

 


QClass 빌드 

 

 

Gradle의 Task에서 [build > clean] ->[other > compileJava] 과정을 차례로 수행해보자.

 

 

 

 

 

다음 사진과 같이 build 디렉터리에 Entity에서 생성된 Q Class가 성공적으로 빌드되었는지 확인해보자.

컴파일 단계에서 Entity와 형태가 같은 Static Class로 QClass을 생성하고, QueryDSL은 해당 클래스를 기반으로 쿼리 메서드를 실행시키게 된다. 따라서 타입의 불일치에 대한 에러 캐치에 관련하여 좋은 장점을 가질 수 있다.

 

 

 

 

QClass 뜯어보기

 

/**
 * QMember is a Querydsl query type for Member
 */
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {

    private static final long serialVersionUID = 1003972608L;

    public static final QMember member = new QMember("member1");

    public final ListPath<Category, QCategory> categories = this.<Category, QCategory>createList("categories", Category.class, QCategory.class, PathInits.DIRECT2);

    public final StringPath email = createString("email");

    public final NumberPath<Long> memberId = createNumber("memberId", Long.class);

    public final StringPath name = createString("name");

    public final StringPath password = createString("password");

    public final EnumPath<com.example.tosshelperappserver.common.constant.RoleType> role = createEnum("role", com.example.tosshelperappserver.common.constant.RoleType.class);

    public QMember(String variable) {
        super(Member.class, forVariable(variable));
    }

    public QMember(Path<? extends Member> path) {
        super(path.getType(), path.getMetadata());
    }

    public QMember(PathMetadata metadata) {
        super(Member.class, metadata);
    }

}

 

생성된 QClass 내부를 살짝만 한번 들여다보고 가보자.

EntityPathBase<T>를 상속받는데, 이는 엔티티에 대한 경로를 나타내는 QueryDSL의 추상 클래스이다.

멤버 변수로 매핑된 엔티티를 기반으로 자동 생성된 속성들이 존재한다.

 

Type으로 그냥 String이 아니라, StringPath, 그리고 createString 따위의 메서드를 사용하는 것을 볼 수 있는데, 이는 Builder Pattern을 활용하여 QType의 속성을 변경할 수 있도록 설계되었기 때문이다.

 

 

 


QueryDSL 사용을 위한 Factory를 Bean으로 등록

 

@Configuration
public class QueryDslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

 

JPAQueryFactory는 QueryDSL에서 제공하는 주요 클래스 중 하나이다. 해당 Config 파일을 만들어 JPAQueryFactory를 QueryDSL을 이용한 JPA 쿼리를 빌드하는 Factory 역할로서 사용할 수 있도록 Bean으로 등록시켜 두자.

 

 

 

 


Repository에서 사용하기

 

 

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {


    // 쿼리 메서드
    Member findMemberByEmail(String email);

    // 동적 쿼리 생성
    @Query(value= "select m from Member m where m.memberId = :id and m.email = :email" )
    Member findMemberByIdAndEmail(@Param("id") Long id, @Param("email") String email);


}

 

기존에 JpaRepository를 사용하여 다음과 같은 쿼리들을 작성해 두었다고 가정해보자, 이제 해당 Repository에서 QueryDsl을 사용한 메서드를 작성할 수 있도록 해 보자.

 

 

 

 

 

Custom Repository에서 QueryDSL을 사용하여  쿼리 빌드

 

public interface MemberCustomRepository {

    Member findAllLeftFetchJoin(Long id);
}
package com.example.tosshelperappserver.repository;
import com.example.tosshelperappserver.domain.Member;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Repository;
import static com.example.tosshelperappserver.domain.QMember.member;


@Repository
@AllArgsConstructor
public class MemberCustomRepositoryImpl implements MemberCustomRepository{

    private final JPAQueryFactory jpaQueryFactory;


    @Override
    public Member findAllLeftFetchJoin(Long id) {
        return jpaQueryFactory.selectFrom(member)
                .where(member.memberId.eq(id))
                .leftJoin(member.categories)
                .fetchJoin()
                .fetchOne();
    }
}

 

JpaRepository는 인터페이스이기 때문에 코드를 구현할 수 없다. 따라서 CustomRepository를 작성해주자.

내부에서는 JPAQueryFactory를 Injection하여 사용하고 있다.

 

쿼리문은 Builder 패턴으로 작성된다. 쿼리문을 작성하는 내용에 대해서는 다음에 다루어볼 예정이지만 해당 쿼리문은 memberId와 일치하는 Member를 Categories와 함께 Join하는 쿼리문이다. 

눈썰미가 있다면 눈치챘을 수도 있겠지만, jpaQueryFactory에서 사용되는 쿼리문의 내부에는 QClass가 사용되고 있다.

 

 

 


JpaRepository에 추가 및 Service에서의 사용

 

@Repository
public interface MemberRepository extends JpaRepository<Member, Long>, MemberCustomRepository {


    // 쿼리 메서드
    Member findMemberByEmail(String email);

    // 동적 쿼리 생성
    @Query(value= "select m from Member m where m.memberId = :id and m.email = :email" )
    Member findMemberByIdAndEmail(@Param("id") Long id, @Param("email") String email);

}

 

이제 해당 Repository를 JpaRepository에서 상속받아 QueryDsl을 사용한 메서드들을 사용할 수 있도록 하자.

 

 

 

@Override
public MemberWithCategoryDto getMemberInfoWithOwnCategory(Long id) {
    Member member = memberJpaRepository.findAllLeftFetchJoin(id);
    MemberWithCategoryDto dto = modelMapper.map(member, MemberWithCategoryDto.class);
    return dto;
}

 

 

 

 

 


결과 확인

 

 

QueryDsl을 사용하여 올바르게 데이터가 나오는지 확인해보자!

 

 

 


 

함께 보기

 

1. QueryDSL 설정과 Repository에서의 사용

https://sjh9708.tistory.com/174

 

[Spring Boot/JPA] QueryDSL 설정과 Repository에서의 사용

이번 포스팅에서는 Spring Boot에서 이전에 사용했던 JPQL와 JpaRepository 보다 조금 더 객체지향스럽고 유동적인 동적 쿼리를 작성할 수 있도록 QueryDSL 사용을 위한 설정을 해보도록 하자. QueryDSL에 사

sjh9708.tistory.com

 

2. QueryDSL 기본 문법 

https://sjh9708.tistory.com/175

 

[Spring Boot/JPA] QueryDSL 문법(1) : 기본 검색 (선택, 조건, 정렬, 집계, 그룹화)

이전 포스팅에서 QueryDSL 사용을 Repository에서 할 수 있도록 설정하는 방법에 대해서 다루어 보았었다. 이제 실제로 자주 사용되는 SQL문을 QueryDSL을 통해 작성해보도록 하자. JpaRepository는 인터페이

sjh9708.tistory.com


3. QueryDSL Join 문법

https://sjh9708.tistory.com/178

 

[Spring Boot/JPA] QueryDSL 문법(2) : Join 표현식

이전 포스팅에서 기본적인 QueryDSL의 검색 쿼리 문법에 대해서 살펴보았었다. 이번 포스팅에서는 연관된 다른 릴레이션과의 연산을 수행하는 Join과 Subquery 방법에 대해서 알아보도록 하겠다. 아

sjh9708.tistory.com


4. QueryDSL 서브쿼리와 상수/문자열 조작

https://sjh9708.tistory.com/180

 

[Spring Boot/JPA] QueryDSL 문법(3) : 서브쿼리, 상수/문자열 조작

이전 포스팅에서 기본적인 QueryDSL의 검색 쿼리 문법에 대해서 살펴보았었다. 이번 포스팅에서는 연관된 다른 릴레이션과의 연산을 수행하는 Join과 Subquery 방법에 대해서 알아보도록 하겠다. 아

sjh9708.tistory.com

 

5.  QueryDSL 프로젝션과 Entity > DTO 변환 방법들

https://sjh9708.tistory.com/181

 

[Spring Boot/JPA] Entity > DTO 변환 방법들 및 QueryDSL 프로젝션

이전 포스팅들에서 JPQL, Spring Data JPA Repository, QueryDSL 등을 통해서 데이터를 조회하는 방법들에 대해서 다루어 보았었다. 이번에는 쿼리 결과로 나온 Entity 혹은 Tuple들을 DTO로 매칭하는 방법을 알

sjh9708.tistory.com

 

6.  QueryDSL 동적 쿼리 작성하기

https://sjh9708.tistory.com/182

 

[Spring Boot/JPA] QueryDSL : 동적 쿼리 작성하기

JPA를 사용할 때, QueryDSL을 도입하는 가장 큰 이유는 동적 쿼리 작성에 매우 유연하다는 점이다. 이번 포스팅에서는 동적 쿼리를 작성하는 방법에 대해서 알아보도록 하겠다. 사용할 데이터 1. Auth

sjh9708.tistory.com

 

반응형

BELATED ARTICLES

more