QueryDSL

김준석·2021년 2월 17일
3

Example-Code

QueryDSL이란?

Querydsl 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해 주는 프레임워크다.

Querydsl - 레퍼런스 문서

QueryDSL 왜 사용할까?

Querydsl은 타입에 안전한 방식으로 HQL 쿼리를 실행하기 위한 목적으로 만들어졌다. HQL 쿼리를 작성하다보면 String 연결을 이용하게 되고, 이는 결과적으로 읽기 어려운 코드를 만드는 문제를 야기한다. String을 이용해서 도메인 타입과 프로퍼티를 참조하다보면 오타 등으로 잘못된 참조를 하게 될 수 있으며, 이는 String을 이용해서 HQL 작성할 때 발생하는 또 다른 문제다.

타입에 안전하도록 도메인 모델을 변경하면 소프트웨어 개발에서 큰 이득을 얻게 된다. 도메인의 변경이 직접적으로 쿼리에 반영되고, 쿼리 작성 과정에서 코드 자동완성 기능을 사용함으로써 쿼리를 더 빠르고 안전하게 만들 수 있게 된다

Querydsl - 레퍼런스 문서

QueryDSL 사용

gradle 설정

plugins {
    // ...
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" // 추가
    // ...
}

// ...

dependencies {
    // ...
    implementation 'com.querydsl:querydsl-jpa' // 추가
    // ...
}

// ...

// queryDSL이 생성하는 QClass 경로 설정
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

sourceSets {
    main.java.srcDir querydslDir
}

configurations {
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

Q클래스 만들기

gradle 설정이 끝났으면 Q클래스를 만들어보자.

만드는 방법은 위의 그림과 같이 먼저 Gradle Project(View → Tool Windows → Gradle Project)를 열고 Tasks → other → compileJava를 실행시키면 build → generated에 Q클래스가 생성된다.

Config 설정

@Configuration
public class QueryDSLConfig {

    @PersistenceContext
    private EntityManager entityManager;

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

이제 JPAQueryFactory를 주입받아 QueryDSL을 사용할 수 있다.

사용법

1. 기본 사용법

Post 엔티티

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    // ...
}

PostRepository

public interface PostRepository extends JpaRepository<Post, Long> {
}

PostRepositorySupport

@Repository
public class PostRepositorySupport extends QuerydslRepositorySupport {

    private final JPAQueryFactory jpaQueryFactory;

    public PostRepositorySupport(final JPAQueryFactory jpaQueryFactory) {
        super(Post.class);
        this.jpaQueryFactory = jpaQueryFactory;
    }

    public List<Post> findByTitle(final String title) {
        return jpaQueryFactory.selectFrom(post)
                .where(post.title.eq(title))
                .fetch();
    }
}

selectFrom에 있는 post는 어디서 온 것일까? 아까 compileJava를 실행시켜서 만든 Q클래스에서 온 것이다.

이제 테스트를 해보자.

@Test
void findByTitle() {
    postRepository.saveAll(Arrays.asList(
            new Post("test", "content"),
            new Post("test", "content"),
            new Post("test", "content"),
            new Post("title1", "content"),
            new Post("title2", "content"),
            new Post("title3", "content")
    ));

    final List<Post> posts = postRepositorySupport.findByTitle("test");

    assertAll(
            () -> assertThat(posts).hasSize(3),
            () -> assertThat(posts.get(0).getTitle()).isEqualTo("test")
    );
}

2. Spring Data Jpa Custom Repository 적용

위와 같이 사용하면 항상 2개의 Repository(QueryDSL의 Custom Repository, JpaRepository를 상속한 Repository)를 의존성으로 받아야 한다.

이번에는 Custom Repository를 JpaRepository 상속 클래스에서 사용해보자.

CustomizedPostRepository

public interface CustomizedPostRepository {
    List<Post> findByTitle(final String title);
}

CustomizedPostRepositoryImpl

public class CustomizedPostRepositoryImpl implements CustomizedPostRepository {

    private final JPAQueryFactory jpaQueryFactory;

    private CustomizedPostRepositoryImpl(final JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public List<Post> findByTitle(final String title) {
        return jpaQueryFactory.selectFrom(post)
                .where(post.title.eq(title))
                .fetch();
    }
}

PostRepository

public interface PostRepository extends JpaRepository<Post, Long>, CustomizedPostRepository {
}

이렇게 구성하면 CustomizedPostRepositoryImpl의 코드를 사용할 수 있다. PostRepository는 어떻게 CustomizedPostRepository을 상속받아서 CustomizedPostRepositoryImpl의 코드를 사용할 수 있을까?

Spring 공식 문서를 보자. 요약하면 CustomizedRepository 인터페이스를 상속한 Impl 클래스의 코드를 당신의 RepositoryCustomizedRepository를 상속받아 사용할 수 있다고 한다. CustomizedRepository의 이름을 한 번 바꿔보았지만 잘 동작했다. 중요한 것은 Impl 접미사 같다.

The most important part of the class name that corresponds to the fragment interface is the Impl postfix.

Spring 공식 문서

이제 테스트해보자.

@Test
void findByTitle() {
    postRepository.saveAll(Arrays.asList(
            new Post("test", "content"),
            new Post("test", "content"),
            new Post("test", "content"),
            new Post("title1", "content"),
            new Post("title2", "content"),
            new Post("title3", "content")
    ));

    final List<Post> posts = postRepository.findByTitle("test");

    assertAll(
            () -> assertThat(posts).hasSize(3),
            () -> assertThat(posts.get(0).getTitle()).isEqualTo("test")
    );
}

3. 상속/구현 없는 Repository

QueryDSL만으로 Repository를 구현하는 방법이다.

PostQueryRepository

@Repository
public class PostQueryRepository {

    private final JPAQueryFactory jpaQueryFactory;

    public PostQueryRepository(final JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    public List<Post> findByTitle(final String title) {
        return jpaQueryFactory.selectFrom(post)
                .where(post.title.eq(title))
                .fetch();
    }
}

테스트해보자.

@Test
void findByTitle() {
    postRepository.saveAll(Arrays.asList(
            new Post("test", "content"),
            new Post("test", "content"),
            new Post("test", "content"),
            new Post("title1", "content"),
            new Post("title2", "content"),
            new Post("title3", "content")
    ));
    
    final List<Post> posts = postQueryRepository.findByTitle("test");

    assertAll(
    	    () -> assertThat(posts).hasSize(3),
    	    () -> assertThat(posts.get(0).getTitle()).isEqualTo("test")
    );
}

참고자료

Querydsl - 레퍼런스 문서

Spring Boot Data Jpa 프로젝트에 Querydsl 적용하기

profile
내 몸에는 꼰대의 피가 흐른다.

0개의 댓글