QueryDsl

이준우·2024년 5월 12일
1

계기

지인의 코드를 보면서 공부하던 중, Repository계층에서 JPAQureyFactory 를 사용하여 쿼리를 날리는 것을 보았다. JPQL을 통해서 쿼리문을 작성하는 것이 아닌, JPAQureyFactory를 사용하여 쿼리를 날리는 이유가 궁금해서 어떤 이유로 쓰고 어떻게 쓰는지 알아보기로 했다!

JPQL의 문제점

JPQL로 쿼리를 작성하다 보면, 복잡한 로직은 쿼리 문자열이 상당히 길어진다. 문자열이 길어진다는 의미는 JPQL 문자열에 오타 혹은 문법적인 오류가 발생할 확률이 높아진다는 것을 의미한다. 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생하게 된다.

QureyDSL

QuryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크이다.

  • 장점
    • 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
    • 자동 완성 등 IDE의 도움을 받을 수 있다.
    • 동적인 쿼리 작성이 편리하다
    • 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.

즉, JPQL의 문제점을 QureyDSL을 통해서 해결할 수 있다.

JPAQureyFactory란?

JPA의 엔티티를 이용하여 JPQLQurey를 보다 쉽고 편리하게 작성할 수 있는 QureyDSL의 도구이다.

엔티티로 설정된 Q모델이라는 쿼리타입 클래스를 미리 생성해놓고 메타데이터로 사용하여 메소드 기반으로 작성한다.

Q-type Class

  • QueryDSL 설정을 마치면 빌드 시 @Entity 가 붙은 클래스를 찾아 자동으로 생성
  • QueryDSL을 사용하여 메소드 기반으로 쿼리 작성 시 Domain Class의 구조를 설명해주는 메타 데이터 역할을 하며 쿼리의 조건을 설정할 때 사용된다.
  • 일반적으로 Git에 업로드 ❌

쿼리 메서드

QueryDSL 설정법, 활용법(검색조건쿼리, 기본 문법들)

해당 글에 예시와 함께 잘 정리되어 있다.

예시 코드

Gradle 세팅

https://docs.google.com/document/d/1j0jcJ9EoXMGzwAA2H0b9TOvRtpwlxI5Dtn3sRtuXQas/edit#heading=h.iayahq64el0u

ext {
    queryDslVersion = "5.0.0"
}

dependencies {
		...
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
    implementation "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
    
    // java.lang.NoClassDefFoundError:javax/persistence/Entity 에러 방지
		annotationProcessor "jakarta.persistence:jakarta.persistence-api"
		annotationProcessor "jakarta.annotation:jakarta.annotation-api"

    ...
}
  • 위 코드는 스프링부트 3.X.X를 기준으로 작성된 설정이다.
  • 3.X.X와 2.X.X의 설정법은 다르니 유의해야 한다.

  • clean을 누른 다음에 compileJava를 누르면 그 결과로

  • build 디렉터리에 Q-Type의 객체가 생성된다.

MemberQureyRepository

import java.util.Optional;

import com.example.memetory.domain.member.entity.Member;
import com.example.memetory.domain.member.entity.SocialType;

public interface MemberQueryRepository {
	Optional<Member> findBySocialTypeAndSocialId(SocialType socialType, String socialId);

	Optional<Member> findByEmail(String email);

	boolean existsMemberByNickname(String email);
}
  • 엔티티 Member 에서 사용하는 모든 조회문들을 담은 Interface
  • 일반적으로 조회문들은 QureyDSL을 사용하는 것이 좋다.

MemberQureyRepositoryImpl

import static com.example.memetory.domain.member.entity.QMember.*;

import java.util.Optional;

import org.springframework.stereotype.Repository;

import com.example.memetory.domain.member.entity.Member;
import com.example.memetory.domain.member.entity.SocialType;
import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class MemberQueryRepositoryImpl implements MemberQueryRepository {
	private final JPAQueryFactory jpaQueryFactory;

	@Override
	public Optional<Member> findBySocialTypeAndSocialId(SocialType socialType, 
	String socialId) {
		return Optional.ofNullable(jpaQueryFactory.selectFrom(member)
			.where(member.socialType.eq(socialType)
				.and(member.socialId.eq(socialId)))
			.fetchFirst());
	}

	@Override
	public Optional<Member> findByEmail(String email) {
		return Optional.ofNullable(jpaQueryFactory.selectFrom(member)
			.where(member.email.eq(email))
			.fetchFirst());
	}

	@Override
	public boolean existsMemberByNickname(String nickname) {
		return jpaQueryFactory.selectFrom(member).where(member
		.nickname.eq(nickname)).fetchOne() != null;
	}
}
  • MemberQureyRepository 을 구현한 클래스이다.
  • JPAQueryFactory를 활용하여 select문들을 완성시켰다
  • Q-type 객체에 정의되어 있는 변수명인 member 활용 하여 Q-type 객체를 불러왔다.

MemberRepository

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.memetory.domain.member.entity.Member;

public interface MemberRepository extends JpaRepository<Member, Long>,
	 MemberQueryRepository {
}
  • JpaRepositoryMemberQueryRepository 의 인터페이스들을 상속받아서, JPA 와 QueryDSL을 모두 활용할 수 있다.

  • 여기서 의문이 생긴다. MemberQueryRepository 는 그저 인터페이스인데 어떻게 구현체인 MemberQueryRepositoryImpl 의 QueryDSL을 활용할 수 있는 것 일까?

    • 스프링은 JpaRepository 인터페이스를 상속받는 Repository 인터페이스를 찾으면, 이를 구현한 프록시 객체를 생성하여 직접 기본 제공 기능을 구현한다.
    • MemberQueryRepository 을 구현한 MemberQueryRepositoryImpl 은 스프링 빈으로 등록된다.
    • 즉 프록시 객체에 Bean으로 등록된 MemberQueryRepositoryImpl이 상속되는 것이다.
    • 따라서 JpaRepository의 기능과 QueryDSL의 기능을 함께 사용할 수 있게 된다.

유의할 점

  • DTO를 사용하여 꼭 필요한 칼럼 만을 조회하여 결과를 저장하는 방식을 사용하는 것이 성능을 높일 수 있는 방법이다.

Select문에 DTO를 사용하여 원하는 필드만 가져오기

example : Jin Jin 코드 ➡️ 지인의 코드

/**
 * @author 김승진
 * @description 멤버 랭킹 응답 정보를 담는 dto
 */
@Getter
@NoArgsConstructor
public class MemberRankResponse {
    private UUID id;
    private String name;
    private String githubId;
    private Long tokens;
    private Tier tier;
    private String profileImage;

    @QueryProjection
    public MemberRankResponse(
            final UUID id,
            final String name,
            final String githubId,
            final Long tokens,
            final Tier tier,
            final String profileImage) {
        this.id = id;
        this.name = name;
        this.githubId = githubId;
        this.tokens = tokens;
        this.tier = tier;
        this.profileImage = profileImage;
    }
}
  • @QueryProjection 을 통해서 Q-Type 객체를 만들어 직접 사용한다.
/**
 * @author 김승진
 * @description 멤버 프로젝션 적용을 위한 클래스
 */
@Component
public class MemberQDtoFactory {

    @Bean
    public QMemberRankResponse qMemberRankResponse() {
        return new QMemberRankResponse(
                member.id,
                member.name,
                member.githubId,
                member.sumOfTokens,
                member.tier,
                member.profileImage);
    }
}
  • QDto를 제공해주는 팩토리 클래스를 활용해서 select()에 주입을 해주는 방식으로 구현되어있다.
@Override
public List<MemberRankResponse> findRanking(final Pageable pageable) {
    return jpaQueryFactory
            .select**(qDtoFactory.qMemberRankResponse()**)
            .from(member)
            .where(member.walletAddress.isNotNull())
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(memberOrderConverter.convert(pageable.getSort()))
            .fetch();
}

Reference

Spring Boot에 QueryDSL을 사용해보자

예제로 QueryDSL 사용해보기

[QueryDsl] Spring JPA와 QueryDsl-JPA

[Spring Boot] JPQL을 Query DSL로 리팩토링해보자!

[QueryDSL] return DTO

spring 게시판 기간조회 동적으로 처리하기 querydsl

profile
잘 살고 싶은 사람

1개의 댓글

comment-user-thumbnail
2024년 5월 15일

잘보고갑니다~

답글 달기