[Spring Boot/JPA] QueryDSL 설정과 Repository에서의 사용
이번 포스팅에서는 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
2. QueryDSL 기본 문법
https://sjh9708.tistory.com/175
3. QueryDSL Join 문법
https://sjh9708.tistory.com/178
4. QueryDSL 서브쿼리와 상수/문자열 조작
https://sjh9708.tistory.com/180
5. QueryDSL 프로젝션과 Entity > DTO 변환 방법들
https://sjh9708.tistory.com/181
6. QueryDSL 동적 쿼리 작성하기
https://sjh9708.tistory.com/182
'Backend > Spring' 카테고리의 다른 글
[Spring Boot/JPA] QueryDSL 문법(2) : Join 표현식 (1) | 2024.01.29 |
---|---|
[Spring Boot/JPA] QueryDSL 문법(1) : 기본 검색 (선택, 조건, 정렬, 집계, 그룹화) (1) | 2024.01.25 |
[Spring Boot] Spring Security : JWT Auth (SpringBoot 3 버전) (9) | 2024.01.15 |
[Spring Boot] Swagger API Docs 작성하기 (SpringDoc, SpringBoot 3 버전) (0) | 2024.01.15 |
[Spring Boot] 사용자 정의 예외처리 : Exception Handler (0) | 2024.01.15 |