티스토리 뷰

Server

[Java] QueryDSL 에 대해 알아보자

니용 2021. 11. 10. 19:57
반응형

Querydsl 소개

Querydsl은 HQL(Hibernate Query Language) 쿼리를 타입에 안전하게 생성 및 관리할 수 있게 해주는 프레임워크” 입니다. 공식 레퍼런스를 인용한 정의인데, 잘 와닿지 않는다면 “Querydsl은 자바 코드 기반으로 쿼리를 작성하게 해준다”라고 생각하면 되겠습니다.

 

Querydsl은 왜 필요할까?

JPA를 사용한다고 가정하면 간단한 쿼리는 인터페이스에 메서드 명세만 잘 정의해 주면 별다른 문제 없이 사용할 수 있습니다. 예를 들면  “제목에 특정 문자열이 포함된 기사를 조회”하는 메서드를 표현하면 아래와 같습니다.

 

Optional<Article> findByTitleContains(String title);

 

그렇다면 조금 더 복잡한 쿼리가 필요한 경우에는 어떨까요? 앞서 살펴본 것처럼 단순히 특정 문자열이 제목에 포함된 기사를 조회하는 것이 아니라, 기사를 작성한 사용자의 레벨을 기준으로 조회하는 방법입니다.

이런 경우에는 JPA 자체 제공 메서드만으로 해결하기 어렵기 때문에 네이티브 쿼리(Native Query)를 고려해볼 수 있습니다. 다음은 레벨이 특정 기준 이상인 사용자가 작성한 기사들을 조회하는 메서드입니다.

@Query(value = "SELECT id, title, user_id FROM article WHERE user_id IN (SELECT id FROM user WHERE level > :level)", nativeQuery = true) 
List<Article> findByLevel(String level);

위에서 정의한 네이티브 쿼리를 다시 살펴보자면 가독성은 감안하더라도 문자열을 이어 붙여가며 직접 작성하기 때문에 오타가 발생하기 아주 좋습니다.

그렇다면 이 코드를 Querydsl로 변경하면 어떻게 될까요? 아직 Querydsl을 사용하는 방법에 대해서 알아보지 않았지만 어떤 모습일지 먼저 살펴보겠습니다. 다음은 위에서 살펴본 네이티브 쿼리와 동일한 쿼리를 수행하는 QueryDSL 예시입니다.

public List<Article> findByUserLevel(String level) {
    QArticle article = QArticle.article;
    QUser user = QUser.user;

    return queryFactory.selectFrom(article)
        .where(
            article.userId.in(
                JPAExpressions
                    .select(user.id)
                    .from(user)
                    .where(user.level.gt(level))
            )
        )
        .fetch();
}

앞서 살펴본 네이티브 쿼리보다 훨씬 가독성이 좋습니다. 바로 여기서 나오는 단점이 바로 소스 코드의 양이 많아진다는 것입니다. 

또한 메서드 타입에 맞지 않는 파라미터를 넘기는 경우 친절하게 컴파일 오류를 발생시켜 잠재적인 버그를 방지해줍니다. 즉, 실행 시점 이전에 잘못된 쿼리 파라미터 타입까지 확인할 수 있는 장점이 있습니다.

그렇다면 지금부터 Querydsl의 사용법에 대해서 알아보겠습니다.

 

Querydsl 관련 설정

예제는 2021년 7월 기준으로 다시 작성되었으며 전체 코드는 글 하단의 Github 저장소 링크를 참고해 주세요.

예제에서 사용된 프레임워크/라이브러리의 버전은 아래와 같습니다.

  • Spring Boot: 2.5.0
  • Gradle 7.1.1
  • Querydsl: 4.4.0
  • Lombok: 1.18.18

gradle 설정

먼저 다음과 같이 build.gradle 파일에 선언합니다. 프로젝트 구성에 필요한 일부 의존성은 생략하고 Querydsl 설정에 필요한 의존성만 나열하였습니다.

dependencies {
    
    implementation "com.querydsl:querydsl-core:${queryDslVersion}"
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"

    /*
     * `NoClassDefFoundError` 관련 대응으로 필요
     *  javax -> jakarta 로 이름 변경
     */
    annotationProcessor(
            "jakarta.persistence:jakarta.persistence-api",
            "jakarta.annotation:jakarta.annotation-api",
            "com.querydsl:querydsl-apt:${queryDslVersion}:jpa")
}

sourceSets {
    main {
        java {
            srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
        }
    }
}

 

Querydsl Config 설정

다음으로 Querydsl을 사용하기 위한 Config 설정을 진행합니다. 여기서 등록한 jpaQueryFactory 빈을 Repository에서 사용하게 됩니다.

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;

@Configuration
public class QuerydslConfig {
	@PersistenceContext
	private EntityManager entityManager;

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

 

Querydsl 사용법

Querydsl을 사용하기 위한 설정은 끝났습니다. 이제 Querydsl을 어떻게 사용하는지 알아보겠습니다.

Entity 클래스 정의

먼저 엔티티(Entity) 클래스를 정의합니다. 각각 article과 user 테이블에 매핑되는 엔티티입니다.

@Getter
@Entity
@Table(name = "article")
public class Article {
	@Id
	private Integer id;
	@Column(name = "user_id")
	private Integer userId;
	private String title;
}
@Getter
@Entity
@Table(name = "user")
public class User {
	@Id
	private Integer id;
	private String name;
	private String level;
}

 

Q클래스 생성

Querydsl은 컴파일 단계에서 엔티티를 기반으로 Q클래스 파일들을 생성하게 됩니다. 이 클래스를 기반으로 쿼리를 작성하게 됩니다.

Q클래스를 생성하려면 Gradle 옵션을 통해서 소스 코드를 컴파일시키면 됩니다. 즉, build task의 build 옵션을 실행하거나 단순히 Q클래스만 만들 목적이라면 other 태스크의 compileJava만 실행시키면 자동으로 생성합니다.

실행 후에는 아래와 같이 빌드 결과물에 Q클래스들이 생기게 됩니다.



Repository 정의

다음으로 실제 쿼리를 작성하고 수행할 Repository 레이어들을 만들어봅니다. JPA 인터페이스 메서드와 Querydsl 기반으로 사용할 메서드를 모두 사용하려고 합니다.

먼저 구현할 Querydsl 인터페이스인데 ~RepositoryCustom 이라는 네이밍을 갖도록 시그니처를 설정합니다.

/**
 * Querydsl로 작성할 쿼리는 이 곳에 시그니처를 선언하고 `~RepositoryImpl`에서 구현한다.
 */
public interface ArticleRepositoryCustom {
	List<Article> findByLevelUsingQuerydsl(String level);
}

 

다음으로 위에서 정의한 시그니처 기반으로 실제 동작을 정의할 구현체입니다. QuerydslConfig 클래스에서 등록한 JPAQueryFactory를 기반으로 쿼리를 작성하고 수행합니다. 메서드 네이밍은 임의로 “~UsingQuerydsl”라는 접미사를 붙였지만, 다른 사람들과 진행하는 프로젝트라면 컨벤션에 맞게 정의하면 되겠습니다.

@Repository
@RequiredArgsConstructor
public class ArticleRepositoryImpl implements ArticleRepositoryCustom {

	private final JPAQueryFactory queryFactory;

	public List<Article> findByLevelUsingQuerydsl(String level) {
		// Q클래스를 이용한다.
		QArticle article = QArticle.article;
		QUser user = QUser.user;

		return queryFactory.selectFrom(article)
			.where(
				article.userId.in(
					JPAExpressions
						.select(user.id)
						.from(user)
						.where(user.level.gt(level))
				)
			)
			.fetch();
	}
}

 

위와 같이 커스텀한 Repository은 네이밍 규약을 잘 지켜야 합니다. 별도의 설정을 하지 않았다면, ~Impl 접미사를 붙여야만 스프링이 와이어링 될 수 있습니다. 관련해서는 spring-data에 포함된 RepositoryConfigurationSourceSupport 클래스와 AnnotationRepositoryConfigurationSource 클래스의 내부 코드를 보면 알 수 있습니다.

마지막으로 JPA 인터페이스 메서드도 같이 사용할 수 있도록 인터페이스를 정의합니다. 아래 findByLevel은 Querydsl과 비교하기 위해 추가했습니다.

public interface ArticleRepository extends JpaRepository<Article, Integer>, ArticleRepositoryCustom {
	@Query(value = "SELECT * FROM article WHERE user_id IN (SELECT id FROM user WHERE level > :level)", nativeQuery = true)
	List<Article> findByLevel(String level);
}

 

동적 쿼리

Querydsl의 또 다른 장점으로 “동적 쿼리”를 뽑을 수 있습니다. 아래와 같이 코드 기반으로 메서드를 정의하여 조건식을 만들 수 있고, 전달되는 파라미터가 없어서 where 절에 null이 들어가는 경우 해당 조건은 생략됩니다.

public List<Article> searchArticle(String title, Integer userId) {
    return queryFactory.selectFrom(article)
        .where(titleContains(title), userIdEq(userId))
        .fetch();
}

private BooleanExpression titleContains(String title) {
    return StringUtils.isNotBlank(title) ? article.title.contains(title) : null;
}

private BooleanExpression userIdEq(Integer userId) {
    return userId != null ? article.userId.eq(userId) : null;
}

 

테스트 코드

@SpringBootTest
class ExampleApplicationTests {

	@Autowired
	private ArticleRepository articleRepository;

	@Test
	void testGetArticleList() {
		// Native Query
		List<Article> articleList = articleRepository.findByLevel("1");

		System.out.println("--------------------------------------------------");
		
		// Querydsl
		List<Article> articleListByQuerydsl = articleRepository.findByLevelUsingQuerydsl("1");

		Assertions.assertEquals(articleList.size(), articleListByQuerydsl.size());
	}
}

 

그 외 설정

아래의 설정을 application.properties 내에 넣으면 쿼리가 이쁘게 출력되는 것을 확인할 수 있습니다.

spring.jpa.properties.hibernate.show_sql=true # 실행된 쿼리 출력
spring.jpa.properties.hibernate.format_sql=true # 쿼리를 예쁘게 출력

결과

Hibernate: 
    SELECT
        id,
        title,
        user_id 
    FROM
        article 
    WHERE
        user_id IN (
            SELECT
                id 
            FROM
                user 
            WHERE
                level > ?
        )
--------------------------------------------------
Hibernate: 
    select
        article0_.id as id1_0_,
        article0_.title as title2_0_,
        article0_.user_id as user_id3_0_ 
    from
        article article0_ 
    where
        article0_.user_id in (
            select
                user1_.id 
            from
                user user1_ 
            where
                user1_.level>?
        )
반응형
댓글
공지사항